import { Flags, StateGroup, Timer } from '@uino/base-thing';
import { Utils } from '../common/Utils';
import { MathUtils } from '../math/MathUtils';
import { BaseComponent } from './BaseComponent';
import { EventType, ProjectionType, PickType } from '../const';
const __ = {
private: Symbol('private'),
}
const Flag = {
Damping: 1 << 0,
Rotate: 1 << 1,
Pan: 1 << 2,
Zoom: 1 << 3,
MapControl: 1 << 4,
AutoMoveTargetForward: 1 << 5,
AutoAdjustTargetPosition: 1 << 6,
AutoAdjustNear: 1 << 7,
AutoAdjustPanSpeed: 1 << 8,
ZoomToMouseCursorOnWheel: 1 << 9,
PickWhenChanging: 1 << 10,
Changing: 1 << 11,
EnableUpdateTargetByChanging: 1 << 12,
}
const cZoomToMouseCursorOnWheel = '__cZoomToMouseCursorOnWheel__';
const cZoomEventTag = '__cZoomEventTag__';
const cZoomToMouseCursorOnWheelEventTag = '__cZoomToMouseCursorOnWheelEventTag__';
const cAutoAdjustTargetPositionEventTag = '__cAutoAdjustTargetPositionEventTag__';
const cAppClickEventTag = '__cAppClickEventTag__';
const cAppDBLClickEventTag = '__cAppDBLClickEventTag__';
const cPickerGroupName = 'OrbitControls-start';
const cPickerPriority = 100 * 100;
let _vec3 = MathUtils.createVec3();
let _position = MathUtils.createVec3();
let _target = MathUtils.createVec3();
let _forward = MathUtils.createVec3();
let _direction = MathUtils.createVec3();
// #region Private Functions
// #endregion
/**
* @class CameraControlComponent
* The camera controller component.
* @memberof THING
* @extends THING.BaseComponent
* @public
*/
class CameraControlComponent extends BaseComponent {
/**
* The camera controller.
*/
constructor() {
super();
this[__.private] = {};
let _private = this[__.private];
_private.controller = null;
_private.flags = new Flags();
_private.stateGroup = new StateGroup({
onChange: (ev) => {
let controller = this.getController();
controller.active(ev.state);
}
});
_private.changeTickcount = 0;
_private.crossPlanes = null;
_private.intervalTimers = {};
_private.adjustCallbacks = {
panSpeed: null,
};
_private.distanceLimited = null;
_private.boundary = null;
_private.zoomToMouseCursorOnWheelSpeed = 0.05;
}
// #region Private Functions
// Unproject screen to world position.
_unprojectScreenToWorld(x, y, factor = -1) {
let app = this.app;
// Fix by pixel ratio
let pixelRatio = app.pixelRatio;
x *= pixelRatio;
y *= pixelRatio;
// Convert position into [-1, 1] range
let size = app.size;
let pX = (x / size[0]) * 2 - 1;
let pY = -(y / size[1]) * 2 + 1;
// Convert to world position
this.object.node.unproject(_position, MathUtils.vec3.set(_position, pX, pY, factor));
return _position;
}
// Clear picker state.
_clearPickerState() {
this.app.picker.stateGroup.enable(true, cPickerGroupName, -cPickerPriority);
}
// Register events.
_registerEvents() {
let app = this.app;
app.on('click', (ev) => {
app.picker.stateGroup.enable(true, cPickerGroupName, -cPickerPriority);
}, cAppClickEventTag);
app.on('dblclick', (ev) => {
app.picker.stateGroup.enable(true, cPickerGroupName, -cPickerPriority);
}, cAppDBLClickEventTag);
}
// Unregister events.
_unregisterEvents() {
let app = this.app;
app.off('click', cAppClickEventTag);
app.off('dblclick', cAppDBLClickEventTag);
}
_createCrossPlanes() {
let _private = this[__.private];
// Create cross planes to pick
let plane = this.app.global.cache.models['plane'];
const size = 1000 * 1000 * 1000;
const planeSize = [size, size, size];
// Create horz plane
let horzPlane = plane.clone();
horzPlane.setVisible(false);
horzPlane.setScale(planeSize);
horzPlane.setQuaternion(MathUtils.getQuatFromAngles([-90, 0, 0]));
// Create vert plane
let vertPlane = plane.clone();
vertPlane.setVisible(false);
vertPlane.setScale(planeSize);
// Build cross planes as scene
let crossPlanes = Utils.createObject('Node');
crossPlanes.setVisible(false);
crossPlanes.add(horzPlane);
crossPlanes.add(vertPlane);
crossPlanes.horzPlane = horzPlane;
crossPlanes.vertPlane = vertPlane;
_private.crossPlanes = crossPlanes;
// Update cross planes
_private.intervalTimers['crossPlanes'] = new Timer({
interval: 0.25,
onInterval: () => {
let object = this.object;
if (this.isChanging) {
let position = object.getWorldPosition(_position);
let target = object.control.getTarget(_target);
vertPlane.lookAt(position);
crossPlanes.setPosition(target);
crossPlanes.updateMatrixWorld();
}
}
});
}
_dispatchChangingEvent() {
let _private = this[__.private];
_private.changeTickcount = 0;
_private.flags.enable(Flag.Changing, true);
}
_createController() {
let controller = Utils.createObject('OrbitControls', {
camera: this.object.node,
appDelegate: this.app.delegate
});
controller.active(false);
return controller;
}
/**
* Check whether need to look at target
* @returns {Boolean}
* @private
*/
_isNeedLookAtTarget() {
if (this.enableZoomToMouseCursorOnWheel) {
return false;
}
let _private = this[__.private];
if (_private.controller.isActivated()) {
return false;
}
return true;
}
_getSourcePositionByDistance(distance) {
let target = this.getTarget();
let source = this.object.position;
let direction = MathUtils.getDirection(source, target);
let offset = MathUtils.scaleVector(direction, distance);
return MathUtils.addVector(source, offset);
}
_getTargetPositionByDistance(distance) {
let target = this.getTarget();
let source = this.object.position;
let direction = MathUtils.getDirection(source, target);
let offset = MathUtils.scaleVector(direction, distance);
return MathUtils.addVector(target, offset);
}
_canTargetMoveForwardByDistanceLimited(ev) {
let _private = this[__.private];
if (ev.delta > 0) {
return false;
}
if (!this.enableAutoMoveTargetForward) {
return false;
}
if (_private.distanceLimited && Utils.isValid(_private.distanceLimited[0])) {
return false;
}
return true;
}
_updateTargetByMoveForwardAction(minDistance) {
// Check distance
let distance = this.distance;
if (distance > minDistance) {
return;
}
// Get the forward target position
let targetPosition = this._getTargetPositionByDistance(minDistance * 2);
if (this._canTargetMoveForwardByBoundary(targetPosition)) {
// Update target position
this.setTarget(targetPosition);
}
}
_canTargetMoveForwardByBoundary(targetPosition) {
let _private = this[__.private];
let boundary = _private.boundary;
if (boundary) {
if (targetPosition[0] <= boundary.min[0] || targetPosition[1] <= boundary.min[1] || targetPosition[2] <= boundary.min[2]) {
return false;
}
if (targetPosition[0] >= boundary.max[0] || targetPosition[1] >= boundary.max[1] || targetPosition[2] >= boundary.max[2]) {
return false;
}
}
return true;
}
_pickFromCrossPlanes(x, y, isVertical) {
let _private = this[__.private];
// Try to pick from cross planes
let crossPlanes = _private.crossPlanes;
if (!crossPlanes) {
return null;
}
const vertPlane = crossPlanes.vertPlane;
const horzPlane = crossPlanes.horzPlane;
const picker = this.object.picker;
let vertPlaneDistance = this.object.distance;
let vertPlaneNormal = vertPlane.getForward(_direction);
let horzPlaneDistance = vertPlaneDistance;
if (!isVertical) {
// The face facing the camera and positioned at the camera's target
vertPlaneNormal = THING.Math.subVector(this.object.position, this.object.target);
vertPlaneDistance = -THING.Math.dotVector(vertPlaneNormal, this.object.target);
// The horizontal plane is always at y=0
horzPlaneDistance = 0;
}
let results = [];
let vertResult = picker.intersectPlane(x, y, vertPlaneNormal, vertPlaneDistance);
if (vertResult) {
results.push(vertResult);
}
let horzResult = picker.intersectPlane(x, y, horzPlane.getForward(_direction), horzPlaneDistance);
if (horzResult) {
results.push(horzResult);
}
return results;
}
_pickInWorld(x, y, testCrossPlanes = true, fastMode = false) {
let result;
// Try to pick from scene
if (fastMode) {
let prevPickType = this.object.pickType;
this.object.pickType = PickType.GPUFast;
result = this.object.pick(x, y);
this.object.pickType = prevPickType;
}
else {
result = this.object.pick(x, y);
}
if (result) {
return result.position;
}
// Try to pick from cross planes
if (testCrossPlanes) {
result = this.pickFromCrossPlanes(x, y);
if (result) {
return result;
}
}
return null;
}
_pick(x, y) {
let pickedPosition = this._pickInWorld(x, y);
if (pickedPosition) {
return pickedPosition;
}
_position[0] = x;
_position[1] = y;
_position[2] = 0.5;
let object = this.object;
let position = object.position;
let direction = MathUtils.normalizeVector(MathUtils.subVector(position, object.screenToWorld(_position)));
return MathUtils.addVector(position, MathUtils.scaleVector(direction, -50));
}
_getCenterPickedPosition() {
let size = this.app.size;
let halfWidth = size[0] / 2;
let halfHeight = size[1] / 2;
return this._pick(halfWidth, halfHeight);
}
/**
* Update zoom to mouse cursor on wheel (delta < 0: forward, delta > 0: backward).
* @param {Object} ev The wheel event object.
* @private
*/
_updateZoomToMouseCursorOnWheel(ev) {
let _private = this[__.private];
// Get the mouse picked position
let mousePickedPosition = this._pick(ev.x, ev.y);
// Get the screen center picked position
let centerPickedPosition = this._getCenterPickedPosition();
// Get the factor for calculating offset
let object = this.object;
let distance = object.distanceTo(mousePickedPosition);
let factor = distance * _private.zoomToMouseCursorOnWheelSpeed;
factor *= this.zoomSpeed;
// Get the offset from center to mouse picked position
let direction = MathUtils.normalizeVector(MathUtils.subVector(mousePickedPosition, object.position));
let offset = MathUtils.scaleVector(direction, factor);
// Update camera position
let cameraNode = object.node;
cameraNode.getPosition(_position);
if (ev.delta < 0) {
_position = MathUtils.addVector(_position, offset);
}
else {
_position = MathUtils.subVector(_position, offset);
}
cameraNode.setPosition(_position);
// Update camera target
this.setTarget(centerPickedPosition);
}
_enableAdjustTargetPosition(value) {
let _private = this[__.private];
let app = this.app;
// Enable
if (value) {
app.on('mousedown', (ev) => {
// Only works for pan move action
if (ev.button != 2) {
return;
}
// If pan is disable then skip to adjust target position
if (!this.enablePan) {
return;
}
// Get object and app size
let object = this.object;
let size = app.size;
// Try to update target poistion by picking from center position of screen
let pickedPosition = this._pickInWorld(size[0] / 2, size[1] / 2, false, true);
if (!pickedPosition) {
return;
}
// Get camera info
let target = object.control.getTarget(_target);
let position = object.getWorldPosition(_position);
let forward = object.getForward(_forward);
// Get the distances
let curDistance = object.distance;
let distanceToPickedPosition = MathUtils.getDistance(pickedPosition, position);
let distanceFromTargetToPickedPosition = MathUtils.getDistance(pickedPosition, target);
// Check for moving forward
if (curDistance >= distanceToPickedPosition) {
distanceFromTargetToPickedPosition *= -1;
}
// Get the target result
let targetResult = MathUtils.addVector(target, MathUtils.scaleVector(forward, distanceFromTargetToPickedPosition));
// Check limited distance
let distanceLimited = _private.distanceLimited;
if (distanceLimited) {
let distanceToTarget = MathUtils.getDistance(position, targetResult);
if (distanceToTarget <= distanceLimited[0]) {
return;
}
if (distanceToTarget >= distanceLimited[1]) {
return;
}
}
// Make sure the target position is in the boundary region
if (_private.boundary) {
if (!MathUtils.intersectsPoint(targetResult, _private.boundary.min, _private.boundary.max)) {
return;
}
}
// Update the target
this.setTarget(targetResult);
}, cAutoAdjustTargetPositionEventTag);
}
// Disable
else {
app.off('mousedown', cAutoAdjustTargetPositionEventTag);
}
}
_enableZoomToMouseCursorOnWheel(value) {
let _private = this[__.private];
let stateGroup = _private.stateGroup;
let app = this.app;
// Enable
if (value) {
this.getController().enable('Zoom', false);
// We must wait for global cache finished
app.global.waitForComplete().then(() => {
// Register events
app.on('wheel', (ev) => {
if (stateGroup.getValue('default') === false) {
return;
}
if (this.enableZoom) {
stateGroup.enable(false, cZoomToMouseCursorOnWheel, 100000);
this._updateZoomToMouseCursorOnWheel(ev);
}
}, cZoomToMouseCursorOnWheelEventTag);
app.on('mousedown', (ev) => {
stateGroup.enable(true, cZoomToMouseCursorOnWheel, 0);
}, cZoomToMouseCursorOnWheelEventTag);
});
}
// Disable
else {
this.getController().enable('Zoom', this.enableZoom);
stateGroup.enable(true, cZoomToMouseCursorOnWheel, 0);
// Unregister events
app.off('wheel', cZoomToMouseCursorOnWheelEventTag);
app.off('mousedown', cZoomToMouseCursorOnWheelEventTag);
}
}
_getDistanceToTargetInPickingMode() {
let object = this.object;
// Get the screen position of target
let cameraTarget = object.control.getTarget(_target);
let screenPosition = object.worldToScreen(cameraTarget, _position);
// Try to pick world position of target in screen
let pickedPosition = this._pickInWorld(screenPosition[0], screenPosition[1], false, true);
if (!pickedPosition) {
return null;
}
// Get the camera distance to picked position
let cameraPosition = object.getWorldPosition(_vec3);
return MathUtils.getDistance(pickedPosition, cameraPosition);
}
_processAdjustPanSpeed() {
let _private = this[__.private];
if (_private.adjustCallbacks['onPanSpeed']) {
let panSpeed = _private.adjustCallbacks['onPanSpeed']();
if (panSpeed) {
this.panSpeed = panSpeed;
}
}
else {
// Get the distance to target
let distance = this.object.distance;
if (!distance) {
return;
}
// Update pan speed of render camera
const factor = 30;
this.panSpeed = MathUtils.clamp(1.0, 0.5, distance / factor);
}
}
_processControllerEndEvent() {
let _private = this[__.private];
this._clearPickerState();
_private.flags.enable(Flag.Changing, false);
}
/**
* Notify the outside world of camera changes.
* @param {String} eventType Event type.
* @private
*/
_change(eventType) {
const object = this.object;
object.getWorldPosition(_position);
object.control.getTarget(_target);
this.app.trigger(eventType, {
position: _position,
target: _target
});
}
// #endregion
// #region BaseComponent Interface
onUpdate(deltaTime) {
let _private = this[__.private];
let object = this.object;
// Update changing flag
if (_private.changeTickcount != -1) {
// Check pre 10ms
if (_private.changeTickcount >= 0.01) {
_private.changeTickcount = -1;
_private.flags.enable(Flag.Changing, false);
}
else {
_private.changeTickcount += deltaTime;
}
}
// Update controller
let controller = _private.controller;
if (controller && controller.isActivated()) {
this.getController().update(deltaTime);
}
// Update adjust near interval timer
for (let key in _private.intervalTimers) {
let timer = _private.intervalTimers[key];
timer.update(deltaTime);
}
}
onAdd(object) {
super.onAdd(object);
this._createCrossPlanes();
this.enableDamping = true;
this.enableRotate = true;
this.enablePan = true;
this.enableZoom = true;
this.enableAutoMoveTargetForward = true;
this.enableAdjustTargetPosition = true;
this.enableAdjustNear = true;
this.enableAdjustPanSpeed = true;
this.enablePickWhenChanging = false; // Disable pick when changing to speed up
this.enableUpdateTargetByChanging = true;
this._registerEvents();
}
onRemove() {
let _private = this[__.private];
this._unregisterEvents();
this.enableDamping = false;
this.enableRotate = false;
this.enablePan = false;
this.enableZoom = false;
this.enableAutoMoveTargetForward = false;
this.enableAdjustTargetPosition = false;
this.enableAdjustNear = false;
this.enableAdjustPanSpeed = false;
this.enablePickWhenChanging = true;
this.enableUpdateTargetByChanging = false;
if (_private.crossPlanes) {
_private.crossPlanes.dispose();
_private.crossPlanes = null;
}
if (_private.controller) {
_private.controller.dispose();
_private.controller = null;
}
if (_private.stateGroup) {
_private.stateGroup.clear();
_private.stateGroup = null;
}
super.onRemove();
}
onCopy(component) {
this.target = component.target;
this.enableDamping = component.enableDamping;
this.enableRotate = component.enableRotate;
this.enablePan = component.enablePan;
this.enableZoom = component.enableZoom;
this.enableAutoMoveTargetForward = component.enableAutoMoveTargetForward;
this.enableAdjustTargetPosition = component.enableAdjustTargetPosition;
this.enableAdjustNear = component.enableAdjustNear;
this.enableAdjustPanSpeed = component.enableAdjustPanSpeed;
this.enablePickWhenChanging = component.enablePickWhenChanging;
this.dampingFactor = component.dampingFactor;
this.zoomSpeed = component.zoomSpeed;
this.rotateSpeed = component.rotateSpeed;
this.panSpeed = component.panSpeed;
this.keyPanSpeed = component.keyPanSpeed;
this.zoomToMouseCursorOnWheelSpeed = component.zoomToMouseCursorOnWheelSpeed;
this.distanceLimited = component.distanceLimited;
this.vertAngleLimit = component.vertAngleLimit;
this.horzAngleLimit = component.horzAngleLimit;
this.boundary = component.boundary;
this.target = component.target;
}
// #endregion
getTarget(target = [0, 0, 0]) {
return this.getController().getTargetWorldPosition(target);
}
setTarget(value) {
if (this._isNeedLookAtTarget()) {
this.object.node.lookAt(value);
}
this.getController().setTargetWorldPosition(value);
}
getController() {
let _private = this[__.private];
if (!_private.controller) {
let controller = this._createController();
this.setController(controller);
}
return _private.controller;
}
setController(controller) {
let _private = this[__.private];
if (controller) {
// Set default options
controller.setKeyPanSpeed(0.5);
controller.setPanSpeed(0.5);
controller.setRotateSpeed(0.95);
controller.setDampingFactor(0.09);
// Register controller event
controller.addEventListener('preStart', (ev) => {
this._change(EventType.CameraChangePreStart);
});
controller.addEventListener('start', (ev) => {
if (!this.enablePickWhenChanging) {
this.app.picker.stateGroup.enable(false, cPickerGroupName, cPickerPriority);
}
_private.flags.enable(Flag.Changing, true);
this._change(EventType.CameraChangeStart);
});
controller.addEventListener('preEnd', (ev) => {
this._processControllerEndEvent();
this._change(EventType.CameraChangePreEnd);
});
controller.addEventListener('end', (ev) => {
this._processControllerEndEvent();
this._change(EventType.CameraChangeEnd);
});
controller.addEventListener('wheel', (ev) => {
if (this._canTargetMoveForwardByDistanceLimited(ev)) {
this._updateTargetByMoveForwardAction(5);
}
this._dispatchChangingEvent();
this._change(EventType.CameraChange);
});
controller.addEventListener('change', (ev) => {
this._dispatchChangingEvent();
this._change(EventType.CameraChange);
});
_private.controller = controller;
}
else {
if (_private.controller) {
_private.controller.dispose();
_private.controller = null;
}
}
}
stopZoom() {
let object = this.object;
object.lerp.stop(cZoomEventTag);
}
/**
* Pan in screen by pixel.
* @param {Number} deltaX The delta X in pixel.
* @param {Number} deltaY The delta Y in pixel.
* @param {Number} [duration=500] The time in milliseconds.
* @example
* // Move camera with bottom direction
* app.camera.pan(0, -50);
* @public
*/
pan(deltaX, deltaY, duration = 500) {
let size = this.app.size;
let object = this.object;
let distance = object.distance;
// half of the fov is center to top of screen
distance *= Math.tan((object.fov / 2) * Math.PI / 180.0);
// we use only client height here so aspect ratio does not distort speed
let offset = [0, 0, 0];
let leftColumn = MathUtils.getVec3FromMatrixColumn(object.matrix, 0);
offset = MathUtils.addVector(offset, MathUtils.scaleVector(leftColumn, 2 * deltaX * distance / size[1]));
let upColumn = MathUtils.getVec3FromMatrixColumn(object.matrix, 1);
offset = MathUtils.addVector(offset, MathUtils.scaleVector(upColumn, 2 * deltaY * distance / size[1]));
// Get the final position and target
let position = MathUtils.addVector(object.position, offset);
let target = MathUtils.addVector(object.target, offset);
// Lerp to pan
if (duration) {
object.lerp.stopFlying();
object.lerp.to({
from: {
position: object.position,
target: object.target,
},
to: {
position,
target,
},
duration,
}, cZoomEventTag);
}
// Zoom directly
else {
object.position = position;
object.target = target;
}
}
/**
* Move forward/backward.
* @param {Number} distance The distance (+: forward, -: backward).
* @param {Object} param The options.
* @param {Number} [param.duration=500] The time in milliseconds.
* @param {Boolean} [param.updateTarget=true] True indicates update target's position.
*/
zoom(distance, options = {}) {
if (!distance) {
return;
}
let duration = Utils.parseValue(options['duration'], Utils.parseValue(options['time'], 500));
let updateTarget = Utils.parseValue(options['updateTarget'], true);
let object = this.object;
object.stopFlying();
switch (object.projectionType) {
case ProjectionType.Orthographic:
let orthoDistance = Math.abs(object.orthoDistance - distance);
// Lerp to zoom
if (duration) {
object.lerp.to({
to: {
orthoDistance: orthoDistance
},
duration,
}, cZoomEventTag);
}
// Zoom directly
else {
object.orthoDistance = orthoDistance;
}
// Move target also to make Orhot<->Perspective much smoothly, but it's optional
object.position = this._getSourcePositionByDistance(distance);
if (updateTarget) {
let direction = MathUtils.getDirection(object.position, object.target);
object.target = MathUtils.getPositionOnDirection(object.position, direction, object.orthoDistance);
}
break;
case ProjectionType.Perspective:
let position = this._getSourcePositionByDistance(distance);
if (updateTarget) {
let target = this._getTargetPositionByDistance(distance);
// Lerp to zoom
if (duration) {
object.lerp.to({
from: {
position: object.position,
target: object.target,
},
to: {
position,
target,
},
duration,
}, cZoomEventTag);
}
// Zoom directly
else {
object.position = position;
object.target = target;
}
}
else {
// Lerp to zoom
if (duration) {
object.lerp.to({
from: {
position: object.position,
},
to: {
position,
},
duration,
}, cZoomEventTag);
}
// Zoom directly
else {
object.position = position;
}
}
break;
default:
break;
}
}
/**
* Stop current control.
*/
stop() {
let _private = this[__.private];
_private.controller.stop();
}
processAdjustNear() {
const object = this.object;
if (!object) {
return;
}
const minDistance = 10;
// Only check when it's far away from target position
let distance = object.distance;
if (distance <= minDistance && distance > object.near) {
return;
}
// Get the distance to target by picking
let distanceByPicking = this._getDistanceToTargetInPickingMode() || distance;
// Update near of render camera
const factor = 120;
object.near = MathUtils.max(0.1, distanceByPicking / factor);
}
pickFromCrossPlanes(x, y, isVertical = true) {
let results = this._pickFromCrossPlanes(x, y, isVertical);
if (!results.length) {
return null;
}
let object = this.object;
results.sort((p1, p2) => {
let d1 = object.distanceTo(p1);
let d2 = object.distanceTo(p2);
return d1 - d2;
});
return results[0];
}
/**
* Check whether is changing or not.
* @type {Boolean}
* @example
* let camera = THING.App.current.camera;
* camera.pan(0, -50);
* // @expect(camera.isChanging == true)
* @public
*/
get isChanging() {
let _private = this[__.private];
return _private.flags.has(Flag.Changing);
}
/**
* Get the state group to active/deactive controller.
* @type {Object}
* @private
*/
get stateGroup() {
return this[__.private].stateGroup;
}
/**
* Enable/Disable control.
* @type {Boolean}
* @example
* let camera = THING.App.current.camera;
* camera.enable = false;
* // @expect(camera.enable == false)
* @public
*/
get enable() {
// Many operations cloud disable camera control, so we just check 'default' state here
let result = this[__.private].stateGroup.getValue('default');
if (result === false) {
return false;
}
return true;
}
set enable(value) {
let _private = this[__.private];
if (this.enableUpdateTargetByChanging) {
this._updateTargetByChangeCameraEnable(value);
}
_private.stateGroup.enable(value, 'default', 1000);
this._clearPickerState();
}
_updateTargetByChangeCameraEnable(value) {
let _private = this[__.private];
if (value === false) {
if (!_private.changeCameraTransform) {
_private.changeCameraTransform = true;
}
}
else {
if (_private.changeCameraTransform) {
_private.changeCameraTransform = null;
const vec3 = MathUtils.scaleVector(this.object.forward, this.distance);
const target = MathUtils.addVector(this.object.position, vec3);
this.object.target = target;
}
}
}
/**
* enable update target by changing camera transform.
* @type {Boolean}
* @private
*/
get enableUpdateTargetByChanging() {
let _private = this[__.private];
return _private.flags.has(Flag.EnableUpdateTargetByChanging);
}
set enableUpdateTargetByChanging(value) {
let _private = this[__.private];
_private.flags.enable(Flag.EnableUpdateTargetByChanging, value);
}
/**
* Enable/Disable auto move target forward.
* @type {Boolean}
* @example
* let camera = THING.App.current.camera;
* camera.enableAutoMoveTargetForward = false;
* // @expect(camera.enableAutoMoveTargetForward == false)
* @public
*/
get enableAutoMoveTargetForward() {
let _private = this[__.private];
return _private.flags.has(Flag.AutoMoveTargetForward);
}
set enableAutoMoveTargetForward(value) {
let _private = this[__.private];
_private.flags.enable(Flag.AutoMoveTargetForward, value);
}
/**
* Enable/Disable adjust target position.
* @type {Boolean}
* @example
* let camera = THING.App.current.camera;
* camera.enableAdjustTargetPosition = false;
* // @expect(camera.enableAdjustTargetPosition == false)
* @public
*/
get enableAdjustTargetPosition() {
let _private = this[__.private];
return _private.flags.has(Flag.AutoAdjustTargetPosition);
}
set enableAdjustTargetPosition(value) {
let _private = this[__.private];
if (_private.flags.enable(Flag.AutoAdjustTargetPosition, value)) {
this._enableAdjustTargetPosition(value);
}
}
/**
* Enable/Disable adjust near.
* @type {Boolean}
* @example
* let camera = THING.App.current.camera;
* camera.enableAdjustNear = false;
* // @expect(camera.enableAdjustNear == false)
* @public
*/
get enableAdjustNear() {
let _private = this[__.private];
return _private.flags.has(Flag.AutoAdjustNear);
}
set enableAdjustNear(value) {
let _private = this[__.private];
if (_private.flags.enable(Flag.AutoAdjustNear, value)) {
if (value) {
let object = this.object;
_private.intervalTimers['adjustNear'] = new Timer({
interval: 0.2,
onInterval: () => {
if (object.isFlying || this.isChanging) {
// Here we should look at target first to prevent angles change before adjust near
let target = object.control.getTarget(_target);
object.node.lookAt(target, object.up);
// Here we also need to make vert plane to look at camera to make sure we can pick correct position from it
let position = object.getWorldPosition(_position);
_private.crossPlanes.vertPlane.lookAt(position);
this.processAdjustNear();
}
}
});
}
else {
delete _private.intervalTimers['adjustNear'];
}
}
}
/**
* Enable/Disable adjust pan speed.
* @type {Boolean}
* @example
* let camera = THING.App.current.camera;
* camera.enableAdjustPanSpeed = false;
* // @expect(camera.enableAdjustPanSpeed == false);
* @public
*/
get enableAdjustPanSpeed() {
let _private = this[__.private];
return _private.flags.has(Flag.AutoAdjustPanSpeed);
}
set enableAdjustPanSpeed(value) {
let _private = this[__.private];
if (_private.flags.enable(Flag.AutoAdjustPanSpeed, value)) {
if (value) {
let object = this.object;
_private.intervalTimers['adjustPanSpeed'] = new Timer({
interval: 0.5,
onInterval: () => {
if (object.isFlying || this.isChanging) {
this._processAdjustPanSpeed();
}
}
});
}
else {
delete _private.intervalTimers['adjustPanSpeed'];
}
}
}
/**
* The function to call when start to adjust pan speed in camera control.
* @callback onPanSpeedCallback
* @returns {Number} The pan speed value.
* @private
*/
/**
* The auto adjust callbacks of camera control.
* @typedef {Object} CameraControlComponentAdjustCallbacks
* @property {onPanSpeedCallback} onPanSpeed When auto adjust pan speed value.
* @private
*/
/**
* Get/Set adjust callbacks.
* @type {CameraControlComponentAdjustCallbacks}
* @example
* THING.App.current.camera.control.adjustCallbacks['onPanSpeed'] = function() {
* return THING.App.current.camera.distance / 100;
* }
* @example
* let camera = THING.App.current.camera;
* let func = function() {}
* camera.adjustCallbacks['onPanSpeed'] = func;
* // @expect(camera.adjustCallbacks['onPanSpeed'] == func);
* @private
*/
get adjustCallbacks() {
let _private = this[__.private];
return _private.adjustCallbacks;
}
set adjustCallbacks(value) {
let _private = this[__.private];
_private.adjustCallbacks = value;
}
/**
* Enable/Disable zoom to mouse cursor on wheel.
* @type {Boolean}
* @example
* let camera = THING.App.current.camera;
* camera.enableZoomToMouseCursorOnWheel = false;
* // @expect(camera.enableZoomToMouseCursorOnWheel == false);
* @public
*/
get enableZoomToMouseCursorOnWheel() {
let _private = this[__.private];
return _private.flags.has(Flag.ZoomToMouseCursorOnWheel);
}
set enableZoomToMouseCursorOnWheel(value) {
let _private = this[__.private];
if (_private.flags.enable(Flag.ZoomToMouseCursorOnWheel, value)) {
this._enableZoomToMouseCursorOnWheel(value);
}
}
/**
* Enable/Disable pick when changing.
* @type {Boolean}
* @example
* let camera = THING.App.current.camera;
* camera.enablePickWhenChanging = false;
* // @expect(camera.enablePickWhenChanging == false);
* @public
*/
get enablePickWhenChanging() {
let _private = this[__.private];
return _private.flags.has(Flag.PickWhenChanging);
}
set enablePickWhenChanging(value) {
let _private = this[__.private];
_private.flags.enable(Flag.PickWhenChanging, value);
if (value) {
this.object.app.picker.stateGroup.enable(true, cPickerGroupName, -cPickerPriority);
}
}
/**
* Enable/Disable damping.
* @type {Boolean}
* @example
* let camera = THING.App.current.camera;
* camera.enableDamping = false;
* // @expect(camera.enableDamping == false);
* @public
*/
get enableDamping() {
let _private = this[__.private];
return _private.flags.has(Flag.Damping);
}
set enableDamping(value) {
let _private = this[__.private];
if (_private.flags.enable(Flag.Damping, value)) {
this.getController().enable('Damping', value);
}
}
/**
* Enable/Disable rotate.
* @type {Boolean}
* @example
* let camera = THING.App.current.camera;
* camera.enableRotate = false;
* // @expect(camera.enableRotate == false);
* @public
*/
get enableRotate() {
let _private = this[__.private];
return _private.flags.has(Flag.Rotate);
}
set enableRotate(value) {
let _private = this[__.private];
if (_private.flags.enable(Flag.Rotate, value)) {
this.getController().enable('Rotate', value);
}
}
/**
* Enable/Disable pan.
* @type {Boolean}
* @example
* let camera = THING.App.current.camera;
* camera.enablePan = false;
* // @expect(camera.enablePan == false);
* @public
*/
get enablePan() {
let _private = this[__.private];
return _private.flags.has(Flag.Pan);
}
set enablePan(value) {
let _private = this[__.private];
if (_private.flags.enable(Flag.Pan, value)) {
this.getController().enable('Pan', value);
}
}
/**
* Enable/Disable zoom.
* @type {Boolean}
* @example
* let camera = THING.App.current.camera;
* camera.enableZoom = false;
* // @expect(camera.enableZoom == false);
* @public
*/
get enableZoom() {
let _private = this[__.private];
return _private.flags.has(Flag.Zoom);
}
set enableZoom(value) {
let _private = this[__.private];
if (_private.flags.enable(Flag.Zoom, value)) {
this.getController().enable('Zoom', value);
}
}
/**
* Enable/Disable map control.
* @type {Boolean}
* @example
* let camera = THING.App.current.camera;
* camera.enableMapControl = false;
* // @expect(camera.enableMapControl == false);
* @public
*/
get enableMapControl() {
let _private = this[__.private];
return _private.flags.has(Flag.MapControl);
}
set enableMapControl(value) {
let _private = this[__.private];
if (_private.flags.enable(Flag.MapControl, value)) {
this.getController().enable('MapControl', value);
}
}
/**
* Get/Set damping factor.
* @type {Number}
* @example
* let camera = THING.App.current.camera;
* camera.dampingFactor = 10;
* // @expect(camera.dampingFactor == 10);
* @public
*/
get dampingFactor() {
return this.getController().getDampingFactor();
}
set dampingFactor(value) {
this.getController().setDampingFactor(value);
}
/**
* Get/Set zoom speed.
* @type {Number}
* @example
* let camera = THING.App.current.camera;
* camera.zoomSpeed = 10;
* // @expect(camera.zoomSpeed == 10);
* @public
*/
get zoomSpeed() {
return this.getController().getZoomSpeed();
}
set zoomSpeed(value) {
this.getController().setZoomSpeed(value);
}
/**
* Get/Set rotate speed.
* @type {Number}
* @example
* let camera = THING.App.current.camera;
* camera.rotateSpeed = 10;
* // @expect(camera.rotateSpeed == 10);
* @public
*/
get rotateSpeed() {
return this.getController().getRotateSpeed();
}
set rotateSpeed(value) {
this.getController().setRotateSpeed(value);
}
/**
* Get/Set pan speed.
* @type {Number}
* @example
* let camera = THING.App.current.camera;
* camera.panSpeed = 10;
* // @expect(camera.panSpeed == 10);
* @public
*/
get panSpeed() {
return this.getController().getPanSpeed();
}
set panSpeed(value) {
this.getController().setPanSpeed(value);
}
/**
* Get/Set key pan speed.
* @type {Number}
* @example
* let camera = THING.App.current.camera;
* camera.keyPanSpeed = 10;
* // @expect(camera.keyPanSpeed == 10);
* @public
*/
get keyPanSpeed() {
return this.getController().getKeyPanSpeed();
}
set keyPanSpeed(value) {
this.getController().setKeyPanSpeed(value);
}
/**
* Get/Set the factor of zoom to mouse cursor on wheel.
* @type {Number}
* @example
* let camera = THING.App.current.camera;
* camera.zoomToMouseCursorOnWheelSpeed = 10;
* // @expect(camera.zoomToMouseCursorOnWheelSpeed == 10);
* @public
*/
get zoomToMouseCursorOnWheelSpeed() {
let _private = this[__.private];
return _private.zoomToMouseCursorOnWheelSpeed;
}
set zoomToMouseCursorOnWheelSpeed(value) {
let _private = this[__.private];
_private.zoomToMouseCursorOnWheelSpeed = value;
}
/**
* Set/Get distance limited range[min, max], null indicates it's unlimited.
* @type {Array<Number|null>}
* @example
* let camera = THING.App.current.camera;
* camera.distanceLimited = [10,1000];
* // @expect(camera.distanceLimited[0] == 10 && camera.distanceLimited[1] == 1000);
* @public
*/
get distanceLimited() {
let _private = this[__.private];
return _private.distanceLimited;
}
set distanceLimited(value) {
let _private = this[__.private];
let controller = _private.controller;
if (value) {
_private.distanceLimited = value.slice(0);
controller.setMinDistance(Utils.parseNumber(value[0], 0.00001));
controller.setMaxDistance(Utils.parseNumber(value[1], Infinity));
}
else {
_private.distanceLimited = null;
controller.setMinDistance(0.00001);
controller.setMaxDistance(Infinity);
}
}
/**
* Set/Get vert angle limited range[min, max], default is [0, 180].
* @type {Array<Number>}
* @example
* let camera = THING.App.current.camera;
* camera.vertAngleLimit = [10,100];
* // @expect(camera.vertAngleLimit[0] == 10 && camera.vertAngleLimit[1] == 100);
* @public
*/
get vertAngleLimit() {
let _private = this[__.private];
let controller = _private.controller;
let minValue = MathUtils.radToDeg(controller.getMinPolarAngle());
let maxValue = MathUtils.radToDeg(controller.getMaxPolarAngle());
return [Math.min(minValue, maxValue), Math.max(minValue, maxValue)];
}
set vertAngleLimit(value) {
let _private = this[__.private];
let minValue = value[0];
let maxValue = value[1];
let controller = _private.controller;
controller.setMinPolarAngle(MathUtils.degToRad(Math.min(minValue, maxValue)));
controller.setMaxPolarAngle(MathUtils.degToRad(Math.max(minValue, maxValue)));
}
/**
* Set/Get horz angle limited range[min, max], default is [0, 180].
* @type {Array<Number>}
* @example
* let camera = THING.App.current.camera;
* camera.horzAngleLimit = [10,100];
* // @expect(camera.horzAngleLimit[0] == 10 && camera.horzAngleLimit[1] == 100);
* @public
*/
get horzAngleLimit() {
let _private = this[__.private];
let controller = _private.controller;
let minValue = MathUtils.radToDeg(controller.getMinAzimuthAngle());
let maxValue = MathUtils.radToDeg(controller.getMaxAzimuthAngle());
return [Math.min(minValue, maxValue), Math.max(minValue, maxValue)];
}
set horzAngleLimit(value) {
let _private = this[__.private];
let minValue = value[0];
let maxValue = value[1];
let controller = _private.controller;
controller.setMinAzimuthAngle(MathUtils.degToRad(Math.min(minValue, maxValue)));
controller.setMaxAzimuthAngle(MathUtils.degToRad(Math.max(minValue, maxValue)));
}
/**
* @typedef {Object} BoundaryResult
* @property {Array<Number>} center The center position.
* @property {Array<Number>} halfSize The half size.
*/
/**
* Get/Set boundary to limit target position(null indicates clear it).
* @type {BoundaryResult}
* @example
* let camera = THING.App.current.camera;
* let center = [10,10,10];
* let halfSize = [20,20,20];
* let boundary = {center, halfSize};
* camera.boundary = boundary;
* // @expect(camera.boundary.center[0] == 10 && camera.boundary.halfSize[0] == 20);
* @public
*/
get boundary() {
let controller = this.getController();
if (!controller) {
return null;
}
let target = {
center: [0, 0, 0],
halfSize: [0, 0, 0]
};
if (!controller.getBoundary(target)) {
return null;
}
return target;
}
set boundary(value) {
let _private = this[__.private];
let controller = this.getController();
if (!controller) {
return;
}
if (value) {
let halfSize = value['halfSize'];
if (!halfSize) {
Utils.error(`Set camera control boundary failed, due to halfSize is invalid`);
return;
}
let center = value['center'] || this.getTarget();
controller.setBoundary({
center,
halfSize
});
_private.boundary = {
min: MathUtils.subVector(center, halfSize),
max: MathUtils.addVector(center, halfSize)
};
}
else {
_private.boundary = null;
controller.setBoundary(null);
}
}
/**
* Get/Set up direction.
* @type {Array<Number>}
* @example
* let camera = THING.App.current.camera;
* camera.upDirection = [10,10,10];
* // @expect(camera.upDirection[0] == 10 && camera.upDirection[1] == 10 && camera.upDirection[2] == 10);
* @public
*/
get upDirection() {
let controller = this.getController();
if (!controller) {
return null;
}
let target = [];
return controller.getUpDirection(target);
}
set upDirection(value) {
let controller = this.getController();
if (!controller) {
return;
}
controller.setUpDirection(value);
}
/**
* Get/Set target of the world space.
* @type {Array<Number>}
* @example
* let camera = THING.App.current.camera;
* camera.target = [10,10,10];
* // @expect(camera.target[0] == 10 && camera.target[1] == 10 && camera.target[2] == 10);
* @public
*/
get target() {
return this.getTarget();
}
set target(value) {
this.setTarget(value);
}
/**
* Get the distance from position to target.
* @type {Number}
*/
get distance() {
this.object.getWorldPosition(_position);
this.getTarget(_target);
return MathUtils.vec3.distance(_position, _target);
}
/**
* Get/Set space panning of the screen.
* True indicates pan action will base on screen, otherwise indicates will base on camera's Y axis direction.
* Default value is true.
* @type {Boolean}
* @example
* let camera = THING.App.current.camera;
* camera.screenSpacePanning = false;
* // @expect(camera.screenSpacePanning == false);
* @public
*/
get screenSpacePanning() {
return this.getController().getScreenSpacePanning();
}
set screenSpacePanning(value) {
this.getController().setScreenSpacePanning(value);
}
}
CameraControlComponent.exportProperties = [
'enable',
'enableAutoMoveTargetForward',
'enableAdjustTargetPosition',
'enableAdjustNear',
'enableAdjustPanSpeed',
'enableZoomToMouseCursorOnWheel',
'enableDamping',
'enableRotate',
'enablePan',
'enableZoom',
'enableMapControl',
'dampingFactor',
'zoomSpeed',
'rotateSpeed',
'panSpeed',
'keyPanSpeed',
'zoomToMouseCursorOnWheelSpeed',
'distanceLimited',
'vertAngleLimit',
'horzAngleLimit',
'boundary',
'target',
'distance',
'screenSpacePanning',
'enableUpdateTargetByChanging',
];
CameraControlComponent.exportFunctions = [
'pan',
'zoom'
];
export { CameraControlComponent }