UNPKG

@threlte/extras

Version:

Utilities, abstractions and plugins for your Threlte apps

105 lines (104 loc) 3.67 kB
import { useTask, useThrelte } from '@threlte/core'; import { getContext, setContext, tick } from 'svelte'; import { fromStore } from 'svelte/store'; import { Box3, Vector3, Group, Quaternion, Matrix4, PerspectiveCamera, OrthographicCamera, Vector2, Vector4, Spherical, Sphere, Raycaster } from 'three'; import { useControlsContext } from '../controls/useControlsContext.js'; import CameraControls from 'camera-controls'; const key = Symbol('<Bounds>'); let installed = false; const install = () => { if (installed) { return; } CameraControls.install({ THREE: { Vector2, Vector3, Vector4, Quaternion, Matrix4, Spherical, Box3, Sphere, Raycaster } }); installed = true; }; export const provideBounds = (ref, margin, animate, onFit) => { install(); const { camera: cameraStore, dom, invalidate } = useThrelte(); const { orbitControls: orbitStore, trackballControls: trackballStore, cameraControls: ccStore } = useControlsContext(); const camera = fromStore(cameraStore); const orbitControls = fromStore(orbitStore); const trackballControls = fromStore(trackballStore); const cameraControls = fromStore(ccStore); const boundsControls = new CameraControls(camera.current, dom); boundsControls.disconnect(); $effect.pre(() => { boundsControls.camera = camera.current; }); let animating = $state(false); const controls = $derived(orbitControls.current ?? trackballControls.current ?? cameraControls.current ?? { enabled: false }); const fit = async () => { const { azimuthAngle, polarAngle } = boundsControls; const currentMargin = margin(); const currentControls = controls; const shouldAnimate = animate(); currentControls.enabled = false; animating = true; await Promise.all([ boundsControls.fitToBox(ref(), shouldAnimate, { paddingBottom: currentMargin, paddingLeft: currentMargin, paddingTop: currentMargin, paddingRight: currentMargin }), // Preserve previous rotation boundsControls?.rotateAzimuthTo(azimuthAngle, shouldAnimate), boundsControls?.rotatePolarTo(polarAngle, shouldAnimate) ]); // Flush the snap to the underlying camera. With `shouldAnimate=true` the // animation task has already been advancing the camera per frame and this // is a no-op; with `shouldAnimate=false` no task ran and the camera is // still at its old position until we drive it once here. boundsControls.update(0); if ('fromJSON' in currentControls) { currentControls.fromJSON(boundsControls.toJSON()); currentControls.update?.(0); } else { boundsControls.getTarget(currentControls.target, true); currentControls.update?.(); } animating = false; await tick(); currentControls.enabled = true; onFit()?.(); }; const reset = async () => { animating = true; await boundsControls.reset(animate()); animating = false; }; useTask((delta) => { if (boundsControls.update(delta)) { invalidate(); } }, { running: () => animating, autoInvalidate: false }); const context = { fit, reset }; setContext(key, context); return context; }; export const useBounds = () => { return getContext(key); };