@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
1,086 lines • 47.1 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Camera as Camera3, Object3D, PerspectiveCamera, Ray, Vector2, Vector3 } from "three";
import { OrbitControls as ThreeOrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { isDevEnvironment } from "../engine/debug/index.js";
import { fitCamera } from "../engine/engine_camera.fit.js";
import { setCameraController } from "../engine/engine_camera.js";
import { Gizmos } from "../engine/engine_gizmos.js";
import { InputEventQueue } from "../engine/engine_input.js";
import { Mathf } from "../engine/engine_math.js";
import { RaycastOptions } from "../engine/engine_physics.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import { getTempVector, getWorldPosition } from "../engine/engine_three_utils.js";
import { DeviceUtilities, getParam } from "../engine/engine_utils.js";
import { Camera } from "./Camera.js";
import { Behaviour, GameObject } from "./Component.js";
import { LookAtConstraint } from "./LookAtConstraint.js";
import { SyncedTransform } from "./SyncedTransform.js";
import { EventSystem, EventSystemEvents } from "./ui/EventSystem.js";
import { tryGetUIComponent } from "./ui/Utils.js";
const debug = getParam("debugorbit");
const freeCam = getParam("freecam");
const debugCameraFit = getParam("debugcamerafit");
const smoothcam = getParam("smoothcam");
const disabledKeys = { LEFT: "", UP: "", RIGHT: "", BOTTOM: "" };
let defaultKeys = undefined;
export var OrbitControlsEventsType;
(function (OrbitControlsEventsType) {
/** Invoked with a CameraTargetReachedEvent */
OrbitControlsEventsType["CameraTargetReached"] = "target-reached";
})(OrbitControlsEventsType || (OrbitControlsEventsType = {}));
export class CameraTargetReachedEvent extends CustomEvent {
constructor(ctrls, type) {
super(OrbitControlsEventsType.CameraTargetReached, {
detail: {
controls: ctrls,
type: type,
}
});
}
}
/**
* [OrbitControls](https://engine.needle.tools/docs/api/OrbitControls) provides interactive camera control using three.js OrbitControls.
* Users can rotate, pan, and zoom the camera to explore 3D scenes.
*
* **Features:**
* - Rotation around a target point (orbit)
* - Panning to move the view
* - Zooming via scroll or pinch
* - Auto-rotation for showcases
* - Configurable angle and distance limits
* - Smooth damping for natural feel
*
* 
*
* **Access underlying controls:**
* - `controls` - The three.js OrbitControls instance
* - `controllerObject` - The object being controlled (usually the camera)
*
* **Debug options:**
* - `?debugorbit` - Log orbit control events
* - `?freecam` - Enable unrestricted camera movement
*
* @example Basic setup
* ```ts
* const orbitControls = camera.getComponent(OrbitControls);
* orbitControls.autoRotate = true;
* orbitControls.autoRotateSpeed = 2;
* ```
*
* @example Set look-at target
* ```ts
* orbitControls.setLookTargetPosition(new Vector3(0, 1, 0), true);
* // Or move both camera and target
* orbitControls.setCameraTargetPosition(new Vector3(5, 2, 5), new Vector3(0, 0, 0));
* ```
*
* @summary Camera controller using three.js OrbitControls
* @category Camera and Controls
* @group Components
* @see {@link LookAtConstraint} for setting the look-at target
* @see {@link SmoothFollow} for smooth camera following
* @see {@link Camera} for camera configuration
* @link https://threejs.org/docs/#examples/en/controls/OrbitControls
* @link https://engine.needle.tools/samples/panorama-controls alternative controls in samples
*/
export class OrbitControls extends Behaviour {
/**
* @inheritdoc
*/
get isCameraController() {
return true;
}
/** The underlying three.js OrbitControls.
* See {@link https://threejs.org/docs/#examples/en/controls/OrbitControls}
* @returns {@type ThreeOrbitControls | null}
*/
get controls() {
return this._controls;
}
/** The object being controlled by the OrbitControls (usually the camera)
* See {@link https://threejs.org/docs/#examples/en/controls/OrbitControls.object}
* @returns {@type Object3D | null}
*/
get controllerObject() {
return this._cameraObject;
}
/** Register callback when user starts interacting with the orbit controls */
onStartInteraction(callback) {
this.controls?.addEventListener("start", callback);
}
/** When enabled OrbitControls will automatically raycast find a look at target in start
* @default true
*/
autoTarget = true;
/** When enabled the scene will be automatically fitted into the camera view in onEnable
* @default false
*/
autoFit = false;
/** When enabled the camera can be rotated
* @default true
*/
enableRotate = true;
/** When enabled the camera will rotate automatically
* @default false
*/
autoRotate = false;
/** The speed at which the camera will rotate automatically. Will only be used when `autoRotate` is enabled
* @default 1.0
*/
autoRotateSpeed = 1.0;
/** The minimum azimuth angle in radians */
minAzimuthAngle = Infinity;
/** The maximum azimuth angle in radians */
maxAzimuthAngle = Infinity;
/** The minimum polar angle in radians
* @default 0
*/
minPolarAngle = 0;
/** The maximum polar angle in radians
* @default Math.PI
*/
maxPolarAngle = Math.PI;
/** When enabled the camera can be moved using keyboard keys. The keys are defined in the `controls.keys` property
* @default false
*/
enableKeys = false;
/** When enabled the camera movement will be damped
* @default true
*/
enableDamping = true;
/** The damping factor for the camera movement. For more information see the [three.js documentation](https://threejs.org/docs/#examples/en/controls/OrbitControls.dampingFactor)
* @default 0.1
*/
dampingFactor = 0.1;
/** When enabled the camera can be zoomed
* @default true
*/
enableZoom = true;
/** The minimum zoom level
* @default 0
*/
minZoom = 0;
/** The maximum zoom level
* @default Infinity
*/
maxZoom = Infinity;
/**
* Sets the zoom speed of the OrbitControls
* @default 1
*/
zoomSpeed = 1;
/**
* Set to true to enable zooming to the cursor position.
* @default false
*/
zoomToCursor = false;
/** When enabled the camera can be panned
* @default true
*/
enablePan = true;
/** Assigning a {@link LookAtConstraint} will make the camera look at the constraint source
* @default null
*/
lookAtConstraint = null;
/** The weight of the first lookAtConstraint source
* @default 1
*/
lookAtConstraint01 = 1;
/** If true user input interrupts the camera from animating to a target
* @default true
*/
allowInterrupt = true;
/** If true the camera will focus on the target when the middle mouse button is clicked */
middleClickToFocus = true;
/** If true the camera will focus on the target when the left mouse button is double clicked
* @default true
*/
doubleClickToFocus = true;
/**
* When enabled the camera will fit the scene to the camera view when the background is clicked the specified number of times within a short time
* @default 2
*/
clickBackgroundToFitScene = 2;
/**
* This is the DOM element that the OrbitControls will listen to for input events. By default this is the renderer's canvas element.
* Set this to a different element to make the OrbitControls listen to that element instead.
*/
get targetElement() {
return this._controls?.domElement ?? this._targetElement;
}
set targetElement(value) {
this._targetElement = value;
if (this._controls && this._controls.domElement !== value) {
this._controls.disconnect();
this._controls.domElement = value;
this._controls.connect();
}
}
_targetElement = null;
/**
* @internal If true debug information will be logged to the console
* @default false
*/
debugLog = false;
/**
* @deprecated use `targetLerpDuration` instead
* ~~The speed at which the camera target and the camera will be lerping to their destinations (if set via script or user input)~~
* */
get targetLerpSpeed() { return 5; }
set targetLerpSpeed(v) { this.targetLerpDuration = 1 / v; }
/** The duration in seconds it takes for the camera look ad and position lerp to reach their destination (when set via `setCameraTargetPosition` and `setLookTargetPosition`)
* @default 1
*/
get targetLerpDuration() { return this._lookTargetLerpDuration; }
set targetLerpDuration(v) { this._lookTargetLerpDuration = v; }
_lookTargetLerpDuration = 1;
targetBounds = null;
/**
* Rotate the camera left (or right) by the specified angle in radians.
* For positive angles the camera will rotate to the left, for negative angles it will rotate to the right.
* Tip: Use Mathf to convert between degrees and radians.
* @param angleInRadians The angle in radians to rotate the camera left
* @example
* ```typescript
* // Rotate the camera left by 0.1 radians
* orbitControls.rotateLeft(0.1);
* ```
*/
rotateLeft(angleInRadians) {
this._controls?._rotateLeft(angleInRadians);
}
/**
* Rotate the camera up (or down) by the specified angle in radians.
* For positive angles the camera will rotate up, for negative angles it will rotate down.
* Tip: Use Mathf to convert between degrees and radians.
* @param angleInRadians The angle in radians to rotate the camera up
* @example
* ```typescript
* // Rotate the camera up by 0.1 radians
* orbitControls.rotateUp(0.1);
* ```
*/
rotateUp(angleInRadians) {
this._controls?._rotateUp(angleInRadians);
}
/**
* Pan the camera by the specified amount in the x and y direction in pixels.
* @param dx The amount to pan the camera in the x direction in pixels.
* @param dy The amount to pan the camera in the y direction in pixels.
*/
pan(dx, dy) {
this._controls?._pan(dx, dy);
}
/**
* Zoom the camera in or out by the specified scale factor. The factor is applied to the current zoom radius / distance.
* If the scale is greater than 0 then the camera will zoom in, if it is less than 0 then the camera will zoom out.
* @param scale The scale factor to zoom the camera in or out. Expected range is between -1 and 1, where 0 means no zoom.
* @example
* ```typescript
* // Zoom in by 0.1
* orbitControls.zoomIn(0.1);
* // Zoom out by 0.1
* orbitControls.zoomIn(-0.1);
* ```
*/
zoomIn(scale) {
if (scale > 0) {
this._controls?._dollyIn(1 - scale);
}
else if (scale < 0) {
this._controls?._dollyOut(1 + scale);
}
}
_controls = null;
_cameraObject = null;
_lookTargetLerpActive = false;
_lookTargetStartPosition = new Vector3();
_lookTargetEndPosition = new Vector3();
_lookTargetLerp01 = 0;
_cameraLerpActive = false;
_cameraStartPosition = new Vector3();
_cameraEndPosition = new Vector3();
_cameraLerp01 = 0;
_cameraLerpDuration = 0;
_fovLerpActive = false;
_fovLerpStartValue = 0;
_fovLerpEndValue = 0;
_fovLerp01 = 0;
_fovLerpDuration = 0;
_inputs = 0;
_enableTime = 0; // use to disable double click when double clicking on UI
_startedListeningToKeyEvents = false;
_eventSystem;
_afterHandleInputFn;
_camera = null;
_syncedTransform;
_didSetTarget = 0;
/** @internal */
awake() {
if (debug)
console.debug("OrbitControls", this);
this._didSetTarget = 0;
this._startedListeningToKeyEvents = false;
if (this.context.domElement.cameraControls === false) {
this.enabled = false;
}
}
/** @internal */
start() {
this._eventSystem = EventSystem.get(this.context) ?? undefined;
if (this._eventSystem) {
this._afterHandleInputFn = this.afterHandleInput.bind(this);
this._eventSystem.addEventListener(EventSystemEvents.AfterHandleInput, this._afterHandleInputFn);
}
}
/** @internal */
onDestroy() {
this._controls?.dispose();
this._eventSystem?.removeEventListener(EventSystemEvents.AfterHandleInput, this._afterHandleInputFn);
}
/** @internal */
onEnable() {
this._didSetTarget = 0;
this._enableTime = this.context.time.time;
const cameraComponent = GameObject.getComponent(this.gameObject, Camera);
this._camera = cameraComponent;
let cam = cameraComponent?.threeCamera;
if (!cam && this.gameObject instanceof PerspectiveCamera) {
cam = this.gameObject;
}
if (cam)
setCameraController(cam, this, true);
if (!this._controls && cam instanceof Object3D) {
this._cameraObject = cam;
// Using the parent if possible to make it possible to disable input on the canvas
// for having HTML content behind it and still receive input
const element = this.targetElement ?? this.context.renderer.domElement;
// HACK: workaround for three orbit controls forcing an update when being created....
const mat = cam?.quaternion.clone();
this._controls = new ThreeOrbitControls(cam, element);
cam?.quaternion.copy(mat);
if (defaultKeys === undefined)
defaultKeys = { ...this._controls.keys };
// set controls look point in front of the current camera by default
// it may be overriden by the autoTarget feature
// but if we don't do this and autoTarget is OFF then the camera will turn to look at 0 0 0 of the scene
const worldPosition = getWorldPosition(cam);
const forward = this.gameObject.worldForward;
const dist = 2.5;
const lookAt = worldPosition.clone().sub(forward.multiplyScalar(dist));
this._controls.target.copy(lookAt);
}
if (this._controls) {
if (freeCam) {
this.enablePan = true;
this.enableZoom = true;
this.middleClickToFocus = true;
if (DeviceUtilities.isMobileDevice())
this.doubleClickToFocus = true;
}
this._controls.addEventListener("start", this.onControlsChangeStarted);
this._controls.addEventListener("endMovement", this.onControlsChangeEnded);
if (!this._startedListeningToKeyEvents && this.enableKeys) {
this._startedListeningToKeyEvents = true;
this._controls.listenToKeyEvents(this.context.domElement);
}
else {
try {
this._controls.stopListenToKeyEvents();
}
catch { /** this fails if we never listened to key events... */ }
}
}
this._syncedTransform = GameObject.getComponent(this.gameObject, SyncedTransform) ?? undefined;
this.context.pre_render_callbacks.push(this.__onPreRender);
this._activePointerEvents = [];
this.context.input.addEventListener("pointerdown", this._onPointerDown, { queue: InputEventQueue.Early });
this.context.input.addEventListener("pointerdown", this._onPointerDownLate, { queue: InputEventQueue.Late });
this.context.input.addEventListener("pointerup", this._onPointerUp, { queue: InputEventQueue.Early });
this.context.input.addEventListener("pointerup", this._onPointerUpLate, { queue: InputEventQueue.Late });
}
/** @internal */
onDisable() {
if (this._camera?.threeCamera) {
setCameraController(this._camera.threeCamera, this, false);
}
if (this._controls) {
this._controls.enabled = false;
this._controls.autoRotate = false;
this._controls.removeEventListener("start", this.onControlsChangeStarted);
this._controls.removeEventListener("endMovement", this.onControlsChangeEnded);
try {
this._controls.stopListenToKeyEvents();
}
catch { /** this fails if we never listened to key events... */ }
this._startedListeningToKeyEvents = false;
}
this._activePointerEvents.length = 0;
this.context.input.removeEventListener("pointerdown", this._onPointerDown);
this.context.input.removeEventListener("pointerdown", this._onPointerDownLate);
this.context.input.removeEventListener("pointerup", this._onPointerUp);
this.context.input.removeEventListener("pointerup", this._onPointerUpLate);
}
_activePointerEvents;
_lastTimeClickOnBackground = -1;
_clickOnBackgroundCount = 0;
_onPointerDown = (_evt) => {
this._activePointerEvents.push(_evt);
};
_onPointerDownLate = (evt) => {
if (evt.used && this._controls) {
// Disabling orbit controls here because otherwise we get a slight movement when e.g. using DragControls
this._controls.enabled = false;
}
};
_onPointerUp = (evt) => {
// make sure we cleanup the active pointer events
for (let i = this._activePointerEvents.length - 1; i >= 0; i--) {
const registered = this._activePointerEvents[i];
if (registered.pointerId === evt.pointerId && registered.button === evt.button) {
this._activePointerEvents.splice(i, 1);
break;
}
}
if (this.clickBackgroundToFitScene > 0 && evt.isClick && evt.button === 0) {
// it's possible that we didnt raycast in this frame
if (!evt.hasRay) {
evt.intersections.push(...this.context.physics.raycast());
}
if (evt.intersections.length <= 0) {
const dt = this.context.time.time - this._lastTimeClickOnBackground;
this._lastTimeClickOnBackground = this.context.time.time;
if (this.clickBackgroundToFitScene <= 1 || dt < this.clickBackgroundToFitScene * .15) {
this._clickOnBackgroundCount += 1;
if (this._clickOnBackgroundCount >= this.clickBackgroundToFitScene - 1) {
this.autoRotate = false;
this.fitCamera({
objects: this.context.scene,
immediate: false,
});
}
}
else {
this._clickOnBackgroundCount = 0;
}
}
if (debug)
console.log(this.clickBackgroundToFitScene, evt.intersections.length, this._clickOnBackgroundCount);
}
};
_onPointerUpLate = (evt) => {
if (this.doubleClickToFocus && evt.isDoubleClick && !evt.used) {
this.setTargetFromRaycast();
}
// Automatically update the camera focus
// else if (!evt.used && this.autoTarget) {
// this.updateTargetNow();
// }
};
updateTargetNow(options) {
const ray = new Ray(this._cameraObject?.worldPosition, this._cameraObject?.worldForward.multiplyScalar(-1));
const hits = this.context.physics.raycastFromRay(ray, options);
const hit = hits.length > 0 ? hits[0] : undefined;
if (hit && hit.distance > this.minZoom && hit.distance < this.maxZoom) {
if (debug)
Gizmos.DrawWireSphere(hit.point, 0.1, 0xff0000, 2);
this._controls?.target.copy(hits[0].point);
}
else {
if (debug)
console.log("OrbitControls: No hit found when updating target", { hits: [...hits] });
}
}
_orbitStartAngle = 0;
_zoomStartDistance = 0;
onControlsChangeStarted = () => {
if (debug)
console.debug("OrbitControls: Change started");
if (this._controls) {
this._orbitStartAngle = this._controls.getAzimuthalAngle() + this._controls.getPolarAngle();
this._zoomStartDistance = this._controls.getDistance();
}
if (this._syncedTransform) {
this._syncedTransform.requestOwnership();
}
};
onControlsChangeEnded = () => {
if (debug)
console.debug("OrbitControls: Change ended", { autoTarget: this.autoTarget });
if (this._controls) {
if (this.autoTarget) {
const newAngle = this._controls.getAzimuthalAngle() + this._controls.getPolarAngle();
const deltaAngle = newAngle - this._orbitStartAngle;
// TODO: "just zoom" probably shouldnt update the target either, unless zoomToCursor is enabled
if (Math.abs(deltaAngle) < .01) {
if (debug)
console.debug("OrbitControls: Update target", { deltaAngle });
this.updateTargetNow({ allowSlowRaycastFallback: false });
}
else if (debug)
console.debug("OrbitControls: No target update", { deltaAngle });
}
}
};
_shouldDisable = false;
afterHandleInput(evt) {
if (evt.detail.args.pointerId === 0) {
if (evt.detail.args.isDown) {
if (this._controls && this._eventSystem) {
this._shouldDisable = this._eventSystem.hasActiveUI;
}
}
else if (!evt.detail.args.isPressed || evt.detail.args.isUp) {
this._shouldDisable = false;
}
}
}
onPausedChanged(isPaused) {
if (!this._controls)
return;
if (isPaused)
this._controls.enabled = false;
}
/** @internal */
onBeforeRender() {
if (!this._controls)
return;
if (this._cameraObject !== this.context.mainCamera) {
this._controls.enabled = false;
return;
}
this._controls.enabled = true;
if (this.context.input.getPointerDown(1) || this.context.input.getPointerDown(2) || this.context.input.mouseWheelChanged || (this.context.input.getPointerPressed(0) && this.context.input.getPointerPositionDelta(0)?.length() || 0 > .1)) {
this._inputs += 1;
}
if (this._inputs > 0 && this.allowInterrupt) {
// if a user has disabled rotation but enabled auto rotate we don't want to change it when we receive input
if (this.enableRotate) {
this.autoRotate = false;
}
this._cameraLerpActive = false;
this._lookTargetLerpActive = false;
}
this._inputs = 0;
if (this.autoTarget) {
// we want to wait one frame so all matrixWorlds are updated
// otherwise raycasting will not work correctly
if (this._didSetTarget++ === 0) {
const camGo = GameObject.getComponent(this.gameObject, Camera);
if (camGo && !this.setLookTargetFromConstraint()) {
if (this.debugLog)
console.log("NO TARGET");
const worldPosition = getWorldPosition(camGo.threeCamera);
// Handle case where the camera is in 0 0 0 of the scene
// if the look at target is set to the camera position we can't move at all anymore
const distanceToCenter = Math.max(.01, worldPosition.length());
const forward = new Vector3(0, 0, -distanceToCenter).applyMatrix4(camGo.threeCamera.matrixWorld);
if (debug)
Gizmos.DrawLine(worldPosition, forward, 0x5555ff, 10);
this.setLookTargetPosition(forward, true);
}
if (!this.setLookTargetFromConstraint()) {
const opts = new RaycastOptions();
// center of the screen:
opts.screenPoint = new Vector2(0, 0);
opts.lineThreshold = 0.1;
const hits = this.context.physics.raycast(opts);
if (hits.length > 0) {
this.setLookTargetPosition(hits[0].point, true);
}
if (debugCameraFit)
console.log("OrbitControls hits", ...hits);
}
}
}
const focusAtPointer = (this.middleClickToFocus && this.context.input.getPointerClicked(1));
if (focusAtPointer) {
this.setTargetFromRaycast();
}
if (this._lookTargetLerpActive || this._cameraLerpActive || this._fovLerpActive) {
// lerp the camera
if (this._cameraLerpActive && this._cameraObject) {
this._cameraLerp01 += this.context.time.deltaTime / this._cameraLerpDuration;
if (this._cameraLerp01 >= 1) {
this._cameraObject.position.copy(this._cameraEndPosition);
this._cameraLerpActive = false;
this.dispatchEvent(new CameraTargetReachedEvent(this, "camera"));
}
else {
const t = Mathf.easeInOutCubic(this._cameraLerp01);
this._cameraObject.position.lerpVectors(this._cameraStartPosition, this._cameraEndPosition, t);
}
}
// lerp the look target
if (this._lookTargetLerpActive) {
this._lookTargetLerp01 += this.context.time.deltaTime / this._lookTargetLerpDuration;
if (this._lookTargetLerp01 >= 1) {
this.lerpLookTarget(this._lookTargetEndPosition, this._lookTargetEndPosition, 1);
this._lookTargetLerpActive = false;
this.dispatchEvent(new CameraTargetReachedEvent(this, "lookat"));
}
else {
const t = Mathf.easeInOutCubic(this._lookTargetLerp01);
this.lerpLookTarget(this._lookTargetStartPosition, this._lookTargetEndPosition, t);
}
}
// lerp the fov
if (this._fovLerpActive && this._cameraObject) {
const cam = this._cameraObject;
this._fovLerp01 += this.context.time.deltaTime / this._fovLerpDuration;
if (this._fovLerp01 >= 1) {
cam.fov = this._fovLerpEndValue;
this._fovLerpActive = false;
}
else {
const t = Mathf.easeInOutCubic(this._fovLerp01);
cam.fov = Mathf.lerp(this._fovLerpStartValue, this._fovLerpEndValue, t);
}
cam.updateProjectionMatrix();
}
}
if (this.targetBounds) {
// #region target bounds
const targetVector = this._controls.target;
const boundsCenter = this.targetBounds.worldPosition;
const boundsHalfSize = getTempVector(this.targetBounds.worldScale).multiplyScalar(0.5);
const min = getTempVector(boundsCenter).sub(boundsHalfSize);
const max = getTempVector(boundsCenter).add(boundsHalfSize);
const newTarget = getTempVector(this._controls.target).clamp(min, max);
const duration = .1;
if (duration <= 0)
targetVector.copy(newTarget);
else
targetVector.lerp(newTarget, this.context.time.deltaTime / duration);
if (this._lookTargetLerpActive) {
if (duration <= 0)
this._lookTargetEndPosition.copy(newTarget);
else
this._lookTargetEndPosition.lerp(newTarget, this.context.time.deltaTime / (duration * 5));
}
if (debug) {
Gizmos.DrawWireBox(boundsCenter, boundsHalfSize.multiplyScalar(2), 0xffaa00);
}
}
if (this._controls) {
if (this.debugLog)
this._controls.domElement = this.context.renderer.domElement;
const viewZoomFactor = 1 / (this.context.focusRectSettings?.zoom || 1);
this._controls.enabled = !this._shouldDisable && this._camera === this.context.mainCameraComponent && !this.context.isInXR && !this._activePointerEvents.some(e => e.used);
this._controls.keys = this.enableKeys ? defaultKeys : disabledKeys;
this._controls.autoRotate = this.autoRotate;
this._controls.autoRotateSpeed = this.autoRotateSpeed;
this._controls.enableZoom = this.enableZoom;
this._controls.zoomSpeed = this.zoomSpeed;
this._controls.zoomToCursor = this.zoomToCursor;
this._controls.enableDamping = this.enableDamping;
this._controls.dampingFactor = this.dampingFactor;
this._controls.enablePan = this.enablePan;
this._controls.panSpeed = viewZoomFactor;
this._controls.enableRotate = this.enableRotate;
this._controls.minAzimuthAngle = this.minAzimuthAngle;
this._controls.maxAzimuthAngle = this.maxAzimuthAngle;
this._controls.minPolarAngle = this.minPolarAngle;
this._controls.maxPolarAngle = this.maxPolarAngle;
// set the min/max zoom if it's not a free cam
if (!freeCam) {
if (this._camera?.threeCamera?.type === "PerspectiveCamera") {
this._controls.minDistance = this.minZoom;
this._controls.maxDistance = this.maxZoom;
this._controls.minZoom = 0;
this._controls.maxZoom = Infinity;
}
else {
this._controls.minDistance = 0;
this._controls.maxDistance = Infinity;
this._controls.minZoom = this.minZoom;
this._controls.maxZoom = this.maxZoom;
}
}
if (typeof smoothcam === "number" || smoothcam === true) {
this._controls.enableDamping = true;
const factor = typeof smoothcam === "number" ? smoothcam : .99;
this._controls.dampingFactor = Math.max(.001, 1 - Math.min(1, factor));
}
if (!this.allowInterrupt) {
if (this._lookTargetLerpActive) {
this._controls.enablePan = false;
}
if (this._cameraLerpActive) {
this._controls.enableRotate = false;
this._controls.autoRotate = false;
}
if (this._lookTargetLerpActive || this._cameraLerpActive) {
this._controls.enableZoom = false;
}
}
// this._controls.zoomToCursor = this.zoomToCursor;
if (!this.context.isInXR) {
if (!freeCam && this.lookAtConstraint?.locked && !this._lookTargetLerpActive) {
this.setLookTargetFromConstraint(0, this.lookAtConstraint01);
}
this._controls.update(this.context.time.deltaTime);
if (debug) {
Gizmos.DrawWireSphere(this._controls.target, 0.1, 0x00ff00);
}
}
}
}
__onPreRender = () => {
// We call this only once when the camera becomes active and use the engine pre_render_callbacks because they are run
// after all scripts have been executed
const index = this.context.pre_render_callbacks.indexOf(this.__onPreRender);
if (index >= 0) {
this.context.pre_render_callbacks.splice(index, 1);
}
if (this.autoFit) {
// we don't want to autofit again if the component is disabled and re-enabled
this.autoFit = false;
this.fitCamera({
centerCamera: "y",
immediate: true,
objects: this.scene.children,
});
}
};
/**
* Sets camera target position and look direction using a raycast in forward direction of the object.
*
* @param source The object to raycast from. If a camera is passed in the camera position will be used as the source.
* @param immediateOrDuration If true the camera target will move immediately to the new position, otherwise it will lerp. If a number is passed in it will be used as the duration of the lerp.
*
* This is useful for example if you want to align your camera with an object in your scene (or another camera). Simply pass in this other camera object
* @returns true if the target was set successfully
*/
setCameraAndLookTarget(source, immediateOrDuration = false) {
if (!source) {
if (isDevEnvironment() || debug)
console.warn("[OrbitControls] setCameraAndLookTarget target is null");
return false;
}
if (!(source instanceof Object3D) && !(source instanceof Camera)) {
if (isDevEnvironment() || debug)
console.warn("[OrbitControls] setCameraAndLookTarget target is not an Object3D or Camera");
return false;
}
if (source instanceof Camera) {
source = source.gameObject;
}
const worldPosition = source.worldPosition;
const forward = source.worldForward;
// The camera render direction is -Z. When a camera is passed in then we'll take the view direction OR the object Z forward direction.
if (source instanceof Camera3) {
if (debug)
console.debug("[OrbitControls] setCameraAndLookTarget flip forward direction for camera");
forward.multiplyScalar(-1);
}
const ray = new Ray(worldPosition, forward);
if (debug)
Gizmos.DrawRay(ray.origin, ray.direction, 0xff0000, 10);
if (!this.setTargetFromRaycast(ray, immediateOrDuration)) {
this.setLookTargetPosition(ray.at(2, getTempVector()), immediateOrDuration);
}
this.setCameraTargetPosition(worldPosition, immediateOrDuration);
return true;
}
/** Moves the camera to position smoothly.
* @param position The position in local space of the controllerObject to move the camera to. If null the camera will stop lerping to the target.
* @param immediateOrDuration If true the camera will move immediately to the new position, otherwise it will lerp. If a number is passed in it will be used as the duration of the lerp.
*/
setCameraTargetPosition(position, immediateOrDuration = false) {
if (!position)
return;
if (position instanceof Object3D) {
position = getWorldPosition(position);
}
if (!this._cameraEndPosition)
this._cameraEndPosition = new Vector3();
this._cameraEndPosition.copy(position);
if (immediateOrDuration === true) {
this._cameraLerpActive = false;
if (this._cameraObject) {
this._cameraObject.position.copy(this._cameraEndPosition);
}
}
else if (this._cameraObject) {
this._cameraLerpActive = true;
this._cameraLerp01 = 0;
this._cameraStartPosition.copy(this._cameraObject?.position);
if (typeof immediateOrDuration === "number") {
this._cameraLerpDuration = immediateOrDuration;
}
else
this._cameraLerpDuration = this.targetLerpDuration;
}
}
// public setCameraTargetRotation(rotation: Vector3 | Euler | Quaternion, immediateOrDuration: boolean | number = false): void {
// if (!this._cameraObject) return;
// if (typeof immediateOrDuration === "boolean") immediateOrDuration = immediateOrDuration ? 0 : this.targetLerpDuration;
// const ray = new Ray(this._cameraObject.worldPosition, getTempVector(0, 0, 1));
// // if the camera is in the middle of lerping we use the end position for the raycast
// if (immediateOrDuration > 0 && this._cameraEndPosition && this._cameraLerpActive) {
// ray.origin = getTempVector(this._cameraEndPosition)
// }
// if (rotation instanceof Vector3) {
// rotation = new Euler().setFromVector3(rotation);
// }
// if (rotation instanceof Euler) {
// rotation = new Quaternion().setFromEuler(rotation);
// }
// ray.direction.applyQuaternion(rotation);
// ray.direction.multiplyScalar(-1);
// const hits = this.context.physics.raycastFromRay(ray);
// if (hits.length > 0) {
// this.setCameraTargetPosition(hits[0].point, immediateOrDuration);
// }
// else {
// this.setLookTargetPosition(ray.at(2, getTempVector()));
// }
// }
/** True while the camera position is being lerped */
get cameraLerpActive() { return this._cameraLerpActive; }
/** Call to stop camera position lerping */
stopCameraLerp() {
this._cameraLerpActive = false;
}
setFieldOfView(fov, immediateOrDuration = false) {
if (!this._controls)
return;
if (typeof fov !== "number")
return;
const cam = this._camera?.threeCamera;
if (!cam)
return;
if (immediateOrDuration === true) {
cam.fov = fov;
}
else {
this._fovLerpActive = true;
this._fovLerp01 = 0;
this._fovLerpStartValue = cam.fov;
this._fovLerpEndValue = fov;
if (typeof immediateOrDuration === "number") {
this._fovLerpDuration = immediateOrDuration;
}
else
this._fovLerpDuration = this.targetLerpDuration;
}
// if (this.context.mainCameraComponent) this.context.mainCameraComponent.fieldOfView = fov;
}
/** Moves the camera look-at target to a position smoothly.
* @param position The position in world space to move the camera target to. If null the camera will stop lerping to the target.
* @param immediateOrDuration If true the camera target will move immediately to the new position, otherwise it will lerp. If a number is passed in it will be used as the duration of the lerp.
*/
setLookTargetPosition(position = null, immediateOrDuration = false) {
if (!this._controls)
return;
if (!position)
return;
if (position instanceof Object3D) {
position = getWorldPosition(position);
}
this._lookTargetEndPosition.copy(position);
// if a user calls setLookTargetPosition we don't want to perform autoTarget in onBeforeRender (and override whatever the user set here)
this._didSetTarget++;
if (debug) {
console.warn("OrbitControls: setLookTargetPosition", position, immediateOrDuration);
Gizmos.DrawWireSphere(this._lookTargetEndPosition, .2, 0xff0000, 2);
}
if (immediateOrDuration === true) {
this.lerpLookTarget(this._lookTargetEndPosition, this._lookTargetEndPosition, 1);
}
else {
this._lookTargetLerpActive = true;
this._lookTargetLerp01 = 0;
this._lookTargetStartPosition.copy(this._controls.target);
if (typeof immediateOrDuration === "number") {
this._lookTargetLerpDuration = immediateOrDuration;
}
else
this._lookTargetLerpDuration = this.targetLerpDuration;
}
}
/** True while the camera look target is being lerped */
get lookTargetLerpActive() { return this._lookTargetLerpActive; }
/** Call to stop camera look target lerping */
stopLookTargetLerp() {
this._lookTargetLerpActive = false;
}
/** Sets the look at target from an assigned lookAtConstraint source by index
* @param index The index of the source to use
* @param t The interpolation factor between the current look at target and the new target
*/
setLookTargetFromConstraint(index = 0, t = 1) {
if (!this._controls)
return false;
if (this.lookAtConstraint?.enabled === false)
return false;
const sources = this.lookAtConstraint?.sources;
if (sources && sources.length > 0) {
const target = sources[index];
if (target) {
target.getWorldPosition(this._lookTargetEndPosition);
this.lerpLookTarget(this._controls.target, this._lookTargetEndPosition, t);
return true;
}
}
return false;
}
lerpLookTarget(start, position, t) {
if (!this._controls)
return;
if (t >= 1)
this._controls.target.copy(position);
else
this._controls.target.lerpVectors(start, position, t);
if (this.lookAtConstraint)
this.lookAtConstraint.setConstraintPosition(this._controls.target);
}
setTargetFromRaycast(ray, immediateOrDuration = false) {
if (!this.controls)
return false;
const rc = ray ? this.context.physics.raycastFromRay(ray) : this.context.physics.raycast();
for (const hit of rc) {
if (hit.distance > 0 && GameObject.isActiveInHierarchy(hit.object)) {
const uiComponent = tryGetUIComponent(hit.object);
if (uiComponent) {
const canvas = uiComponent.canvas;
if (canvas?.screenspace) {
break;
}
}
this.setLookTargetPosition(hit.point, immediateOrDuration);
return true;
}
}
return false;
}
fitCamera(objectsOrOptions, options) {
let objects = undefined;
// If the user passed in an array as first argument
if (Array.isArray(objectsOrOptions)) {
objects = objectsOrOptions;
}
// If the user passed in an object as first argument
else if (objectsOrOptions && "type" in objectsOrOptions) {
objects = objectsOrOptions;
}
// If the user passed in an object as first argument and options as second argument
else if (objectsOrOptions && typeof objectsOrOptions === "object") {
if (!(objectsOrOptions instanceof Object3D) && !Array.isArray(objectsOrOptions)) {
options = objectsOrOptions;
objects = options.objects;
}
}
// Ensure objects are setup correctly
if (objects && !Array.isArray(objects)) {
objects = [objects];
}
if (!Array.isArray(objects) || objects && objects.length <= 0) {
objects = this.context.scene.children;
}
// Make sure there's anything to fit to
if (!Array.isArray(objects) || objects.length <= 0) {
console.warn("No objects to fit camera to...");
return;
}
const res = fitCamera({
objects: [...objects],
...options,
autoApply: false,
context: this.context,
camera: this._cameraObject,
currentZoom: this._controls?.getDistance() || undefined,
minZoom: this.minZoom,
maxZoom: this.maxZoom,
});
if (!res)
return;
this.setLookTargetPosition(res.lookAt, options?.immediate || false);
this.setCameraTargetPosition(res.position, options?.immediate || false);
this.setFieldOfView(options?.fov, options?.immediate || false);
this.onBeforeRender();
}
_haveAttachedKeyboardEvents = false;
}
__decorate([
serializable()
], OrbitControls.prototype, "autoTarget", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "autoFit", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "enableRotate", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "autoRotate", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "autoRotateSpeed", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "minAzimuthAngle", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "maxAzimuthAngle", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "minPolarAngle", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "maxPolarAngle", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "enableKeys", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "enableDamping", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "dampingFactor", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "enableZoom", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "minZoom", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "maxZoom", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "zoomSpeed", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "enablePan", void 0);
__decorate([
serializable(LookAtConstraint)
], OrbitControls.prototype, "lookAtConstraint", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "lookAtConstraint01", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "allowInterrupt", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "middleClickToFocus", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "doubleClickToFocus", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "clickBackgroundToFitScene", void 0);
__decorate([
serializable()
], OrbitControls.prototype, "targetLerpDuration", null);
__decorate([
serializable(Object3D)
], OrbitControls.prototype, "targetBounds", void 0);
//# sourceMappingURL=OrbitControls.js.map