@deck.gl/core 9.3.0-alpha.1 → 9.3.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/debug.min.js +1 -1
  2. package/dist/controllers/controller.d.ts +5 -4
  3. package/dist/controllers/controller.d.ts.map +1 -1
  4. package/dist/controllers/controller.js +18 -7
  5. package/dist/controllers/controller.js.map +1 -1
  6. package/dist/controllers/first-person-controller.d.ts +3 -2
  7. package/dist/controllers/first-person-controller.d.ts.map +1 -1
  8. package/dist/controllers/first-person-controller.js +13 -5
  9. package/dist/controllers/first-person-controller.js.map +1 -1
  10. package/dist/controllers/globe-controller.d.ts +1 -0
  11. package/dist/controllers/globe-controller.d.ts.map +1 -1
  12. package/dist/controllers/globe-controller.js +66 -5
  13. package/dist/controllers/globe-controller.js.map +1 -1
  14. package/dist/controllers/map-controller.d.ts +7 -18
  15. package/dist/controllers/map-controller.d.ts.map +1 -1
  16. package/dist/controllers/map-controller.js +94 -50
  17. package/dist/controllers/map-controller.js.map +1 -1
  18. package/dist/controllers/orbit-controller.d.ts +12 -4
  19. package/dist/controllers/orbit-controller.d.ts.map +1 -1
  20. package/dist/controllers/orbit-controller.js +118 -10
  21. package/dist/controllers/orbit-controller.js.map +1 -1
  22. package/dist/controllers/orthographic-controller.d.ts +117 -9
  23. package/dist/controllers/orthographic-controller.d.ts.map +1 -1
  24. package/dist/controllers/orthographic-controller.js +302 -37
  25. package/dist/controllers/orthographic-controller.js.map +1 -1
  26. package/dist/controllers/terrain-controller.d.ts +29 -0
  27. package/dist/controllers/terrain-controller.d.ts.map +1 -0
  28. package/dist/controllers/terrain-controller.js +108 -0
  29. package/dist/controllers/terrain-controller.js.map +1 -0
  30. package/dist/controllers/view-state.d.ts +2 -1
  31. package/dist/controllers/view-state.d.ts.map +1 -1
  32. package/dist/controllers/view-state.js +2 -1
  33. package/dist/controllers/view-state.js.map +1 -1
  34. package/dist/debug/loggers.d.ts.map +1 -1
  35. package/dist/debug/loggers.js +1 -4
  36. package/dist/debug/loggers.js.map +1 -1
  37. package/dist/dist.dev.js +3800 -1675
  38. package/dist/effects/lighting/lighting-effect.d.ts +1 -0
  39. package/dist/effects/lighting/lighting-effect.d.ts.map +1 -1
  40. package/dist/effects/lighting/lighting-effect.js +14 -5
  41. package/dist/effects/lighting/lighting-effect.js.map +1 -1
  42. package/dist/index.cjs +775 -123
  43. package/dist/index.cjs.map +4 -4
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +1 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/lib/attribute/attribute-manager.d.ts.map +1 -1
  49. package/dist/lib/attribute/attribute-manager.js +2 -0
  50. package/dist/lib/attribute/attribute-manager.js.map +1 -1
  51. package/dist/lib/deck-picker.d.ts +6 -1
  52. package/dist/lib/deck-picker.d.ts.map +1 -1
  53. package/dist/lib/deck-picker.js +15 -3
  54. package/dist/lib/deck-picker.js.map +1 -1
  55. package/dist/lib/deck-renderer.d.ts +6 -1
  56. package/dist/lib/deck-renderer.d.ts.map +1 -1
  57. package/dist/lib/deck-renderer.js +14 -2
  58. package/dist/lib/deck-renderer.js.map +1 -1
  59. package/dist/lib/deck.d.ts +5 -0
  60. package/dist/lib/deck.d.ts.map +1 -1
  61. package/dist/lib/deck.js +13 -3
  62. package/dist/lib/deck.js.map +1 -1
  63. package/dist/lib/init.js +2 -2
  64. package/dist/lib/layer.d.ts.map +1 -1
  65. package/dist/lib/layer.js +1 -0
  66. package/dist/lib/layer.js.map +1 -1
  67. package/dist/passes/draw-layers-pass.d.ts +2 -0
  68. package/dist/passes/draw-layers-pass.d.ts.map +1 -1
  69. package/dist/passes/draw-layers-pass.js +3 -0
  70. package/dist/passes/draw-layers-pass.js.map +1 -1
  71. package/dist/passes/layers-pass.d.ts +2 -1
  72. package/dist/passes/layers-pass.d.ts.map +1 -1
  73. package/dist/passes/layers-pass.js +3 -0
  74. package/dist/passes/layers-pass.js.map +1 -1
  75. package/dist/passes/pick-layers-pass.d.ts +5 -2
  76. package/dist/passes/pick-layers-pass.d.ts.map +1 -1
  77. package/dist/passes/pick-layers-pass.js +3 -2
  78. package/dist/passes/pick-layers-pass.js.map +1 -1
  79. package/dist/shaderlib/project/project.glsl.d.ts.map +1 -1
  80. package/dist/shaderlib/project/project.glsl.js +3 -0
  81. package/dist/shaderlib/project/project.glsl.js.map +1 -1
  82. package/dist/utils/deep-merge.d.ts +5 -0
  83. package/dist/utils/deep-merge.d.ts.map +1 -0
  84. package/dist/utils/deep-merge.js +31 -0
  85. package/dist/utils/deep-merge.js.map +1 -0
  86. package/dist/utils/math-utils.d.ts +4 -0
  87. package/dist/utils/math-utils.d.ts.map +1 -1
  88. package/dist/utils/math-utils.js +8 -0
  89. package/dist/utils/math-utils.js.map +1 -1
  90. package/dist/viewports/globe-viewport.d.ts +1 -0
  91. package/dist/viewports/globe-viewport.d.ts.map +1 -1
  92. package/dist/viewports/globe-viewport.js +1 -1
  93. package/dist/viewports/globe-viewport.js.map +1 -1
  94. package/dist/viewports/orbit-viewport.d.ts.map +1 -1
  95. package/dist/viewports/orbit-viewport.js +7 -2
  96. package/dist/viewports/orbit-viewport.js.map +1 -1
  97. package/dist/viewports/orthographic-viewport.d.ts +8 -2
  98. package/dist/viewports/orthographic-viewport.d.ts.map +1 -1
  99. package/dist/viewports/orthographic-viewport.js.map +1 -1
  100. package/dist/views/orthographic-view.d.ts +38 -4
  101. package/dist/views/orthographic-view.d.ts.map +1 -1
  102. package/dist/views/orthographic-view.js.map +1 -1
  103. package/dist/views/view.d.ts.map +1 -1
  104. package/dist/views/view.js +2 -8
  105. package/dist/views/view.js.map +1 -1
  106. package/dist.min.js +220 -144
  107. package/package.json +9 -9
  108. package/src/controllers/controller.ts +23 -9
  109. package/src/controllers/first-person-controller.ts +18 -8
  110. package/src/controllers/globe-controller.ts +89 -5
  111. package/src/controllers/map-controller.ts +105 -56
  112. package/src/controllers/orbit-controller.ts +147 -13
  113. package/src/controllers/orthographic-controller.ts +417 -41
  114. package/src/controllers/terrain-controller.ts +146 -0
  115. package/src/controllers/view-state.ts +8 -1
  116. package/src/debug/loggers.ts +1 -5
  117. package/src/effects/lighting/lighting-effect.ts +20 -8
  118. package/src/index.ts +1 -0
  119. package/src/lib/attribute/attribute-manager.ts +1 -0
  120. package/src/lib/deck-picker.ts +18 -4
  121. package/src/lib/deck-renderer.ts +17 -3
  122. package/src/lib/deck.ts +19 -3
  123. package/src/lib/layer.ts +1 -0
  124. package/src/passes/draw-layers-pass.ts +5 -0
  125. package/src/passes/layers-pass.ts +5 -1
  126. package/src/passes/pick-layers-pass.ts +8 -4
  127. package/src/shaderlib/project/project.glsl.ts +3 -0
  128. package/src/utils/deep-merge.ts +33 -0
  129. package/src/utils/math-utils.ts +12 -0
  130. package/src/viewports/globe-viewport.ts +1 -1
  131. package/src/viewports/orbit-viewport.ts +8 -2
  132. package/src/viewports/orthographic-viewport.ts +8 -2
  133. package/src/views/orthographic-view.ts +38 -4
  134. package/src/views/view.ts +2 -8
