@@ -191,10 +191,19 @@ export default class GlobeViewport extends Viewport {
191191 ] ;
192192 }
193193
194+ unproject ( xyz : number [ ] , options ?: { topLeft ?: boolean ; targetZ ?: number } ) : number [ ] ;
194195 unproject (
195196 xyz : number [ ] ,
196- { topLeft = true , targetZ} : { topLeft ?: boolean ; targetZ ?: number } = { }
197- ) : number [ ] {
197+ options : { topLeft ?: boolean ; targetZ ?: number ; fallback : false }
198+ ) : number [ ] | null ;
199+ unproject (
200+ xyz : number [ ] ,
201+ {
202+ topLeft = true ,
203+ targetZ,
204+ fallback = true
205+ } : { topLeft ?: boolean ; targetZ ?: number ; fallback ?: boolean } = { }
206+ ) : number [ ] | null {
198207 const [ x , y , z ] = xyz ;
199208
200209 const y2 = topLeft ? y : this . height - y ;
@@ -207,23 +216,21 @@ export default class GlobeViewport extends Viewport {
207216 } else {
208217 // since we don't know the correct projected z value for the point,
209218 // unproject two points to get a line and then find the point on that line that intersects with the sphere
210- const coord0 = transformVector ( pixelUnprojectionMatrix , [ x , y2 , - 1 , 1 ] ) ;
211- const coord1 = transformVector ( pixelUnprojectionMatrix , [ x , y2 , 1 , 1 ] ) ;
212-
213- const lt = ( ( targetZ || 0 ) / EARTH_RADIUS + 1 ) * GLOBE_RADIUS ;
214- const lSqr = vec3 . sqrLen ( vec3 . sub ( [ ] , coord0 , coord1 ) ) ;
215- const l0Sqr = vec3 . sqrLen ( coord0 ) ;
216- const l1Sqr = vec3 . sqrLen ( coord1 ) ;
217- const sSqr = ( 4 * l0Sqr * l1Sqr - ( lSqr - l0Sqr - l1Sqr ) ** 2 ) / 16 ;
218- const dSqr = ( 4 * sSqr ) / lSqr ;
219- const r0 = Math . sqrt ( l0Sqr - dSqr ) ;
220- const discriminant = lt * lt - dSqr ;
219+ const { coord0, coord1, lSqr, r0, discriminant} = this . _getRaySphereIntersection (
220+ x ,
221+ y2 ,
222+ targetZ
223+ ) ;
221224
222225 if ( discriminant < 0 ) {
226+ if ( ! fallback ) {
227+ return null ;
228+ }
223229 // Ray misses the sphere — project the closest-approach point onto the sphere surface
224230 const tClosest = r0 / Math . sqrt ( lSqr ) ;
225231 const closest = vec3 . lerp ( [ ] , coord0 , coord1 , tClosest ) ;
226232 const len = vec3 . len ( closest ) ;
233+ const lt = ( ( targetZ || 0 ) / EARTH_RADIUS + 1 ) * GLOBE_RADIUS ;
227234 coord = len > 0 ? vec3 . scale ( [ ] , closest , lt / len ) : [ 0 , 0 , lt ] ;
228235 } else {
229236 const dr = Math . sqrt ( discriminant ) ;
@@ -239,6 +246,15 @@ export default class GlobeViewport extends Viewport {
239246 return Number . isFinite ( targetZ ) ? [ X , Y , targetZ as number ] : [ X , Y ] ;
240247 }
241248
249+ isPointOnGlobe (
250+ pixel : number [ ] ,
251+ { topLeft = true , targetZ} : { topLeft ?: boolean ; targetZ ?: number } = { }
252+ ) : boolean {
253+ const [ x , y ] = pixel ;
254+ const y2 = topLeft ? y : this . height - y ;
255+ return this . _getRaySphereIntersection ( x , y2 , targetZ ) . discriminant >= 0 ;
256+ }
257+
242258 projectPosition ( xyz : number [ ] ) : [ number , number , number ] {
243259 const [ lng , lat , Z = 0 ] = xyz ;
244260 const lambda = lng * DEGREES_TO_RADIANS ;
@@ -269,13 +285,35 @@ export default class GlobeViewport extends Viewport {
269285 return xyz as [ number , number ] ;
270286 }
271287
288+ /**
289+ * Pan the globe using delta-based movement.
290+ * Used when the pointer starts outside the globe so dragging spins the globe.
291+ */
292+ panByPosition ( coords : number [ ] , pixel : number [ ] , startPixel ?: number [ ] ) : GlobeViewportOptions {
293+ if ( ! startPixel ) {
294+ return this . panByLngLat ( coords , pixel ) ;
295+ }
296+
297+ const [ startLng , startLat , startZoom ] = coords ;
298+ // Scale rotation speed inversely with zoom to keep off-globe drags predictable.
299+ const scale = Math . pow ( 2 , this . zoom - zoomAdjust ( this . latitude ) ) ;
300+ const rotationSpeed = 0.25 / scale ;
301+
302+ const longitude = startLng + rotationSpeed * ( startPixel [ 0 ] - pixel [ 0 ] ) ;
303+ const latitude = Math . max (
304+ Math . min ( startLat - rotationSpeed * ( startPixel [ 1 ] - pixel [ 1 ] ) , MAX_LATITUDE ) ,
305+ - MAX_LATITUDE
306+ ) ;
307+ const zoom = startZoom + zoomAdjust ( latitude ) - zoomAdjust ( startLat ) ;
308+ return { longitude, latitude, zoom} ;
309+ }
310+
272311 /**
273312 * Pan the globe so that a geographic position appears at a given screen pixel.
274- * Shifts center by (coords - unproject(pixel)) — i.e. keeps the grabbed lng/lat
275- * under the cursor. Used for drag-pan and zoom-toward-cursor.
313+ * Used for on-globe drag-pan and zoom-toward-cursor.
276314 */
277- panByPosition ( coords : number [ ] , pixel : number [ ] ) : GlobeViewportOptions {
278- const currentAtPixel = this . unproject ( pixel ) ;
315+ panByLngLat ( coords : number [ ] , pixel : number [ ] ) : GlobeViewportOptions {
316+ const currentAtPixel = this . unproject ( pixel , { fallback : false } ) ;
279317 if ( ! currentAtPixel ) {
280318 return { longitude : this . longitude , latitude : this . latitude } ;
281319 }
@@ -288,6 +326,20 @@ export default class GlobeViewport extends Viewport {
288326 const zoom = this . zoom + zoomAdjust ( latitude ) - zoomAdjust ( this . latitude ) ;
289327 return { longitude, latitude, zoom} ;
290328 }
329+
330+ private _getRaySphereIntersection ( x : number , y : number , targetZ ?: number ) {
331+ const coord0 = transformVector ( this . pixelUnprojectionMatrix , [ x , y , - 1 , 1 ] ) ;
332+ const coord1 = transformVector ( this . pixelUnprojectionMatrix , [ x , y , 1 , 1 ] ) ;
333+ const lt = ( ( targetZ || 0 ) / EARTH_RADIUS + 1 ) * GLOBE_RADIUS ;
334+ const lSqr = vec3 . sqrLen ( vec3 . sub ( [ ] , coord0 , coord1 ) ) ;
335+ const l0Sqr = vec3 . sqrLen ( coord0 ) ;
336+ const l1Sqr = vec3 . sqrLen ( coord1 ) ;
337+ const sSqr = ( 4 * l0Sqr * l1Sqr - ( lSqr - l0Sqr - l1Sqr ) ** 2 ) / 16 ;
338+ const dSqr = ( 4 * sSqr ) / lSqr ;
339+ const r0 = Math . sqrt ( l0Sqr - dSqr ) ;
340+
341+ return { coord0, coord1, lSqr, r0, discriminant : lt * lt - dSqr } ;
342+ }
291343}
292344
293345export function zoomAdjust ( latitude : number ) : number {
0 commit comments