@deck.gl/arcgis 9.3.4 → 9.3.6

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.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Use deck.gl as a custom ArcGIS API for JavaScript layer",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
- "version": "9.3.4",
6
+ "version": "9.3.6",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -48,5 +48,5 @@
48
48
  "devDependencies": {
49
49
  "@arcgis/core": "^4.21.0"
50
50
  },
51
- "gitHead": "c489a387f9c314a3d381111176a5d65c9fd74e87"
51
+ "gitHead": "9e8e923e9408824d3537cee6d071777fd299074b"
52
52
  }
package/src/commons.ts CHANGED
@@ -19,7 +19,6 @@ export type RenderResources = {
19
19
  texture: Texture;
20
20
  model: Model;
21
21
  fbo: Framebuffer;
22
- _externalFramebuffer?: {handle: WebGLFramebuffer; wrapper: Framebuffer};
23
22
  };
24
23
 
25
24
  async function createDeckInstance(gl: WebGL2RenderingContext): Promise<{
@@ -87,7 +86,9 @@ out vec4 fragColor;
87
86
 
88
87
  void main(void) {
89
88
  vec4 imageColor = texture(deckglTexture, v_texcoord);
90
- imageColor.rgb *= imageColor.a;
89
+ // FBO stores premultiplied RGBA (rgb already multiplied by alpha).
90
+ // The composite blend (ONE, ONE_MINUS_SRC_ALPHA) handles premultiplied
91
+ // input correctly; multiplying again here would darken overlays.
91
92
  fragColor = imageColor;
92
93
  }
93
94
  `,
@@ -95,8 +96,8 @@ void main(void) {
95
96
  deckglTexture: texture
96
97
  },
97
98
  parameters: {
98
- depthWriteEnabled: true,
99
- depthCompare: 'less-equal',
99
+ depthWriteEnabled: false,
100
+ depthCompare: 'always',
100
101
  blendColorSrcFactor: 'one',
101
102
  blendColorDstFactor: 'one-minus-src-alpha',
102
103
  blendAlphaSrcFactor: 'one',
@@ -128,6 +129,18 @@ void main(void) {
128
129
 
129
130
  _customRender: redrawReason => {
130
131
  if (redrawReason === 'arcgis') {
132
+ // ArcGIS renders with alphaSrc=ZERO (preserves destination alpha for its
133
+ // own compositing pipeline). Without resetting this, deck layers inherit
134
+ // that blend state and write alpha=0 into the FBO, making the composite
135
+ // shader output (0,0,0,0) everywhere. Reset to standard premultiplied-alpha
136
+ // blend so the FBO stores correct RGBA for the composite pass.
137
+ const glCtx = (device as WebGLDevice).gl;
138
+ glCtx.blendFuncSeparate(
139
+ glCtx.ONE,
140
+ glCtx.ONE_MINUS_SRC_ALPHA,
141
+ glCtx.ONE,
142
+ glCtx.ONE_MINUS_SRC_ALPHA
143
+ );
131
144
  deckInstance._drawLayers(redrawReason);
132
145
  } else {
133
146
  this.redraw();
@@ -149,44 +162,81 @@ export function render(
149
162
  altitude?: number;
150
163
  pitch: number;
151
164
  bearing: number;
165
+ views?: unknown;
166
+ viewState?: unknown;
152
167
  }
153
168
  ) {
154
169
  const {model, deck, fbo} = resources;
155
170
  const device = model.device;
156
171
  if (device instanceof WebGLDevice) {
157
- const {width, height, ...viewState} = viewport;
172
+ // @ts-ignore device.getParametersWebGL should return `any` not `void`?
173
+ const rawScreenFbo: WebGLFramebuffer | null = device.getParametersWebGL(GL.FRAMEBUFFER_BINDING);
174
+ const {width, height, views, viewState, ...defaultViewState} = viewport;
158
175
 
159
176
  /* global window */
160
177
  const dpr = window.devicePixelRatio;
161
178
  const pixelWidth = Math.round(width * dpr);
162
179
  const pixelHeight = Math.round(height * dpr);
163
180
 
164
- // Wrap the external framebuffer handle so luma.gl treats it as a proper Framebuffer resource.
165
- const externalFbo = device.getParametersWebGL(GL.FRAMEBUFFER_BINDING);
166
- let screenFbo: Framebuffer | null = null;
167
- if (externalFbo) {
168
- if (resources._externalFramebuffer?.handle !== externalFbo) {
169
- resources._externalFramebuffer?.wrapper.destroy();
170
- const wrapper = device.createFramebuffer({
171
- handle: externalFbo,
172
- width: pixelWidth,
173
- height: pixelHeight
174
- });
175
- resources._externalFramebuffer = {handle: externalFbo, wrapper};
176
- }
177
- screenFbo = resources._externalFramebuffer!.wrapper;
178
- }
179
-
180
181
  fbo.resize({width: pixelWidth, height: pixelHeight});
181
182
 
182
- deck.setProps({viewState});
183
+ // luma's Framebuffer.resize() clones and destroys the color attachment texture when
184
+ // dimensions change, leaving the cached texture reference and the model sampler binding
185
+ // pointing at a destroyed GPU handle. Re-sync if the attachment was replaced.
186
+ const currentTexture =
187
+ (fbo.colorAttachments[0] as any).texture ?? (fbo.colorAttachments[0] as unknown as Texture);
188
+ if (currentTexture !== resources.texture) {
189
+ resources.texture = currentTexture;
190
+ (model as any).setBindings({deckglTexture: currentTexture});
191
+ }
192
+
193
+ // Pass CSS pixel dimensions — deck handles DPR internally. Passing physical
194
+ // pixels would double-apply DPR and project layer geometry off-screen.
195
+ // Without width/height, deck's viewport aspect diverges from ArcGIS's,
196
+ // causing the overlay to drift off the ground plane under tilt/rotation.
197
+ const deckProps: any = {
198
+ width,
199
+ height,
200
+ viewState: viewState || defaultViewState
201
+ };
202
+ if (views) {
203
+ deckProps.views = views;
204
+ }
205
+ deck.setProps(deckProps);
183
206
  // redraw deck immediately into deckFbo
184
207
  deck.redraw('arcgis');
185
208
 
186
209
  // We overlay the texture on top of the map using the full-screen quad.
210
+ const {gl} = device;
211
+
212
+ // drawBuffers is per-FBO state not tracked by luma's state cache;
213
+ // restore it for the screen FBO after rendering to the offscreen FBO.
214
+ if (rawScreenFbo) {
215
+ gl.bindFramebuffer(GL.FRAMEBUFFER, rawScreenFbo);
216
+ gl.drawBuffers([GL.COLOR_ATTACHMENT0]);
217
+ } else {
218
+ gl.drawBuffers([GL.BACK]);
219
+ }
220
+
221
+ // The model sets blend factors but not blend:true, so
222
+ // setDeviceParameters won't enable blending on its own.
223
+ gl.enable(GL.BLEND);
224
+ gl.blendFuncSeparate(GL.ONE, GL.ONE_MINUS_SRC_ALPHA, GL.ONE, GL.ONE_MINUS_SRC_ALPHA);
225
+ gl.blendEquationSeparate(GL.FUNC_ADD, GL.FUNC_ADD);
226
+
227
+ // luma's WEBGLRenderPass duck-types the framebuffer prop, reading `.handle`, `.width`,
228
+ // `.height`, and `.colorAttachments`. A raw WebGLFramebuffer has none of those, which
229
+ // breaks viewport auto-setup and draw-buffer selection. Build a minimal adapter that
230
+ // luma's state tracker accepts; `'handle' in obj` is true, so the raw FBO is bound.
231
+ const screenFboAdapter = {
232
+ handle: rawScreenFbo,
233
+ width: pixelWidth,
234
+ height: pixelHeight,
235
+ colorAttachments: [null]
236
+ };
187
237
 
188
238
  const textureToScreenPass = device.beginRenderPass({
189
- framebuffer: screenFbo,
239
+ framebuffer: screenFboAdapter as any,
190
240
  parameters: {viewport: [0, 0, pixelWidth, pixelHeight]},
191
241
  clearColor: false,
192
242
  clearDepth: false
@@ -204,5 +254,4 @@ export function finalizeResources(resources: RenderResources) {
204
254
  resources.model.destroy();
205
255
  resources.fbo.destroy();
206
256
  resources.texture.destroy();
207
- resources._externalFramebuffer?.wrapper.destroy();
208
257
  }
@@ -7,69 +7,262 @@ import {initializeResources, render, finalizeResources, RenderResources} from '.
7
7
 
8
8
  import SceneView from '@arcgis/core/views/SceneView';
9
9
  import type {DeckProps} from '@deck.gl/core';
10
+ import MapView from '../../core/src/views/map-view';
10
11
 
11
- /** Convert ArcGIS FOV to WebMercatorViewport altitude */
12
- function arcgisFOVToDeckAltitude(fov: number, aspectRatio: number): number {
13
- // ArcGIS fov is corner-to-corner
14
- const D = Math.sqrt(1 + aspectRatio ** 2);
15
- const halfFOV = (fov / 2 / 180) * Math.PI;
16
- return D / 2 / Math.tan(halfFOV);
12
+ // Web Mercator scale at zoom 0 for 256 px tiles; deck.gl uses 512 px tiles,
13
+ // so the final zoom is shifted by -1.
14
+ const ARCGIS_WEB_MERCATOR_SCALE_AT_ZOOM_0 = 591657550.5;
15
+ const DECK_GROUND_MPP_AT_ZOOM_0 = 78271.484;
16
+ const METERS_PER_DEG_LAT = 111320;
17
+ const ARCGIS_DIAGONAL_FOV_RADIANS = (55 * Math.PI) / 180;
18
+ const ALTITUDE_BLEND_START_TILT = 65;
19
+ const ALTITUDE_BLEND_END_TILT = 80;
20
+
21
+ function getDistanceMeters(a, b) {
22
+ const midLatRad = (((a.latitude + b.latitude) / 2) * Math.PI) / 180;
23
+ const dLatM = (a.latitude - b.latitude) * METERS_PER_DEG_LAT;
24
+ const dLngM = (a.longitude - b.longitude) * METERS_PER_DEG_LAT * Math.cos(midLatRad);
25
+
26
+ return Math.sqrt(dLatM * dLatM + dLngM * dLngM);
27
+ }
28
+
29
+ function getZoom(
30
+ view: SceneView,
31
+ longitude: number,
32
+ latitude: number,
33
+ width: number,
34
+ height: number
35
+ ) {
36
+ const focalPoint = view.toMap({x: width / 2, y: height / 2} as any);
37
+ const focalPointRight = view.toMap({x: width / 2 + 1, y: height / 2} as any);
38
+
39
+ if (focalPoint && focalPointRight) {
40
+ const horizontalMetersPerPixel = getDistanceMeters(focalPoint, focalPointRight);
41
+
42
+ if (horizontalMetersPerPixel > 0) {
43
+ const latitudeRad = (latitude * Math.PI) / 180;
44
+ return Math.log2(
45
+ (DECK_GROUND_MPP_AT_ZOOM_0 * Math.cos(latitudeRad)) / horizontalMetersPerPixel
46
+ );
47
+ }
48
+ }
49
+
50
+ return Math.log2(ARCGIS_WEB_MERCATOR_SCALE_AT_ZOOM_0 / view.scale) - 1;
17
51
  }
18
52
 
19
- export default function createDeckRenderer(DeckProps, externalRenderers) {
53
+ function smoothstep(edge0: number, edge1: number, value: number) {
54
+ const t = Math.max(0, Math.min(1, (value - edge0) / (edge1 - edge0)));
55
+ return t * t * (3 - 2 * t);
56
+ }
57
+
58
+ function getFovMatchedAltitude(width: number, height: number) {
59
+ const aspect = width / height;
60
+ const verticalTan = Math.tan(ARCGIS_DIAGONAL_FOV_RADIANS / 2) / Math.sqrt(1 + aspect * aspect);
61
+
62
+ return 0.5 / verticalTan;
63
+ }
64
+
65
+ function getAltitude(tilt: number, slantAltitude: number, width: number, height: number) {
66
+ const fovAltitude = getFovMatchedAltitude(width, height);
67
+ const blend = smoothstep(ALTITUDE_BLEND_START_TILT, ALTITUDE_BLEND_END_TILT, tilt);
68
+
69
+ return slantAltitude * (1 - blend) + fovAltitude * blend;
70
+ }
71
+
72
+ function getArcGISMatrixViewState(
73
+ view: SceneView,
74
+ longitude: number,
75
+ latitude: number,
76
+ zoom: number,
77
+ resolution: number,
78
+ altitude: number
79
+ ) {
80
+ return {
81
+ views: new MapView({id: 'arcgis-scene'}),
82
+ viewState: {
83
+ 'arcgis-scene': {
84
+ id: 'arcgis-scene',
85
+ longitude,
86
+ latitude,
87
+ zoom,
88
+ pitch: view.camera.tilt,
89
+ bearing: view.camera.heading,
90
+ altitude,
91
+ position: [0, 0, 0],
92
+ resolution
93
+ }
94
+ }
95
+ };
96
+ }
97
+
98
+ export default function createDeckRenderer(DeckProps, RenderNode) {
20
99
  class DeckRenderer {
21
100
  view: SceneView;
22
101
  deck: any;
23
102
  resources: RenderResources | null = null;
24
103
  cancelInitialization: (() => void) | null = null;
104
+ renderNode: any = null;
25
105
 
26
- constructor(view: SceneView, props: DeckProps) {
27
- this.view = view;
28
- this.deck = new DeckProps(props);
29
- }
106
+ constructor(viewOrProps: SceneView | ({view: SceneView} & DeckProps), props?: DeckProps) {
107
+ if (viewOrProps && typeof viewOrProps === 'object' && 'view' in viewOrProps) {
108
+ const {view, ...deckProps} = viewOrProps;
109
+ this.view = view;
110
+ this.deck = new DeckProps(deckProps);
111
+ } else {
112
+ this.view = viewOrProps;
113
+ this.deck = new DeckProps(props);
114
+ }
30
115
 
31
- async setup(context) {
32
- const gl = context.gl;
116
+ let isInitializing = false;
33
117
 
34
- let cancelled = false;
35
- this.cancelInitialization = () => (cancelled = true);
36
- const resources = await initializeResources.call(this, gl);
37
- // If the renderer got disposed while awaiting, do not proceed
38
- if (cancelled) {
39
- finalizeResources(resources);
40
- return;
41
- }
118
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
119
+ const self = this;
120
+ const DeckRenderNode = RenderNode.createSubclass({
121
+ consumes: {required: ['composite-color']},
122
+ produces: 'composite-color',
123
+
124
+ initialize() {},
125
+
126
+ render(inputs: Array<{name: string}>) {
127
+ // biome-ignore lint/complexity/noThisInStatic: RenderNode render is an instance method
128
+ // eslint-disable-next-line @typescript-eslint/no-this-alias, consistent-this
129
+ const renderNode: any = this;
130
+ const passthrough = inputs.find(i => i.name === 'composite-color');
131
+
132
+ if (!self.resources && !isInitializing) {
133
+ const gl = renderNode.gl;
134
+ if (gl) {
135
+ isInitializing = true;
136
+ let cancelled = false;
137
+ self.cancelInitialization = () => {
138
+ cancelled = true;
139
+ };
140
+ initializeResources
141
+ .call(self, gl)
142
+ .then(resources => {
143
+ isInitializing = false;
144
+ if (cancelled) {
145
+ finalizeResources(resources);
146
+ return;
147
+ }
148
+ self.deck.on('change', deckProps => resources.deck.setProps(deckProps));
149
+ resources.deck.setProps(self.deck.toJSON());
150
+ self.resources = resources;
151
+ renderNode.requestRender();
152
+ })
153
+ .catch(err => {
154
+ isInitializing = false;
155
+ // eslint-disable-next-line no-console
156
+ console.error('DeckRenderer: failed to initialize resources', err);
157
+ });
158
+ }
159
+ return passthrough;
160
+ }
161
+ if (!self.resources) {
162
+ return passthrough;
163
+ }
164
+
165
+ const output = renderNode.bindRenderTarget();
166
+ const gl = renderNode.gl as WebGL2RenderingContext;
42
167
 
43
- this.deck.on('change', props => resources.deck.setProps(props));
168
+ const dpr = window.devicePixelRatio || 1;
169
+ const width = gl.drawingBufferWidth / dpr;
170
+ const height = gl.drawingBufferHeight / dpr;
44
171
 
45
- resources.deck.setProps(this.deck.toJSON());
46
- this.resources = resources;
172
+ // Keep deck.gl's SceneView approximation anchored at the ArcGIS
173
+ // focal point every frame. Zoom comes from the actual horizontal
174
+ // ground meters-per-pixel at screen center (`toMap(center)` and
175
+ // `toMap(center + 1px)`), which stays stable under tilt in local
176
+ // SceneView. We keep the older `view.scale` formula only as a
177
+ // fallback when focal-point sampling is unavailable.
178
+ //
179
+ // deck.gl's `altitude` still couples camera distance and FOV, while
180
+ // ArcGIS keeps them separate. We therefore compute a slant-distance
181
+ // altitude from the live camera position and focal point, then only
182
+ // at high tilt blend toward the ArcGIS fixed-FOV altitude. That
183
+ // preserves the better mid-tilt camera match while reducing the
184
+ // remaining extreme-angle drift.
185
+ const cameraPos = self.view.camera.position;
186
+ const focalPoint = self.view.toMap({x: width / 2, y: height / 2} as any);
187
+ const latitude = focalPoint ? focalPoint.latitude : self.view.center.latitude;
188
+ const longitude = focalPoint ? focalPoint.longitude : self.view.center.longitude;
189
+
190
+ const midLatRad = (((latitude + cameraPos.latitude) / 2) * Math.PI) / 180;
191
+ const dLatM = (cameraPos.latitude - latitude) * METERS_PER_DEG_LAT;
192
+ const dLngM =
193
+ (cameraPos.longitude - longitude) * METERS_PER_DEG_LAT * Math.cos(midLatRad);
194
+ const horizM = Math.sqrt(dLatM * dLatM + dLngM * dLngM);
195
+ const slantM = Math.sqrt(horizM * horizM + cameraPos.z * cameraPos.z);
196
+
197
+ const zoom = getZoom(self.view, longitude, latitude, width, height);
198
+
199
+ const viewportHeightM = height * self.view.resolution;
200
+ const slantAltitude = slantM / viewportHeightM;
201
+ const altitude = getAltitude(self.view.camera.tilt, slantAltitude, width, height);
202
+ const arcgisMatrixView = getArcGISMatrixViewState(
203
+ self.view,
204
+ longitude,
205
+ latitude,
206
+ zoom,
207
+ self.view.resolution,
208
+ altitude
209
+ );
210
+
211
+ try {
212
+ if (arcgisMatrixView) {
213
+ render(self.resources, {
214
+ width,
215
+ height,
216
+ latitude,
217
+ longitude,
218
+ altitude,
219
+ zoom,
220
+ bearing: self.view.camera.heading,
221
+ pitch: self.view.camera.tilt,
222
+ ...arcgisMatrixView
223
+ });
224
+ } else {
225
+ render(self.resources, {
226
+ width,
227
+ height,
228
+ latitude,
229
+ longitude,
230
+ altitude,
231
+ zoom,
232
+ bearing: self.view.camera.heading,
233
+ pitch: self.view.camera.tilt
234
+ });
235
+ }
236
+ } catch (e) {
237
+ // eslint-disable-next-line no-console
238
+ console.error('DeckRenderer: render error', e);
239
+ }
240
+
241
+ return output;
242
+ }
243
+ });
244
+
245
+ this.renderNode = new DeckRenderNode({view: this.view});
47
246
  }
48
247
 
248
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
249
+ async setup(_context?: unknown) {}
250
+
49
251
  dispose() {
50
252
  this.cancelInitialization?.();
51
253
  if (this.resources) {
52
254
  finalizeResources(this.resources);
255
+ this.resources = null;
53
256
  }
257
+ this.renderNode = null;
54
258
  }
55
259
 
56
260
  redraw() {
57
- externalRenderers.requestRender(this.view);
261
+ this.renderNode?.requestRender();
58
262
  }
59
263
 
60
264
  render() {
61
- const [width, height] = this.view.size;
62
-
63
- render(this.resources!, {
64
- width,
65
- height,
66
- latitude: this.view.center.latitude,
67
- longitude: this.view.center.longitude,
68
- altitude: arcgisFOVToDeckAltitude(this.view.camera.fov, width / height),
69
- zoom: this.view.zoom,
70
- bearing: this.view.camera.heading,
71
- pitch: this.view.camera.tilt
72
- });
265
+ this.renderNode?.requestRender();
73
266
  }
74
267
  }
75
268
 
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@ import createDeckRenderer from './deck-renderer';
10
10
  import Accessor from '@arcgis/core/core/Accessor';
11
11
  import Layer from '@arcgis/core/layers/Layer';
12
12
  import BaseLayerViewGL2D from '@arcgis/core/views/2d/layers/BaseLayerViewGL2D';
13
- import * as externalRenderers from '@arcgis/core/views/3d/externalRenderers';
13
+ import RenderNode from '@arcgis/core/views/3d/webgl/RenderNode';
14
14
 
15
15
  // require-style loader
16
16
  export {loadArcGISModules} from './load-modules';
@@ -19,4 +19,4 @@ export {loadArcGISModules} from './load-modules';
19
19
  const DeckProps = createDeckProps(Accessor);
20
20
  const DeckLayerView2D = createDeckLayerView2D(BaseLayerViewGL2D);
21
21
  export const DeckLayer = createDeckLayer(DeckProps, Layer, DeckLayerView2D);
22
- export const DeckRenderer = createDeckRenderer(DeckProps, externalRenderers);
22
+ export const DeckRenderer = createDeckRenderer(DeckProps, RenderNode);
@@ -45,28 +45,28 @@ async function loadArcGISModule(
45
45
  const Layer = esri.layers.Layer;
46
46
  const Accessor = esri.core.Accessor;
47
47
  const BaseLayerViewGL2D = esri.views['2d'].layers.BaseLayerViewGL2D;
48
- const externalRenderers = esri.views['3d'].externalRenderers;
48
+ const RenderNode = esri.views['3d'].webgl.RenderNode;
49
49
 
50
- return initialize(Layer, Accessor, BaseLayerViewGL2D, externalRenderers);
50
+ return initialize(Layer, Accessor, BaseLayerViewGL2D, RenderNode);
51
51
  }
52
52
 
53
- const [Layer, Accessor, BaseLayerViewGL2D, externalRenderers] = await esriLoaderLoadModules(
53
+ const [Layer, Accessor, BaseLayerViewGL2D, RenderNode] = await esriLoaderLoadModules(
54
54
  [
55
55
  'esri/layers/Layer',
56
56
  'esri/core/Accessor',
57
57
  'esri/views/2d/layers/BaseLayerViewGL2D',
58
- 'esri/views/3d/externalRenderers'
58
+ 'esri/views/3d/webgl/RenderNode'
59
59
  ],
60
60
  loadScriptOptions
61
61
  );
62
- return initialize(Layer, Accessor, BaseLayerViewGL2D, externalRenderers);
62
+ return initialize(Layer, Accessor, BaseLayerViewGL2D, RenderNode);
63
63
  }
64
64
 
65
- function initialize(Layer, Accessor, BaseLayerViewGL2D, externalRenderers): LoadedModules {
65
+ function initialize(Layer, Accessor, BaseLayerViewGL2D, RenderNode): LoadedModules {
66
66
  const DeckProps = createDeckProps(Accessor);
67
67
  const DeckLayerView2D = createDeckLayerView2D(BaseLayerViewGL2D);
68
68
  const DeckLayer = createDeckLayer(DeckProps, Layer, DeckLayerView2D);
69
- const DeckRenderer = createDeckRenderer(DeckProps, externalRenderers);
69
+ const DeckRenderer = createDeckRenderer(DeckProps, RenderNode);
70
70
 
71
71
  arcGIS = {DeckLayer, DeckRenderer};
72
72
  return arcGIS;