@@ -3,66 +3,442 @@
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
5
  import {clamp} from '@math.gl/core';
6
- import Controller from './controller';
7
- import {OrbitState} from './orbit-controller';
6
+ import Controller, {ControllerProps} from './controller';
7
+ import ViewState from './view-state';
8
+
9
+ import type Viewport from '../viewports/viewport';
8
10
  import LinearInterpolator from '../transitions/linear-interpolator';
9
11
 
10
- class OrthographicState extends OrbitState {
11
- zoomAxis: 'X' | 'Y' | 'all';
12
+ export type OrthographicStateProps = {
13
+ width: number;
14
+ height: number;
15
+ target?: number[];
16
+ zoom?: number | number[];
17
+ zoomX?: number;
18
+ zoomY?: number;
19
+ zoomAxis?: 'X' | 'Y' | 'all';
20
+
21
+ /** Viewport constraints */
22
+ maxZoomX?: number;
23
+ minZoomX?: number;
24
+ maxZoomY?: number;
25
+ minZoomY?: number;
26
+
27
+ maxBounds?: ControllerProps['maxBounds'];
28
+ };
29
+
30
+ type OrthographicStateInternal = {
31
+ startPanPosition?: number[];
32
+ startZoomPosition?: number[];
33
+ startZoom?: number[];
34
+ };
35
+
36
+ function normalizeZoom({
37
+ zoom = 0,
38
+ zoomX,
39
+ zoomY
40
+ }: {
41
+ zoom?: number | number[];
42
+ zoomX?: number;
43
+ zoomY?: number;
44
+ }): {
45
+ zoomX: number;
46
+ zoomY: number;
47
+ } {
48
+ zoomX = zoomX ?? (Array.isArray(zoom) ? zoom[0] : zoom);
49
+ zoomY = zoomY ?? (Array.isArray(zoom) ? zoom[1] : zoom);
50
+ return {zoomX, zoomY};
51
+ }
52
+
53
+ export class OrthographicState extends ViewState<
54
+ OrthographicState,
55
+ OrthographicStateProps,
56
+ OrthographicStateInternal
57
+ > {
58
+ constructor(
59
+ options: OrthographicStateProps &
60
+ OrthographicStateInternal & {
61
+ maxZoom?: number;
62
+ minZoom?: number;
63
+ makeViewport: (props: Record<string, any>) => Viewport;
64
+ }
65
+ ) {
66
+ const {
67
+ /* Viewport arguments */
68
+ width, // Width of viewport
69
+ height, // Height of viewport
70
+ target = [0, 0, 0],
71
+ zoom = 0,
72
+ zoomAxis = 'all',
73
+
74
+ /* Viewport constraints */
75
+ minZoom = -Infinity,
76
+ maxZoom = Infinity,
77
+ minZoomX = minZoom,
78
+ maxZoomX = maxZoom,
79
+ minZoomY = minZoom,
80
+ maxZoomY = maxZoom,
81
+
82
+ maxBounds = null,
83
+
84
+ /** Interaction states, required to calculate change during transform */
85
+ // Model state when the pan operation first started
86
+ startPanPosition,
87
+ // Model state when the zoom operation first started
88
+ startZoomPosition,
89
+ startZoom
90
+ } = options;
91
+
92
+ const {zoomX, zoomY} = normalizeZoom(options);
93
+
94
+ super(
95
+ {
96
+ width,
97
+ height,
98
+ target,
99
+ zoom,
100
+ zoomX,
101
+ zoomY,
102
+ zoomAxis,
103
+ minZoomX,
104
+ maxZoomX,
105
+ minZoomY,
106
+ maxZoomY,
107
+ maxBounds
108
+ },
109
+ {
110
+ startPanPosition,
111
+ startZoomPosition,
112
+ startZoom
113
+ },
114
+ options.makeViewport
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Start panning
120
+ * @param {[Number, Number]} pos - position on screen where the pointer grabs
121
+ */
122
+ panStart({pos}: {pos: [number, number]}): OrthographicState {
123
+ return this._getUpdatedState({
124
+ startPanPosition: this._unproject(pos)
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Pan
130
+ * @param {[Number, Number]} pos - position on screen where the pointer is
131
+ */
132
+ pan({pos, startPosition}: {pos: [number, number]; startPosition?: number[]}): OrthographicState {
133
+ const startPanPosition = this.getState().startPanPosition || startPosition;
134
+
135
+ if (!startPanPosition) {
136
+ return this;
137
+ }
138
+
139
+ const viewport = this.makeViewport(this.getViewportProps());
140
+ const newProps = viewport.panByPosition(startPanPosition, pos);
141
+
142
+ return this._getUpdatedState(newProps);
143
+ }
144
+
145
+ /**
146
+ * End panning
147
+ * Must call if `panStart()` was called
148
+ */
149
+ panEnd(): OrthographicState {
150
+ return this._getUpdatedState({
151
+ startPanPosition: null
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Start rotating
157
+ */
158
+ rotateStart(): OrthographicState {
159
+ return this;
160
+ }
161
+
162
+ /**
163
+ * Rotate
164
+ */
165
+ rotate(): OrthographicState {
166
+ return this;
167
+ }
168
+
169
+ /**
170
+ * End rotating
171
+ */
172
+ rotateEnd(): OrthographicState {
173
+ return this;
174
+ }
175
+
176
+ // shortest path between two view states
177
+ shortestPathFrom(viewState: OrthographicState): OrthographicStateProps {
178
+ const fromProps = viewState.getViewportProps();
179
+ const props = {...this.getViewportProps()};
180
+ return props;
181
+ }
182
+
183
+ /**
184
+ * Start zooming
185
+ * @param {[Number, Number]} pos - position on screen where the pointer grabs
186
+ */
187
+ zoomStart({pos}: {pos: [number, number]}): OrthographicState {
188
+ const {zoomX, zoomY} = this.getViewportProps();
189
+ return this._getUpdatedState({
190
+ startZoomPosition: this._unproject(pos),
191
+ startZoom: [zoomX, zoomY]
192
+ });
193
+ }
194
+
195
+ /**
196
+ * Zoom
197
+ * @param {[Number, Number]} pos - position on screen where the current target is
198
+ * @param {[Number, Number]} startPos - the target position at
199
+ * the start of the operation. Must be supplied of `zoomStart()` was not called
200
+ * @param {Number} scale - a number between [0, 1] specifying the accumulated
201
+ * relative scale.
202
+ */
203
+ zoom({
204
+ pos,
205
+ startPos,
206
+ scale
207
+ }: {
208
+ pos: [number, number];
209
+ startPos?: [number, number];
210
+ scale: number;
211
+ }): OrthographicState {
212
+ let {startZoom, startZoomPosition} = this.getState();
213
+ if (!startZoomPosition) {
214
+ // We have two modes of zoom:
215
+ // scroll zoom that are discrete events (transform from the current zoom level),
216
+ // and pinch zoom that are continuous events (transform from the zoom level when
217
+ // pinch started).
218
+ // If startZoom state is defined, then use the startZoom state;
219
+ // otherwise assume discrete zooming
220
+ const {zoomX, zoomY} = this.getViewportProps();
221
+ startZoom = [zoomX, zoomY];
222
+ startZoomPosition = this._unproject(startPos || pos);
223
+ }
224
+ if (!startZoomPosition) {
225
+ return this;
226
+ }
227
+ const newZoomProps = this._constrainZoom(this._calculateNewZoom({scale, startZoom}));
228
+ const zoomedViewport = this.makeViewport({...this.getViewportProps(), ...newZoomProps});
229
+
230
+ return this._getUpdatedState({
231
+ ...newZoomProps,
232
+ ...zoomedViewport.panByPosition(startZoomPosition, pos)
233
+ });
234
+ }
235
+
236
+ /**
237
+ * End zooming
238
+ * Must call if `zoomStart()` was called
239
+ */
240
+ zoomEnd(): OrthographicState {
241
+ return this._getUpdatedState({
242
+ startZoomPosition: null,
243
+ startZoom: null
244
+ });
245
+ }
246
+
247
+ zoomIn(speed: number = 2): OrthographicState {
248
+ return this._getUpdatedState(this._calculateNewZoom({scale: speed}));
249
+ }
250
+
251
+ zoomOut(speed: number = 2): OrthographicState {
252
+ return this._getUpdatedState(this._calculateNewZoom({scale: 1 / speed}));
253
+ }
254
+
255
+ moveLeft(speed: number = 50): OrthographicState {
256
+ return this._panFromCenter([-speed, 0]);
257
+ }
258
+
259
+ moveRight(speed: number = 50): OrthographicState {
260
+ return this._panFromCenter([speed, 0]);
261
+ }
262
+
263
+ moveUp(speed: number = 50): OrthographicState {
264
+ return this._panFromCenter([0, -speed]);
265
+ }
266
+
267
+ moveDown(speed: number = 50): OrthographicState {
268
+ return this._panFromCenter([0, speed]);
269
+ }
270
+
271
+ rotateLeft(speed: number = 15): OrthographicState {
272
+ return this;
273
+ }
274
+
275
+ rotateRight(speed: number = 15): OrthographicState {
276
+ return this;
277
+ }
278
+
279
+ rotateUp(speed: number = 10): OrthographicState {
280
+ return this;
281
+ }
282
+
283
+ rotateDown(speed: number = 10): OrthographicState {
284
+ return this;
285
+ }
12
286
 
13
- constructor(props) {
14
- super(props);
287
+ /* Private methods */
15
288
 
16
- this.zoomAxis = props.zoomAxis || 'all';
289
+ _project(pos: number[]): number[] {
290
+ const viewport = this.makeViewport(this.getViewportProps());
291
+ return viewport.project(pos);
292
+ }
293
+ _unproject(pos: number[]): number[] {
294
+ const viewport = this.makeViewport(this.getViewportProps());
295
+ return viewport.unproject(pos);
17
296
  }
18
297
 
19
- _calculateNewZoom({scale, startZoom}) {
20
- const {maxZoom, minZoom} = this.getViewportProps();
298
+ // Calculates new zoom
299
+ _calculateNewZoom({scale, startZoom}: {scale: number; startZoom?: number[]}): {
300
+ zoomX: number;
301
+ zoomY: number;
302
+ } {
303
+ const {zoomX, zoomY, zoomAxis} = this.getViewportProps();
21
304
  if (startZoom === undefined) {
22
- startZoom = this.getViewportProps().zoom;
305
+ startZoom = [zoomX, zoomY];
306
+ }
307
+ const deltaZoom = Math.log2(scale);
308
+ let [newZoomX, newZoomY] = startZoom;
309
+ switch (zoomAxis) {
310
+ case 'X':
311
+ // Scale x only
312
+ newZoomX += deltaZoom;
313
+ break;
314
+ case 'Y':
315
+ // Scale y only
316
+ newZoomY += deltaZoom;
317
+ break;
318
+ default:
319
+ // Lock aspect ratio
320
+ newZoomX += deltaZoom;
321
+ newZoomY += deltaZoom;
322
+ }
323
+ return {
324
+ zoomX: newZoomX,
325
+ zoomY: newZoomY
326
+ };
327
+ }
328
+
329
+ _panFromCenter(offset) {
330
+ const {target} = this.getViewportProps();
331
+ const center = this._project(target);
332
+ return this.pan({
333
+ startPosition: target,
334
+ pos: [center[0] + offset[0], center[1] + offset[1]]
335
+ });
336
+ }
337
+
338
+ _getUpdatedState(newProps): OrthographicState {
339
+ // @ts-ignore
340
+ return new this.constructor({
341
+ makeViewport: this.makeViewport,
342
+ ...this.getViewportProps(),
343
+ ...this.getState(),
344
+ ...newProps
345
+ });
346
+ }
347
+
348
+ // Apply any constraints (mathematical or defined by _viewportProps) to map state
349
+ applyConstraints(props: Required<OrthographicStateProps>): Required<OrthographicStateProps> {
350
+ // Ensure zoom is within specified range
351
+ const {zoomX, zoomY} = this._constrainZoom(props, props);
352
+ props.zoomX = zoomX;
353
+ props.zoomY = zoomY;
354
+ // Backward compatibility: update zoom to reflect new view state
355
+ // zoom will always be ignored when zoomX and zoomY are specified, but legacy apps may still read zoom in `onViewStateChange`
356
+ props.zoom =
357
+ Array.isArray(props.zoom) || props.zoomX !== props.zoomY
358
+ ? [props.zoomX, props.zoomY]
359
+ : props.zoomX;
360
+
361
+ const {maxBounds, target} = props;
362
+ if (maxBounds) {
363
+ // only calculate center and zoom ranges at rotation=0
364
+ // to maintain visual stability when rotating
365
+ const halfWidth = props.width / 2 / 2 ** zoomX;
366
+ const halfHeight = props.height / 2 / 2 ** zoomY;
367
+ const minX = maxBounds[0][0] + halfWidth;
368
+ const maxX = maxBounds[1][0] - halfWidth;
369
+ const minY = maxBounds[0][1] + halfHeight;
370
+ const maxY = maxBounds[1][1] - halfHeight;
371
+ const x = clamp(target[0], minX, maxX);
372
+ const y = clamp(target[1], minY, maxY);
373
+ if (x !== target[0] || y !== target[1]) {
374
+ props.target = target.slice();
375
+ props.target[0] = x;
376
+ props.target[1] = y;
377
+ }
23
378
  }
24
- let deltaZoom = Math.log2(scale);
25
- if (Array.isArray(startZoom)) {
26
- let [newZoomX, newZoomY] = startZoom;
27
- switch (this.zoomAxis) {
28
- case 'X':
29
- // Scale x only
30
- newZoomX = clamp(newZoomX + deltaZoom, minZoom, maxZoom);
31
- break;
32
- case 'Y':
33
- // Scale y only
34
- newZoomY = clamp(newZoomY + deltaZoom, minZoom, maxZoom);
35
- break;
36
- default:
37
- // Lock aspect ratio
38
- let z = Math.min(newZoomX + deltaZoom, newZoomY + deltaZoom);
39
- if (z < minZoom) {
40
- deltaZoom += minZoom - z;
41
- }
42
- z = Math.max(newZoomX + deltaZoom, newZoomY + deltaZoom);
43
- if (z > maxZoom) {
44
- deltaZoom += maxZoom - z;
45
- }
46
- newZoomX += deltaZoom;
47
- newZoomY += deltaZoom;
379
+ return props;
380
+ }
381
+
382
+ _constrainZoom(
383
+ {zoomX, zoomY}: {zoomX: number; zoomY: number},
384
+ props?: Required<OrthographicStateProps>
385
+ ): {zoomX: number; zoomY: number} {
386
+ props ||= this.getViewportProps();
387
+ const {zoomAxis, maxZoomX, maxZoomY, maxBounds} = props;
388
+ let {minZoomX, minZoomY} = props;
389
+ const shouldApplyMaxBounds = maxBounds !== null && props.width > 0 && props.height > 0;
390
+
391
+ if (shouldApplyMaxBounds) {
392
+ const bl = maxBounds[0];
393
+ const tr = maxBounds[1];
394
+ const w = tr[0] - bl[0];
395
+ const h = tr[1] - bl[1];
396
+ // ignore bound size of 0 or Infinity
397
+ if (Number.isFinite(w) && w > 0) {
398
+ minZoomX = Math.max(minZoomX, Math.log2(props.width / w));
399
+ if (minZoomX > maxZoomX) minZoomX = maxZoomX;
48
400
  }
49
- return [newZoomX, newZoomY];
401
+ if (Number.isFinite(h) && h > 0) {
402
+ minZoomY = Math.max(minZoomY, Math.log2(props.height / h));
403
+ if (minZoomY > maxZoomY) minZoomY = maxZoomY;
404
+ }
405
+ }
406
+
407
+ switch (zoomAxis) {
408
+ case 'X':
409
+ zoomX = clamp(zoomX, minZoomX, maxZoomX);
410
+ break;
411
+ case 'Y':
412
+ zoomY = clamp(zoomY, minZoomY, maxZoomY);
413
+ break;
414
+ default:
415
+ // Lock aspect ratio
416
+ let delta = Math.min(maxZoomX - zoomX, maxZoomY - zoomY, 0);
417
+ if (delta === 0) {
418
+ delta = Math.max(minZoomX - zoomX, minZoomY - zoomY, 0);
419
+ }
420
+ if (delta !== 0) {
421
+ zoomX += delta;
422
+ zoomY += delta;
423
+ }
50
424
  }
51
- // Ignore `zoomAxis`
52
- // `LinearTransitionInterpolator` does not support interpolation between a number and an array
53
- // So if zoom is a number (legacy use case), new zoom still has to be a number
54
- return clamp(startZoom + deltaZoom, minZoom, maxZoom);
425
+ return {zoomX, zoomY};
55
426
  }
56
427
  }
57
428
 
58
- export default class OrthographicController extends Controller<OrbitState> {
429
+ export default class OrthographicController extends Controller<OrthographicState> {
59
430
  ControllerState = OrthographicState;
60
431
  transition = {
61
432
  transitionDuration: 300,
62
- transitionInterpolator: new LinearInterpolator(['target', 'zoom'])
433
+ transitionInterpolator: new LinearInterpolator(['target', 'zoomX', 'zoomY'])
63
434
  };
64
435
  dragMode: 'pan' | 'rotate' = 'pan';
65
436
 
437
+ setProps(props: ControllerProps & OrthographicStateProps) {
438
+ Object.assign(props, normalizeZoom(props));
439
+ super.setProps(props);
440
+ }
441
+
66
442
  _onPanRotate() {
67
443
  // No rotation in orthographic view
68
444
  return false;
@@ -0,0 +1,146 @@
1
+ // deck.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import MapController from './map-controller';
6
+ import {MapState, MapStateProps} from './map-controller';
7
+ import type {ControllerProps, InteractionState} from './controller';
8
+ import type {MjolnirGestureEvent, MjolnirWheelEvent} from 'mjolnir.js';
9
+
10
+ /**
11
+ * Controller that extends MapController with terrain-aware behavior.
12
+ * The camera smoothly follows terrain elevation during pan/zoom.
13
+ */
14
+ export default class TerrainController extends MapController {
15
+ /** Cached terrain altitude from depth picking at viewport center (smoothed) */
16
+ private _terrainAltitude?: number = undefined;
17
+ /** Raw (unsmoothed) terrain altitude from latest pick */
18
+ private _terrainAltitudeTarget?: number = undefined;
19
+
20
+ setProps(
21
+ props: ControllerProps &
22
+ MapStateProps & {
23
+ rotationPivot?: 'center' | '2d' | '3d';
24
+ getAltitude?: (pos: [number, number]) => number | undefined;
25
+ }
26
+ ) {
27
+ super.setProps({rotationPivot: '3d', ...props});
28
+
29
+ // Drive smoothing animation when terrain altitude hasn't converged yet.
30
+ if (
31
+ this._terrainAltitude !== undefined &&
32
+ this._terrainAltitudeTarget !== undefined &&
33
+ Math.abs(this._terrainAltitudeTarget - this._terrainAltitude) > 0.01
34
+ ) {
35
+ this.updateViewport(
36
+ new this.ControllerState({
37
+ makeViewport: this.makeViewport,
38
+ ...this.props,
39
+ ...this.state
40
+ } as any)
41
+ );
42
+ }
43
+ }
44
+
45
+ protected updateViewport(
46
+ newControllerState: MapState,
47
+ extraProps: Record<string, any> | null = null,
48
+ interactionState: InteractionState = {}
49
+ ): void {
50
+ const SMOOTHING = 0.05;
51
+
52
+ // No interactions yet, do not update
53
+ if (this._terrainAltitudeTarget === undefined) return;
54
+
55
+ if (this._terrainAltitude === undefined) {
56
+ // First interaction, rebase to avoid jump
57
+ this._terrainAltitude = this._terrainAltitudeTarget;
58
+ extraProps = this._rebaseViewport(
59
+ this._terrainAltitudeTarget,
60
+ newControllerState,
61
+ extraProps
62
+ );
63
+ } else {
64
+ // Standard interaction, smoothly blend target into actual altitude
65
+ this._terrainAltitude += (this._terrainAltitudeTarget - this._terrainAltitude) * SMOOTHING;
66
+ }
67
+
68
+ const viewportProps = newControllerState.getViewportProps();
69
+ const pos = viewportProps.position || [0, 0, 0];
70
+ extraProps = {
71
+ ...extraProps,
72
+ position: [pos[0], pos[1], this._terrainAltitude]
73
+ };
74
+
75
+ super.updateViewport(newControllerState, extraProps, interactionState);
76
+ }
77
+
78
+ protected _onPanStart(event: MjolnirGestureEvent): boolean {
79
+ this._pickTerrainCenterAltitude();
80
+ return super._onPanStart(event);
81
+ }
82
+
83
+ protected _onWheel(event: MjolnirWheelEvent): boolean {
84
+ this._pickTerrainCenterAltitude();
85
+ return super._onWheel(event);
86
+ }
87
+
88
+ protected _onDoubleClick(event: MjolnirGestureEvent): boolean {
89
+ this._pickTerrainCenterAltitude();
90
+ return super._onDoubleClick(event);
91
+ }
92
+
93
+ private _pickTerrainCenterAltitude(): void {
94
+ // TODO use async picking?
95
+ if (!this.pickPosition) {
96
+ return;
97
+ }
98
+ const {x, y, width, height} = this.props;
99
+ const pickResult = this.pickPosition(x + width / 2, y + height / 2);
100
+ if (pickResult?.coordinate && pickResult.coordinate.length >= 3) {
101
+ this._terrainAltitudeTarget = pickResult.coordinate[2];
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Utility function to return viewport that looks the same, but with
107
+ * a position shifted to [0, 0, altitude]
108
+ */
109
+ private _rebaseViewport(
110
+ altitude: number,
111
+ newControllerState: MapState,
112
+ extraProps: Record<string, any> | null
113
+ ): Record<string, any> | null {
114
+ const viewportProps = newControllerState.getViewportProps();
115
+ const oldViewport = this.makeViewport({...viewportProps, position: [0, 0, 0]});
116
+ const oldCameraPos = oldViewport.cameraPosition;
117
+
118
+ const centerZOffset = altitude * oldViewport.distanceScales.unitsPerMeter[2];
119
+ const cameraHeightAboveOldCenter = oldCameraPos[2];
120
+ const newCameraHeightAboveCenter = cameraHeightAboveOldCenter - centerZOffset;
121
+ if (newCameraHeightAboveCenter <= 0) {
122
+ return extraProps;
123
+ }
124
+
125
+ const zoomDelta = Math.log2(cameraHeightAboveOldCenter / newCameraHeightAboveCenter);
126
+ const newZoom = viewportProps.zoom + zoomDelta;
127
+
128
+ const newViewport = this.makeViewport({
129
+ ...viewportProps,
130
+ zoom: newZoom,
131
+ position: [0, 0, altitude]
132
+ });
133
+ const {width, height} = viewportProps;
134
+ const screenCenter: [number, number] = [width / 2, height / 2];
135
+ const worldPoint = oldViewport.unproject(screenCenter, {targetZ: altitude});
136
+ if (
137
+ worldPoint &&
138
+ 'panByPosition3D' in newViewport &&
139
+ typeof newViewport.panByPosition3D === 'function'
140
+ ) {
141
+ const adjusted = newViewport.panByPosition3D(worldPoint, screenCenter);
142
+ return {...extraProps, position: [0, 0, altitude], zoom: newZoom, ...adjusted};
143
+ }
144
+ return extraProps;
145
+ }
146
+ }
@@ -13,7 +13,14 @@ export default abstract class ViewState<
13
13
  private _viewportProps: Required<Props>;
14
14
  private _state: State;
15
15
 
16
- constructor(props: Required<Props>, state: State) {
16
+ makeViewport: (props: Record<string, any>) => Viewport;
17
+
18
+ constructor(
19
+ props: Required<Props>,
20
+ state: State,
21
+ makeViewport: (props: Record<string, any>) => Viewport
22
+ ) {
23
+ this.makeViewport = makeViewport;
17
24
  this._viewportProps = this.applyConstraints(props);
18
25
  this._state = state;
19
26
  }
@@ -123,7 +123,7 @@ export const getLoggers = (log: Log): Record<string, Function> => ({
123
123
  /* Render events */
124
124
 
125
125
  'deckRenderer.renderLayers': (deckRenderer, renderStats, opts) => {
126
- const {pass, redrawReason, stats} = opts;
126
+ const {pass, redrawReason} = opts;
127
127
  for (const status of renderStats) {
128
128
  const {totalCount, visibleCount, compositeCount, pickableCount} = status;
129
129
  const primitiveCount = totalCount - compositeCount;
@@ -135,10 +135,6 @@ export const getLoggers = (log: Log): Record<string, Function> => ({
135
135
  ${visibleCount} (of ${totalCount} layers) to ${pass} because ${redrawReason} \
136
136
  (${hiddenCount} hidden, ${compositeCount} composite ${pickableCount} pickable)`
137
137
  )();
138
-
139
- if (stats) {
140
- stats.get('Redraw Layers').add(visibleCount);
141
- }
142
138
  }
143
139
  }
144
140
  });