Source: framework/components/element/element-drag-helper.js

Object.assign(pc, function () {
    var _inputScreenPosition = new pc.Vec2();
    var _inputWorldPosition = new pc.Vec3();
    var _rayOrigin = new pc.Vec3();
    var _rayDirection = new pc.Vec3();
    var _planeOrigin = new pc.Vec3();
    var _planeNormal = new pc.Vec3();
    var _entityRotation = new pc.Quat();

    var OPPOSITE_AXIS = {
        x: 'y',
        y: 'x'
    };

    /**
     * @component
     * @name pc.ElementDragHelper
     * @description Create a new ElementDragHelper
     * @classdesc Helper class that makes it easy to create Elements that can be dragged by the mouse or touch.
     * @param {pc.ElementComponent} element The Element that should become draggable.
     * @param {String} [axis] Optional axis to constrain to, either 'x', 'y' or null.
     */
    var ElementDragHelper = function ElementDragHelper(element, axis) {
        if (!element || !(element instanceof pc.ElementComponent)) {
            throw new Error('Element was null or not an ElementComponent');
        }

        if (axis && axis !== 'x' && axis !== 'y') {
            throw new Error('Unrecognized axis: ' + axis);
        }

        this._element = element;
        this._app = element.system.app;
        this._axis = axis || null;
        this._enabled = true;
        this._dragScale = new pc.Vec3();
        this._dragStartMousePosition = new pc.Vec3();
        this._dragStartHandlePosition = new pc.Vec3();
        this._deltaMousePosition = new pc.Vec3();
        this._deltaHandlePosition = new pc.Vec3();
        this._isDragging = false;

        pc.events.attach(this);

        this._toggleLifecycleListeners('on');
    };

    Object.assign(ElementDragHelper.prototype, {
        _toggleLifecycleListeners: function (onOrOff) {
            this._element[onOrOff]('mousedown', this._onMouseDownOrTouchStart, this);
            this._element[onOrOff]('touchstart', this._onMouseDownOrTouchStart, this);
        },

        _toggleDragListeners: function (onOrOff) {
            var isOn = onOrOff === 'on';
            var addOrRemoveEventListener = isOn ? 'addEventListener' : 'removeEventListener';

            // Prevent multiple listeners
            if (this._hasDragListeners && isOn) {
                return;
            }

            if (!this._handleMouseUpOrTouchEnd) {
                this._handleMouseUpOrTouchEnd = this._onMouseUpOrTouchEnd.bind(this);
            }

            // Note that we handle release events directly on the window object, rather than
            // on app.mouse or app.touch. This is in order to correctly handle cases where the
            // user releases the mouse/touch outside of the window.
            this._app.mouse[onOrOff]('mousemove', this._onMove, this);
            window[addOrRemoveEventListener]('mouseup', this._handleMouseUpOrTouchEnd, false);

            if (pc.platform.touch) {
                this._app.touch[onOrOff]('touchmove', this._onMove, this);
                window[addOrRemoveEventListener]('touchend', this._handleMouseUpOrTouchEnd, false);
                window[addOrRemoveEventListener]('touchcancel', this._handleMouseUpOrTouchEnd, false);
            }

            this._hasDragListeners = isOn;
        },

        _onMouseDownOrTouchStart: function (event) {
            if (this._element && !this._isDragging && this.enabled) {
                this._dragCamera = event.camera;
                this._calculateDragScale();

                var currentMousePosition = this._screenToLocal(event);

                if (currentMousePosition) {
                    this._toggleDragListeners('on');
                    this._isDragging = true;
                    this._dragStartMousePosition.copy(currentMousePosition);
                    this._dragStartHandlePosition.copy(this._element.entity.getLocalPosition());

                    this.fire('drag:start');
                }
            }
        },

        _onMouseUpOrTouchEnd: function () {
            if (this._isDragging) {
                this._isDragging = false;
                this._toggleDragListeners('off');

                this.fire('drag:end');
            }
        },

        _screenToLocal: function (event) {
            this._determineInputPosition(event);
            this._chooseRayOriginAndDirection();

            _planeOrigin.copy(this._element.entity.getPosition());
            _planeNormal.copy(this._element.entity.forward).scale(-1);

            var denominator = _planeNormal.dot(_rayDirection);

            // If the ray and plane are not parallel
            if (Math.abs(denominator) > 0) {
                var rayOriginToPlaneOrigin = _planeOrigin.sub(_rayOrigin);
                var collisionDistance = rayOriginToPlaneOrigin.dot(_planeNormal) / denominator;
                var position = _rayOrigin.add(_rayDirection.scale(collisionDistance));

                _entityRotation.copy(this._element.entity.getRotation()).invert().transformVector(position, position);

                position.mul(this._dragScale);

                return position;
            }

            return null;
        },

        _determineInputPosition: function (event) {
            var devicePixelRatio = this._app.graphicsDevice.maxPixelRatio;

            if (typeof event.x !== 'undefined' && typeof event.y !== 'undefined') {
                _inputScreenPosition.x = event.x * devicePixelRatio;
                _inputScreenPosition.y = event.y * devicePixelRatio;
            } else if (event.changedTouches) {
                _inputScreenPosition.x = event.changedTouches[0].x * devicePixelRatio;
                _inputScreenPosition.y = event.changedTouches[0].y * devicePixelRatio;
            } else {
                console.warn('Could not determine position from input event');
            }
        },

        _chooseRayOriginAndDirection: function () {
            if (this._element.screen && this._element.screen.screen.screenSpace) {
                _rayOrigin.set(_inputScreenPosition.x, -_inputScreenPosition.y, 0);
                _rayDirection.set(0, 0, -1);
            } else {
                _inputWorldPosition.copy(this._dragCamera.screenToWorld(_inputScreenPosition.x, _inputScreenPosition.y, 1));
                _rayOrigin.copy(this._dragCamera.entity.getPosition());
                _rayDirection.copy(_inputWorldPosition).sub(_rayOrigin).normalize();
            }
        },

        _calculateDragScale: function () {
            var current = this._element.entity.parent;
            var screen = this._element.screen && this._element.screen.screen;
            var isWithin2DScreen = screen && screen.screenSpace;
            var screenScale = isWithin2DScreen ? screen.scale : 1;
            var dragScale = this._dragScale;

            dragScale.set(screenScale, screenScale, screenScale);

            while (current) {
                dragScale.mul(current.getLocalScale());
                current = current.parent;

                if (isWithin2DScreen && current.screen) {
                    break;
                }
            }

            dragScale.x = 1 / dragScale.x;
            dragScale.y = 1 / dragScale.y;
            dragScale.z = 1 / dragScale.z;
        },

        _onMove: function (event) {
            if (this._element && this._isDragging && this.enabled && this._element.enabled && this._element.entity.enabled) {
                var currentMousePosition = this._screenToLocal(event);

                if (this._dragStartMousePosition && currentMousePosition) {
                    this._deltaMousePosition.copy(currentMousePosition).sub(this._dragStartMousePosition);
                    this._deltaHandlePosition.copy(this._dragStartHandlePosition).add(this._deltaMousePosition);

                    if (this._axis) {
                        var currentPosition = this._element.entity.getLocalPosition();
                        var constrainedAxis = OPPOSITE_AXIS[this._axis];
                        this._deltaHandlePosition[constrainedAxis] = currentPosition[constrainedAxis];
                    }

                    this._element.entity.setLocalPosition(this._deltaHandlePosition);
                    this.fire('drag:move', this._deltaHandlePosition);
                }
            }
        },

        destroy: function () {
            this._toggleLifecycleListeners('off');
            this._toggleDragListeners('off');
        }
    });

    Object.defineProperty(ElementDragHelper.prototype, 'enabled', {
        get: function () {
            return this._enabled;
        },

        set: function (value) {
            this._enabled = value;
        }
    });

    Object.defineProperty(ElementDragHelper.prototype, 'isDragging', {
        get: function () {
            return this._isDragging;
        }
    });

    return {
        ElementDragHelper: ElementDragHelper
    };
}());

/**
 * @event
 * @name pc.ElementDragHelper#drag:start
 * @description Fired when a new drag operation starts.
 */

/**
 * @event
 * @name pc.ElementDragHelper#drag:end
 * @description Fired when the current new drag operation ends.
 */

/**
 * @event
 * @name pc.ElementDragHelper#drag:move
 * @description Fired whenever the position of the dragged element changes.
 * @param {pc.Vec3} value The current position.
 */