@threlte/extras
Version:
Utilities, abstractions and plugins for your Threlte apps
105 lines (104 loc) • 3.67 kB
JavaScript
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);
};