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

keplergl / kepler.gl / 24150959471

08 Apr 2026 06:09PM UTC coverage: 59.739% (-0.1%) from 59.866%
24150959471

push

github

web-flow
fix(effects): fixes for effects (#3368)

* fix: fixes for effects

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>

* lint

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>

---------

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>

6530 of 13032 branches covered (50.11%)

Branch coverage included in aggregate %.

18 of 79 new or added lines in 4 files covered. (22.78%)

4 existing lines in 2 files now uncovered.

13338 of 20226 relevant lines covered (65.94%)

78.14 hits per line

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

57.44
/src/utils/src/effect-utils.ts
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import SunCalc from 'suncalc';
5
import cloneDeep from 'lodash/cloneDeep';
6

7
import type {Effect as DeckEffect} from '@deck.gl/core';
8

9
import {
10
  LIGHT_AND_SHADOW_EFFECT,
11
  LIGHT_AND_SHADOW_EFFECT_TIME_MODES,
12
  FILTER_TYPES,
13
  FILTER_VIEW_TYPES,
14
  DISTANCE_FOG_TYPE,
15
  SURFACE_FOG_TYPE
16
} from '@kepler.gl/constants';
17
import {arrayMove} from '@kepler.gl/common-utils';
18
import {MapState, Effect, EffectProps, EffectDescription} from '@kepler.gl/types';
19
import {findById} from './utils';
20
import {clamp} from './data-utils';
21

22
// TODO isolate types - depends on @kepler.gl/schemas
23
type VisState = any;
24

25
// Retains the last LightingEffect deckEffect so we can keep it in the
26
// effects array (with shadows disabled) after the user removes the
27
// Light & Shadow effect from the UI. Without this, deck.gl calls
28
// cleanup() which removes the shadow shader module, but existing layer
29
// models still have shadow_uShadowMap bindings → texture errors.
30
let _lastLightingDeckEffect: any = null;
15✔
31

32
export function computeDeckEffects({
33
  visState,
34
  mapState,
35
  isExport
36
}: {
37
  visState: VisState;
38
  mapState: MapState;
39
  isExport?: boolean;
40
}): DeckEffect[] {
41
  // TODO: 1) deck effects per deck context 2) preserved between draws
42
  let hasLightingShadow = false;
2✔
43

44
  const deckEffects = visState.effectOrder
2✔
45
    .map(effectId => {
46
      const effect = findById(effectId)(visState.effects) as Effect | undefined;
4✔
47
      if (effect?.deckEffect) {
4!
48
        if (effect.isEnabled) {
4!
49
          updateEffect({visState, mapState, effect});
4✔
50
        } else if (effect.type === LIGHT_AND_SHADOW_EFFECT.type) {
×
51
          // Keep lighting effects in the array even when disabled to avoid
52
          // removing the shadow shader module. Composite layer sublayers
53
          // don't regenerate models when default shader modules change,
54
          // leaving stale pipelines with shadow_uShadowMap bindings.
55
          // Disabling shadow on the lights avoids visual effects.
56
          disableLightingEffect(effect);
×
57
        }
58
        if (effect.isEnabled || effect.type === LIGHT_AND_SHADOW_EFFECT.type) {
4!
59
          if (effect.type === LIGHT_AND_SHADOW_EFFECT.type) {
4✔
60
            hasLightingShadow = true;
2✔
61
            if (!isExport) {
2!
62
              _lastLightingDeckEffect = effect.deckEffect;
2✔
63
            }
64
          }
65
          return effect.deckEffect;
4✔
66
        }
67
      }
68
      return null;
×
69
    })
70
    .filter(effect => effect);
4✔
71

72
  if (!hasLightingShadow && _lastLightingDeckEffect) {
2!
73
    disableDeckLightingEffect(_lastLightingDeckEffect);
×
74
    deckEffects.unshift(_lastLightingDeckEffect);
×
75
  }
76

77
  return deckEffects;
2✔
78
}
79

80
/**
81
 * Always keep light & shadow effect at the top, then distance fog and
82
 * surface fog right after it (before other post-processing effects).
83
 * Both fog effects read the depth buffer from renderBuffers[0];
84
 * subsequent effects clear depth during their render passes, so fog
85
 * must run before that happens.
86
 */
87
export const fixEffectOrder = (effects: Effect[], effectOrder: string[]): string[] => {
15✔
88
  const lightShadowEffect = effects.find(effect => effect.type === LIGHT_AND_SHADOW_EFFECT.type);
52✔
89
  if (lightShadowEffect) {
26✔
90
    const ind = effectOrder.indexOf(lightShadowEffect.id);
7✔
91
    if (ind > 0) {
7✔
92
      effectOrder.splice(ind, 1);
1✔
93
      effectOrder.unshift(lightShadowEffect.id);
1✔
94
    }
95
  }
96

97
  const distanceFogEffect = effects.find(effect => effect.type === DISTANCE_FOG_TYPE);
53✔
98
  if (distanceFogEffect) {
26!
99
    const ind = effectOrder.indexOf(distanceFogEffect.id);
×
100
    const targetPos = lightShadowEffect ? 1 : 0;
×
101
    if (ind > targetPos) {
×
102
      effectOrder.splice(ind, 1);
×
103
      effectOrder.splice(targetPos, 0, distanceFogEffect.id);
×
104
    }
105
  }
106

107
  const surfaceFogEffect = effects.find(effect => effect.type === SURFACE_FOG_TYPE);
53✔
108
  if (surfaceFogEffect) {
26!
109
    const ind = effectOrder.indexOf(surfaceFogEffect.id);
×
110
    let targetPos = 0;
×
111
    if (lightShadowEffect) targetPos++;
×
112
    if (distanceFogEffect) targetPos++;
×
113
    if (ind > targetPos) {
×
114
      effectOrder.splice(ind, 1);
×
115
      effectOrder.splice(targetPos, 0, surfaceFogEffect.id);
×
116
    }
117
  }
118

119
  return effectOrder;
26✔
120
};
121

122
export function reorderEffectOrder(
123
  effectOrder: string[],
124
  originEffectId: string,
125
  destinationEffectId: string
126
): string[] {
127
  const activeIndex = effectOrder.indexOf(originEffectId);
×
128
  const overIndex = effectOrder.indexOf(destinationEffectId);
×
129
  return arrayMove(effectOrder, activeIndex, overIndex);
×
130
}
131

132
/**
133
 * Check if the current time is daytime at the given location
134
 * @param {number} lat Latitude
135
 * @param {number} lon Longitude
136
 * @param {number} timestamp Milliseconds since the Unix Epoch
137
 * @returns boolean
138
 */
139
function isDaytime(lat, lon, timestamp) {
140
  const date = new Date(timestamp);
2✔
141
  const {sunrise, sunset} = SunCalc.getTimes(date, lat, lon);
2✔
142
  return date >= sunrise && date <= sunset;
2✔
143
}
144

145
/**
146
 * Disable shadow rendering on a lighting effect without removing it.
147
 * This keeps the shadow shader module registered and prevents stale
148
 * texture binding errors in composite layer sublayers.
149
 */
150
function disableLightingEffect(effect: Effect) {
151
  const deckEffect = effect.deckEffect;
×
152
  if (!deckEffect) return;
×
153
  disableDeckLightingEffect(deckEffect);
×
154
}
155

156
/**
157
 * Disable shadow rendering directly on a deck.gl LightingEffect instance.
158
 */
159
function disableDeckLightingEffect(deckEffect: any) {
160
  deckEffect.shadow = false;
×
161
  deckEffect.outputUniformShadow = false;
×
162
  for (const light of deckEffect.directionalLights || []) {
×
163
    light.shadow = false;
×
164
  }
165
}
166

167
/**
168
 * Update effect to match latest vis and map states
169
 */
170
function updateEffect({visState, mapState, effect}) {
171
  if (effect.type === LIGHT_AND_SHADOW_EFFECT.type) {
4✔
172
    // Re-enable shadow rendering in case it was previously disabled
173
    const deckEffect = effect.deckEffect;
2✔
174
    for (const light of deckEffect.directionalLights || []) {
2!
175
      light.shadow = true;
2✔
176
    }
177
    deckEffect.shadow = deckEffect.directionalLights?.some(l => l.shadow) ?? false;
2!
178

179
    let {timestamp} = effect.parameters;
2✔
180
    const {timeMode} = effect.parameters;
2✔
181
    const sunLight = effect.deckEffect.directionalLights[0];
2✔
182

183
    // set timestamp for shadow
184
    if (timeMode === LIGHT_AND_SHADOW_EFFECT_TIME_MODES.current) {
2!
185
      timestamp = Date.now();
×
186
      sunLight.timestamp = timestamp;
×
187
    } else if (timeMode === LIGHT_AND_SHADOW_EFFECT_TIME_MODES.animation) {
2!
188
      timestamp = visState.animationConfig.currentTime ?? 0;
×
189
      if (!timestamp) {
×
190
        const filter = visState.filters.find(
×
191
          filter =>
192
            filter.type === FILTER_TYPES.timeRange &&
×
193
            (filter.view === FILTER_VIEW_TYPES.enlarged || filter.syncedWithLayerTimeline)
194
        );
195
        if (filter) {
×
196
          timestamp = filter.value?.[0] ?? 0;
×
197
        }
198
      }
199
      sunLight.timestamp = timestamp;
×
200
    }
201

202
    // output uniform shadow during nighttime
203
    if (isDaytime(mapState.latitude, mapState.longitude, timestamp)) {
2✔
204
      effect.deckEffect.outputUniformShadow = false;
1✔
205
      sunLight.intensity = effect.parameters.sunLightIntensity;
1✔
206
    } else {
207
      effect.deckEffect.outputUniformShadow = true;
1✔
208
      sunLight.intensity = 0;
1✔
209
    }
210
  }
211
}
212

213
/**
214
 * Validates parameters for an effect, clamps numbers to allowed ranges
215
 * or applies default values in case of wrong non-numeric values.
216
 * All unknown properties aren't modified.
217
 * @param parameters Parameters candidate for an effect.
218
 * @param effectDescription Description of an effect.
219
 * @returns
220
 */
221
export function validateEffectParameters(
222
  parameters: EffectProps['parameters'] = {},
26✔
223
  effectDescription: EffectDescription['parameters']
224
): EffectProps['parameters'] {
225
  const result = cloneDeep(parameters);
59✔
226
  effectDescription.forEach(description => {
59✔
227
    const {defaultValue, name, type, min, max} = description;
135✔
228

229
    if (!Object.prototype.hasOwnProperty.call(result, name)) return;
135✔
230
    const property = result[name];
20✔
231

232
    if (type === 'color' || type === 'array') {
20✔
233
      if (!Array.isArray(defaultValue)) return;
6!
234
      if (property.length !== defaultValue?.length) {
6✔
235
        result[name] = defaultValue;
4✔
236
        return;
4✔
237
      }
238
      defaultValue.forEach((v, i) => {
2✔
239
        let value = property[i];
5✔
240
        value = Number.isFinite(value) ? clamp([min, max], value) : defaultValue[i] ?? min;
5!
241
        if (value !== undefined) {
5!
242
          property[i] = value;
5✔
243
        }
244
      });
245
      return;
2✔
246
    }
247

248
    if (type === 'checkbox') {
14!
NEW
249
      result[name] = Boolean(property);
×
NEW
250
      return;
×
251
    }
252

253
    const value = Number.isFinite(property) ? clamp([min, max], property) : defaultValue ?? min;
14✔
254

255
    if (value !== undefined) {
14!
256
      result[name] = value;
14✔
257
    }
258
  });
259
  return result;
59✔
260
}
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