@@ -103,16 +103,18 @@ export default function createDeckRenderer(DeckProps, RenderNode) {
103103 cancelInitialization : ( ( ) => void ) | null = null ;
104104 renderNode : any = null ;
105105
106- constructor ( view : SceneView , props : DeckProps ) {
107- this . view = view ;
108- this . deck = new DeckProps ( props ) ;
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+ }
109115
110- // Guard against concurrent initialization attempts (e.g. if render fires
111- // multiple times before the async init completes).
112116 let isInitializing = false ;
113117
114- // Alias outer `this` for use inside the RenderNode.createSubclass()
115- // callbacks below, where `this` refers to the RenderNode instance.
116118 // eslint-disable-next-line @typescript-eslint/no-this-alias
117119 const self = this ;
118120 const DeckRenderNode = RenderNode . createSubclass ( {
@@ -122,15 +124,11 @@ export default function createDeckRenderer(DeckProps, RenderNode) {
122124 initialize ( ) { } ,
123125
124126 render ( inputs : Array < { name : string } > ) {
125- // `this` here is the RenderNode instance, typed loosely because
126- // ArcGIS's RenderNode.createSubclass() does not surface types.
127127 // biome-ignore lint/complexity/noThisInStatic: RenderNode render is an instance method
128128 // eslint-disable-next-line @typescript-eslint/no-this-alias, consistent-this
129129 const renderNode : any = this ;
130130 const passthrough = inputs . find ( i => i . name === 'composite-color' ) ;
131131
132- // Lazy init: the first render() call is the earliest point at which
133- // `gl` is guaranteed to be available on the RenderNode.
134132 if ( ! self . resources && ! isInitializing ) {
135133 const gl = renderNode . gl ;
136134 if ( gl ) {
@@ -164,59 +162,31 @@ export default function createDeckRenderer(DeckProps, RenderNode) {
164162 return passthrough ;
165163 }
166164
167- // bindRenderTarget() must be called BEFORE render() so that the
168- // FRAMEBUFFER_BINDING captured inside commons.render() points at the
169- // OUTPUT framebuffer rather than the composite-color INPUT.
170165 const output = renderNode . bindRenderTarget ( ) ;
171-
172- // Use gl drawing buffer rather than view.size, which can include
173- // non-GL chrome pixels.
174166 const gl = renderNode . gl as WebGL2RenderingContext ;
175167
176168 const dpr = window . devicePixelRatio || 1 ;
177169 const width = gl . drawingBufferWidth / dpr ;
178170 const height = gl . drawingBufferHeight / dpr ;
179171
180- // deck.gl's 512-px tile convention: at zoom z / latitude φ,
181- // ground-meters-per-pixel = 78271.5 * cos(φ) / 2^z. ArcGIS's
182- // `view.scale` is related to ArcGIS's `view.resolution` by
183- // `resolution = scale / 3779.527559` (96 DPI), where resolution
184- // is *projected* Web-Mercator meters-per-pixel. Ground mpp at
185- // latitude is `resolution * cos(φ)` — the cos(φ) stretch is
186- // baked into both sides, so it cancels when equating deck ground
187- // mpp to ArcGIS ground mpp. The zoom formula is therefore:
188- // zoom = log2(591657550.5 / view.scale) - 1
189- // (the `-1` converts the 256-px constant to deck's 512-px
190- // convention.) Earlier revisions included a `* cos(lat)` factor
191- // here; that was wrong — it was inadvertently matching
192- // *projected* Mercator mpp instead of ground mpp, and caused
193- // the deck layer to render ~26% smaller than the basemap at SF
194- // latitudes.
195- // deck.gl's `altitude` is the camera distance above the focal
196- // plane, measured in viewport-height units. In deck.gl,
197- // `altitude` couples camera distance with projection FOV:
198- // verticalFOV = 2 * atan(0.5 / altitude)
199- // and the camera sits at slant distance = altitude from the
200- // focal point regardless of pitch.
201- //
202- // ArcGIS behaves differently: it uses a fixed 55° *diagonal*
203- // FOV and moves the camera closer to the focal point as tilt
204- // increases (keeping the focal point at screen center). Using
205- // the FOV-only formula (altitude = 0.5 / tan(verticalFOV/2))
206- // works at tilt=0 but diverges at high tilt because ArcGIS's
207- // actual camera is closer.
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.
208178 //
209- // We match ArcGIS by deriving altitude from the *actual*
210- // slant distance between `camera.position` and the focal
211- // point on the ground, expressed in viewport-height units.
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.
212185 const cameraPos = self . view . camera . position ;
213186 const focalPoint = self . view . toMap ( { x : width / 2 , y : height / 2 } as any ) ;
214187 const latitude = focalPoint ? focalPoint . latitude : self . view . center . latitude ;
215188 const longitude = focalPoint ? focalPoint . longitude : self . view . center . longitude ;
216189
217- // Horizontal ground distance from camera to focal point (meters).
218- // Use a flat-earth approximation; viewingMode='local' in the
219- // SceneView makes this exact.
220190 const midLatRad = ( ( ( latitude + cameraPos . latitude ) / 2 ) * Math . PI ) / 180 ;
221191 const dLatM = ( cameraPos . latitude - latitude ) * METERS_PER_DEG_LAT ;
222192 const dLngM =
@@ -226,8 +196,6 @@ export default function createDeckRenderer(DeckProps, RenderNode) {
226196
227197 const zoom = getZoom ( self . view , longitude , latitude , width , height ) ;
228198
229- // Viewport height in meters at the focal plane. ArcGIS's
230- // `view.resolution` is meters-per-pixel at the focal point.
231199 const viewportHeightM = height * self . view . resolution ;
232200 const slantAltitude = slantM / viewportHeightM ;
233201 const altitude = getAltitude ( self . view . camera . tilt , slantAltitude , width , height ) ;
@@ -277,8 +245,6 @@ export default function createDeckRenderer(DeckProps, RenderNode) {
277245 this . renderNode = new DeckRenderNode ( { view : this . view } ) ;
278246 }
279247
280- // Kept for API compatibility. Resource initialization now happens lazily
281- // inside the RenderNode's render() callback on the first frame.
282248 // eslint-disable-next-line @typescript-eslint/no-unused-vars
283249 async setup ( _context ?: unknown ) { }
284250
0 commit comments