Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Settling Utility #948

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion src/r3f/components/CameraTransition.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

// only respect the camera initially so the default camera settings are automatically used

}, [] );

Check warning on line 42 in src/r3f/components/CameraTransition.jsx

View workflow job for this annotation

GitHub Actions / build (18.x)

React Hook useMemo has missing dependencies: 'camera' and 'mode'. Either include them or remove the dependency array

Check warning on line 42 in src/r3f/components/CameraTransition.jsx

View workflow job for this annotation

GitHub Actions / build (20.x)

React Hook useMemo has missing dependencies: 'camera' and 'mode'. Either include them or remove the dependency array

useEffect( () => {

Expand Down Expand Up @@ -147,7 +147,7 @@

}

}, [ mode, manager, invalidate, controls ] );
}, [ mode, manager, invalidate, controls, onBeforeToggle ] );

// rerender the frame when the transition animates
useEffect( () => {
Expand Down
115 changes: 115 additions & 0 deletions src/r3f/components/SettledObjects.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { cloneElement, createContext, forwardRef, useContext, useEffect, useMemo, useRef } from 'react';
import { useMultipleRefs } from '../utilities/useMultipleRefs.js';
import { TilesRendererContext } from './TilesRenderer.jsx';
import { QueryManager } from '../utilities/QueryManager.js';
import { useDeepOptions } from '../utilities/useOptions.jsx';
import { OBJECT_FRAME } from '../../three/math/Ellipsoid.js';
import { Matrix4, Ray } from 'three';
import { useThree } from '@react-three/fiber';

const QueryManagerContext = createContext( null );

export const SettledObject = forwardRef( function SettledObject( props, ref ) {

const {
component = <group />,
lat = null,
lon = null,
rayorigin = null,
raydirection = null,
...rest
} = props;

const objectRef = useRef( null );
const tiles = useContext( TilesRendererContext );
const queries = useContext( QueryManagerContext );
const invalidate = useThree( ( { invalidate } ) => invalidate );

useEffect( () => {

if ( lat !== null && lon !== null ) {

const matrix = new Matrix4();
const index = queries.registerLatLonQuery( lat, lon, hit => {

if ( hit !== null && objectRef.current !== null ) {

objectRef.current.position.copy( hit.point );
queries.ellipsoid.getRotationMatrixFromAzElRoll( lat, lon, 0, 0, 0, matrix, OBJECT_FRAME ).premultiply( tiles.group.matrixWorld );
objectRef.current.quaternion.setFromRotationMatrix( matrix );
invalidate();

}

} );

return () => queries.unregisterQuery( index );

} else if ( rayorigin !== null && raydirection !== null ) {

const ray = new Ray();
ray.origin.copy( rayorigin );
ray.direction.copy( raydirection );
const index = queries.registerRayQuery( ray, hit => {

if ( hit !== null && objectRef.current !== null ) {

objectRef.current.position.copy( hit.point );
objectRef.current.quaternion.identity();
invalidate();

}

} );

return () => queries.unregisterQuery( index );

}

}, [ lat, lon, rayorigin, raydirection, queries, tiles, invalidate ] );

return cloneElement( component, { ...rest, ref: useMultipleRefs( objectRef, ref ), raycast: () => false } );

} );

export const SettledObjects = forwardRef( function SettledObjects( props, ref ) {

const threeScene = useThree( ( { scene } ) => scene );
const {
scene = threeScene,
children,
...rest
} = props;

const tiles = useContext( TilesRendererContext );
const queries = useMemo( () => new QueryManager(), [] );

useDeepOptions( queries, rest );

useEffect( () => {

queries.setScene( ...( Array.isArray( scene ) ? scene : [ scene ] ) );

}, [ queries, scene ] );

useEffect( () => {

if ( tiles ) {

// TODO: we need to react to matrix update
queries.ellipsoid.copy( tiles.ellipsoid );
queries.frame.copy( tiles.group.matrixWorld );

}

}, [ queries, tiles ] );

useMultipleRefs( ref )( queries );

return (
<QueryManagerContext.Provider value={ queries }>
{ children }
</QueryManagerContext.Provider>
);

} );
222 changes: 222 additions & 0 deletions src/r3f/utilities/QueryManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import {
Ray,

Check failure on line 2 in src/r3f/utilities/QueryManager.js

View workflow job for this annotation

GitHub Actions / build (18.x)

'Ray' is defined but never used

Check failure on line 2 in src/r3f/utilities/QueryManager.js

View workflow job for this annotation

GitHub Actions / build (20.x)

'Ray' is defined but never used
Raycaster,
Matrix4,
EventDispatcher,
} from 'three';
import { SceneObserver } from './SceneObserver.js';
import { Ellipsoid } from '../../three/math/Ellipsoid.js';

const _raycaster = /* @__PURE__ */ new Raycaster();
export class QueryManager extends EventDispatcher {

constructor() {

super();

// settings
this.autoRun = true;

// queries
this.queryMap = new Map();
this.index = 0;

// jobs
this.queued = new Set();
this.scheduled = false;
this.duration = 1;

// scene
this.objects = [];
this.observer = new SceneObserver();
this.ellipsoid = new Ellipsoid();
this.frame = new Matrix4();

// register to mark items as dirty
const queueAll = ( () => {

let queued = false;
return () => {

if ( ! queued ) {

queued = true;
queueMicrotask( () => {

this.queryMap.forEach( q => this._enqueue( q ) );

queued = false;

} );

}

};

} )();

this.observer.addEventListener( 'childadded', queueAll );
this.observer.addEventListener( 'childremoved', queueAll );

}

// job runner
_enqueue( info ) {

this.queued.add( info );
this._scheduleRun();

}

_runJobs() {

const { queued, duration } = this;
const start = performance.now();
for ( const item of queued ) {

if ( queued.size === 0 || performance.now() - start > duration ) {

break;

}

queued.delete( item );
this._updateQuery( item );

}

if ( queued.size !== 0 ) {

this._scheduleRun();

}

}

_scheduleRun() {

if ( this.autoRun && ! this.scheduled ) {

this.scheduled = true;
requestAnimationFrame( () => {

this.scheduled = false;
this._runJobs();

} );

}

}

_updateQuery( item ) {

const { queued, ellipsoid, frame } = this;

if ( item.ray ) {

_raycaster.ray.copy( item.ray );

} else {

const { lat, lon } = item;
const ray = _raycaster.ray;
ellipsoid.getCartographicToPosition( lat, lon, 1e4, ray.origin ).applyMatrix4( frame );
ellipsoid.getCartographicToNormal( lat, lon, ray.direction ).transformDirection( frame ).multiplyScalar( - 1 );

}

item.callback( _raycaster.intersectObjects( this.objects )[ 0 ] || null );
queued.delete( item );


}

runIfNeeded( index ) {

const { queued } = this;
const item = this.queryMap.get( index );
if ( queued.has( item ) ) {

this._updateQuery( item );

}

}

// set the scene used for query
setScene( ...objects ) {

const { observer } = this;
observer.dispose();
objects.forEach( o => observer.observe( o ) );
this.objects = objects;
this._scheduleRun();

}

setEllipsoidFromTilesRenderer( tilesRenderer ) {

const { observer, ellipsoid, frame, objects } = this;
if (
! ellipsoid.radius.equals( tilesRenderer.ellipsoid.radius ) ||
! frame.equals( tilesRenderer.group.matrixWorld )
) {

ellipsoid.copy( tilesRenderer.ellipsoid );
frame.copy( tilesRenderer.group.matrixWorld );
objects.forEach( o => observer.observe( o ) );
this._scheduleRun();

}

}

// register query callbacks
registerRayQuery( ray, callback ) {

const index = this.index ++;
const item = {
ray: ray.clone(),
callback,
};

this.queryMap.set( index, item );
this._enqueue( item );
return index;

}

registerLatLonQuery( lat, lon, callback ) {

const index = this.index ++;
const item = {
lat, lon,
callback,
};

this.queryMap.set( index, item );
this._enqueue( item );
return index;

}

unregisterQuery( index ) {

const { queued, queryMap } = this;
const item = queryMap.get( index );
queued.delete( item );

}

// dispose of everything
dispose() {

this.queryMap.clear();
this.queued.clear();
this.objects.length = 0;
this.observer.dispose();

}

}
Loading
Loading