﻿/**
 * @author arodic / https://github.com/arodic
 */

(function () {

    'use strict';

    var GizmoMaterial = function (parameters) {

        THREE.MeshBasicMaterial.call(this);

        this.depthTest = false;
        this.depthWrite = false;
        this.fog = false;
        this.side = THREE.FrontSide;
        this.transparent = true;

        this.setValues(parameters);

        this.oldColor = this.color.clone();
        this.oldOpacity = this.opacity;

        this.highlight = function (highlighted) {

            if (highlighted) {

                this.color.setRGB(1, 1, 0);
                this.opacity = 1;

            } else {

                this.color.copy(this.oldColor);
                this.opacity = this.oldOpacity;

            }

        };

    };

    GizmoMaterial.prototype = Object.create(THREE.MeshBasicMaterial.prototype);
    GizmoMaterial.prototype.constructor = GizmoMaterial;


    var GizmoLineMaterial = function (parameters) {

        THREE.LineBasicMaterial.call(this);

        this.depthTest = false;
        this.depthWrite = false;
        this.fog = false;
        this.transparent = true;
        this.linewidth = 1;

        this.setValues(parameters);

        this.oldColor = this.color.clone();
        this.oldOpacity = this.opacity;

        this.highlight = function (highlighted) {

            if (highlighted) {

                this.color.setRGB(1, 1, 0);
                this.opacity = 1;

            } else {

                this.color.copy(this.oldColor);
                this.opacity = this.oldOpacity;

            }

        };

    };

    GizmoLineMaterial.prototype = Object.create(THREE.LineBasicMaterial.prototype);
    GizmoLineMaterial.prototype.constructor = GizmoLineMaterial;


    var pickerMaterial = new GizmoMaterial({ visible: false, transparent: false });


    THREE.TransformGizmo = function () {

        this.init = function () {

            THREE.Object3D.call(this);

            this.handles = new THREE.Object3D();
            this.pickers = new THREE.Object3D();
            this.planes = new THREE.Object3D();

            this.add(this.handles);
            this.add(this.pickers);
            this.add(this.planes);

            //// PLANES

            var planeGeometry = new THREE.PlaneBufferGeometry(50, 50, 2, 2);
            var planeMaterial = new THREE.MeshBasicMaterial({ visible: false, side: THREE.DoubleSide });

            var planes = {
                "XY": new THREE.Mesh(planeGeometry, planeMaterial),
                "YZ": new THREE.Mesh(planeGeometry, planeMaterial),
                "XZ": new THREE.Mesh(planeGeometry, planeMaterial),
                "XYZE": new THREE.Mesh(planeGeometry, planeMaterial)
            };

            this.activePlane = planes["XYZE"];

            planes["YZ"].rotation.set(0, Math.PI / 2, 0);
            planes["XZ"].rotation.set(-Math.PI / 2, 0, 0);

            for (var i in planes) {

                planes[i].name = i;
                this.planes.add(planes[i]);
                this.planes[i] = planes[i];

            }

            //// HANDLES AND PICKERS

            var setupGizmos = function (gizmoMap, parent) {

                for (var name in gizmoMap) {

                    for (i = gizmoMap[name].length; i--;) {

                        var object = gizmoMap[name][i][0];
                        var position = gizmoMap[name][i][1];
                        var rotation = gizmoMap[name][i][2];

                        object.name = name;

                        object.renderOrder = Infinity; // avoid being hidden by other transparent objects

                        if (position) object.position.set(position[0], position[1], position[2]);
                        if (rotation) object.rotation.set(rotation[0], rotation[1], rotation[2]);

                        parent.add(object);

                    }

                }

            };

            setupGizmos(this.handleGizmos, this.handles);
            setupGizmos(this.pickerGizmos, this.pickers);

            // reset Transformations

            this.traverse(function (child) {

                if (child instanceof THREE.Mesh) {

                    child.updateMatrix();

                    var tempGeometry = child.geometry.clone();
                    tempGeometry.applyMatrix(child.matrix);
                    child.geometry = tempGeometry;

                    child.position.set(0, 0, 0);
                    child.rotation.set(0, 0, 0);
                    child.scale.set(1, 1, 1);

                }

            });

        };

        this.highlight = function (axis) {

            this.traverse(function (child) {

                if (child.material && child.material.highlight) {

                    if (child.name === axis) {

                        child.material.highlight(true);

                    } else {

                        child.material.highlight(false);

                    }

                }

            });

        };

    };

    THREE.TransformGizmo.prototype = Object.create(THREE.Object3D.prototype);
    THREE.TransformGizmo.prototype.constructor = THREE.TransformGizmo;

    THREE.TransformGizmo.prototype.update = function (rotation, eye) {

        var vec1 = new THREE.Vector3(0, 0, 0);
        var vec2 = new THREE.Vector3(0, 1, 0);
        var lookAtMatrix = new THREE.Matrix4();

        this.traverse(function (child) {

            if (child.name.search("E") !== -1) {

                child.quaternion.setFromRotationMatrix(lookAtMatrix.lookAt(eye, vec1, vec2));

            } else if (child.name.search("X") !== -1 || child.name.search("Y") !== -1 || child.name.search("Z") !== -1) {

                child.quaternion.setFromEuler(rotation);

            }

        });

    };

    THREE.TransformGizmoTranslate = function () {

        THREE.TransformGizmo.call(this);

        var arrowGeometry = new THREE.ConeBufferGeometry(0.05, 0.2, 12, 1, false);
        arrowGeometry.translate(0, 0.5, 0);

        var lineXGeometry = new THREE.BufferGeometry();
        lineXGeometry.addAttribute('position', new THREE.Float32BufferAttribute([0, 0, 0, 1, 0, 0], 3));

        var lineYGeometry = new THREE.BufferGeometry();
        lineYGeometry.addAttribute('position', new THREE.Float32BufferAttribute([0, 0, 0, 0, 1, 0], 3));

        var lineZGeometry = new THREE.BufferGeometry();
        lineZGeometry.addAttribute('position', new THREE.Float32BufferAttribute([0, 0, 0, 0, 0, 1], 3));

        this.handleGizmos = {

            X: [
				[new THREE.Mesh(arrowGeometry, new GizmoMaterial({ color: 0xff0000 })), [0.5, 0, 0], [0, 0, -Math.PI / 2]],
				[new THREE.Line(lineXGeometry, new GizmoLineMaterial({ color: 0xff0000 }))]
            ],

            Y: [
				[new THREE.Mesh(arrowGeometry, new GizmoMaterial({ color: 0x00ff00 })), [0, 0.5, 0]],
				[new THREE.Line(lineYGeometry, new GizmoLineMaterial({ color: 0x00ff00 }))]
            ],

            Z: [
				[new THREE.Mesh(arrowGeometry, new GizmoMaterial({ color: 0x0000ff })), [0, 0, 0.5], [Math.PI / 2, 0, 0]],
				[new THREE.Line(lineZGeometry, new GizmoLineMaterial({ color: 0x0000ff }))]
            ],

            XYZ: [
				[new THREE.Mesh(new THREE.OctahedronGeometry(0.1, 0), new GizmoMaterial({ color: 0xffffff, opacity: 0.25 })), [0, 0, 0], [0, 0, 0]]
            ],

            XY: [
				[new THREE.Mesh(new THREE.PlaneBufferGeometry(0.29, 0.29), new GizmoMaterial({ color: 0xffff00, opacity: 0.25 })), [0.15, 0.15, 0]]
            ],

            YZ: [
				[new THREE.Mesh(new THREE.PlaneBufferGeometry(0.29, 0.29), new GizmoMaterial({ color: 0x00ffff, opacity: 0.25 })), [0, 0.15, 0.15], [0, Math.PI / 2, 0]]
            ],

            XZ: [
				[new THREE.Mesh(new THREE.PlaneBufferGeometry(0.29, 0.29), new GizmoMaterial({ color: 0xff00ff, opacity: 0.25 })), [0.15, 0, 0.15], [-Math.PI / 2, 0, 0]]
            ]

        };

        this.pickerGizmos = {

            X: [
				[new THREE.Mesh(new THREE.CylinderBufferGeometry(0.2, 0, 1, 4, 1, false), pickerMaterial), [0.6, 0, 0], [0, 0, -Math.PI / 2]]
            ],

            Y: [
				[new THREE.Mesh(new THREE.CylinderBufferGeometry(0.2, 0, 1, 4, 1, false), pickerMaterial), [0, 0.6, 0]]
            ],

            Z: [
				[new THREE.Mesh(new THREE.CylinderBufferGeometry(0.2, 0, 1, 4, 1, false), pickerMaterial), [0, 0, 0.6], [Math.PI / 2, 0, 0]]
            ],

            XYZ: [
				[new THREE.Mesh(new THREE.OctahedronGeometry(0.2, 0), pickerMaterial)]
            ],

            XY: [
				[new THREE.Mesh(new THREE.PlaneBufferGeometry(0.4, 0.4), pickerMaterial), [0.2, 0.2, 0]]
            ],

            YZ: [
				[new THREE.Mesh(new THREE.PlaneBufferGeometry(0.4, 0.4), pickerMaterial), [0, 0.2, 0.2], [0, Math.PI / 2, 0]]
            ],

            XZ: [
				[new THREE.Mesh(new THREE.PlaneBufferGeometry(0.4, 0.4), pickerMaterial), [0.2, 0, 0.2], [-Math.PI / 2, 0, 0]]
            ]

        };

        this.setActivePlane = function (axis, eye) {

            var tempMatrix = new THREE.Matrix4();
            eye.applyMatrix4(tempMatrix.getInverse(tempMatrix.extractRotation(this.planes["XY"].matrixWorld)));

            if (axis === "X") {

                this.activePlane = this.planes["XY"];

                if (Math.abs(eye.y) > Math.abs(eye.z)) this.activePlane = this.planes["XZ"];

            }

            if (axis === "Y") {

                this.activePlane = this.planes["XY"];

                if (Math.abs(eye.x) > Math.abs(eye.z)) this.activePlane = this.planes["YZ"];

            }

            if (axis === "Z") {

                this.activePlane = this.planes["XZ"];

                if (Math.abs(eye.x) > Math.abs(eye.y)) this.activePlane = this.planes["YZ"];

            }

            if (axis === "XYZ") this.activePlane = this.planes["XYZE"];

            if (axis === "XY") this.activePlane = this.planes["XY"];

            if (axis === "YZ") this.activePlane = this.planes["YZ"];

            if (axis === "XZ") this.activePlane = this.planes["XZ"];

        };

        this.init();

    };

    THREE.TransformGizmoTranslate.prototype = Object.create(THREE.TransformGizmo.prototype);
    THREE.TransformGizmoTranslate.prototype.constructor = THREE.TransformGizmoTranslate;

    THREE.TransformGizmoRotate = function () {

        THREE.TransformGizmo.call(this);

        var CircleGeometry = function (radius, facing, arc) {

            var geometry = new THREE.BufferGeometry();
            var vertices = [];
            arc = arc ? arc : 1;

            for (var i = 0; i <= 64 * arc; ++i) {

                if (facing === 'x') vertices.push(0, Math.cos(i / 32 * Math.PI) * radius, Math.sin(i / 32 * Math.PI) * radius);
                if (facing === 'y') vertices.push(Math.cos(i / 32 * Math.PI) * radius, 0, Math.sin(i / 32 * Math.PI) * radius);
                if (facing === 'z') vertices.push(Math.sin(i / 32 * Math.PI) * radius, Math.cos(i / 32 * Math.PI) * radius, 0);

            }

            geometry.addAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
            return geometry;

        };

        this.handleGizmos = {

            X: [
				[new THREE.Line(new CircleGeometry(1, 'x', 0.5), new GizmoLineMaterial({ color: 0xff0000 }))]
            ],

            Y: [
				[new THREE.Line(new CircleGeometry(1, 'y', 0.5), new GizmoLineMaterial({ color: 0x00ff00 }))]
            ],

            Z: [
				[new THREE.Line(new CircleGeometry(1, 'z', 0.5), new GizmoLineMaterial({ color: 0x0000ff }))]
            ],

            E: [
				[new THREE.Line(new CircleGeometry(1.25, 'z', 1), new GizmoLineMaterial({ color: 0xcccc00 }))]
            ],

            XYZE: [
				[new THREE.Line(new CircleGeometry(1, 'z', 1), new GizmoLineMaterial({ color: 0x787878 }))]
            ]

        };

        this.pickerGizmos = {

            X: [
				[new THREE.Mesh(new THREE.TorusBufferGeometry(1, 0.12, 4, 12, Math.PI), pickerMaterial), [0, 0, 0], [0, -Math.PI / 2, -Math.PI / 2]]
            ],

            Y: [
				[new THREE.Mesh(new THREE.TorusBufferGeometry(1, 0.12, 4, 12, Math.PI), pickerMaterial), [0, 0, 0], [Math.PI / 2, 0, 0]]
            ],

            Z: [
				[new THREE.Mesh(new THREE.TorusBufferGeometry(1, 0.12, 4, 12, Math.PI), pickerMaterial), [0, 0, 0], [0, 0, -Math.PI / 2]]
            ],

            E: [
				[new THREE.Mesh(new THREE.TorusBufferGeometry(1.25, 0.12, 2, 24), pickerMaterial)]
            ],

            XYZE: [
				[new THREE.Mesh(new THREE.TorusBufferGeometry(1, 0.12, 2, 24), pickerMaterial)]
            ]

        };

        this.pickerGizmos.XYZE[0][0].visible = false; // disable XYZE picker gizmo

        this.setActivePlane = function (axis) {

            if (axis === "E") this.activePlane = this.planes["XYZE"];

            if (axis === "X") this.activePlane = this.planes["YZ"];

            if (axis === "Y") this.activePlane = this.planes["XZ"];

            if (axis === "Z") this.activePlane = this.planes["XY"];

        };

        this.update = function (rotation, eye2) {

            THREE.TransformGizmo.prototype.update.apply(this, arguments);

            var tempMatrix = new THREE.Matrix4();
            var worldRotation = new THREE.Euler(0, 0, 1);
            var tempQuaternion = new THREE.Quaternion();
            var unitX = new THREE.Vector3(1, 0, 0);
            var unitY = new THREE.Vector3(0, 1, 0);
            var unitZ = new THREE.Vector3(0, 0, 1);
            var quaternionX = new THREE.Quaternion();
            var quaternionY = new THREE.Quaternion();
            var quaternionZ = new THREE.Quaternion();
            var eye = eye2.clone();

            worldRotation.copy(this.planes["XY"].rotation);
            tempQuaternion.setFromEuler(worldRotation);

            tempMatrix.makeRotationFromQuaternion(tempQuaternion).getInverse(tempMatrix);
            eye.applyMatrix4(tempMatrix);

            this.traverse(function (child) {

                tempQuaternion.setFromEuler(worldRotation);

                if (child.name === "X") {

                    quaternionX.setFromAxisAngle(unitX, Math.atan2(-eye.y, eye.z));
                    tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionX);
                    child.quaternion.copy(tempQuaternion);

                }

                if (child.name === "Y") {

                    quaternionY.setFromAxisAngle(unitY, Math.atan2(eye.x, eye.z));
                    tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionY);
                    child.quaternion.copy(tempQuaternion);

                }

                if (child.name === "Z") {

                    quaternionZ.setFromAxisAngle(unitZ, Math.atan2(eye.y, eye.x));
                    tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionZ);
                    child.quaternion.copy(tempQuaternion);

                }

            });

        };

        this.init();

    };

    THREE.TransformGizmoRotate.prototype = Object.create(THREE.TransformGizmo.prototype);
    THREE.TransformGizmoRotate.prototype.constructor = THREE.TransformGizmoRotate;

    THREE.TransformGizmoScale = function () {

        THREE.TransformGizmo.call(this);

        var arrowGeometry = new THREE.BoxBufferGeometry(0.125, 0.125, 0.125);
        arrowGeometry.translate(0, 0.5, 0);

        var lineXGeometry = new THREE.BufferGeometry();
        lineXGeometry.addAttribute('position', new THREE.Float32BufferAttribute([0, 0, 0, 1, 0, 0], 3));

        var lineYGeometry = new THREE.BufferGeometry();
        lineYGeometry.addAttribute('position', new THREE.Float32BufferAttribute([0, 0, 0, 0, 1, 0], 3));

        var lineZGeometry = new THREE.BufferGeometry();
        lineZGeometry.addAttribute('position', new THREE.Float32BufferAttribute([0, 0, 0, 0, 0, 1], 3));

        this.handleGizmos = {

            X: [
				[new THREE.Mesh(arrowGeometry, new GizmoMaterial({ color: 0xff0000 })), [0.5, 0, 0], [0, 0, -Math.PI / 2]],
				[new THREE.Line(lineXGeometry, new GizmoLineMaterial({ color: 0xff0000 }))]
            ],

            Y: [
				[new THREE.Mesh(arrowGeometry, new GizmoMaterial({ color: 0x00ff00 })), [0, 0.5, 0]],
				[new THREE.Line(lineYGeometry, new GizmoLineMaterial({ color: 0x00ff00 }))]
            ],

            Z: [
				[new THREE.Mesh(arrowGeometry, new GizmoMaterial({ color: 0x0000ff })), [0, 0, 0.5], [Math.PI / 2, 0, 0]],
				[new THREE.Line(lineZGeometry, new GizmoLineMaterial({ color: 0x0000ff }))]
            ],

            XYZ: [
				[new THREE.Mesh(new THREE.BoxBufferGeometry(0.125, 0.125, 0.125), new GizmoMaterial({ color: 0xffffff, opacity: 0.25 }))]
            ]

        };

        this.pickerGizmos = {

            X: [
				[new THREE.Mesh(new THREE.CylinderBufferGeometry(0.2, 0, 1, 4, 1, false), pickerMaterial), [0.6, 0, 0], [0, 0, -Math.PI / 2]]
            ],

            Y: [
				[new THREE.Mesh(new THREE.CylinderBufferGeometry(0.2, 0, 1, 4, 1, false), pickerMaterial), [0, 0.6, 0]]
            ],

            Z: [
				[new THREE.Mesh(new THREE.CylinderBufferGeometry(0.2, 0, 1, 4, 1, false), pickerMaterial), [0, 0, 0.6], [Math.PI / 2, 0, 0]]
            ],

            XYZ: [
				[new THREE.Mesh(new THREE.BoxBufferGeometry(0.4, 0.4, 0.4), pickerMaterial)]
            ]

        };

        this.setActivePlane = function (axis, eye) {

            var tempMatrix = new THREE.Matrix4();
            eye.applyMatrix4(tempMatrix.getInverse(tempMatrix.extractRotation(this.planes["XY"].matrixWorld)));

            if (axis === "X") {

                this.activePlane = this.planes["XY"];
                if (Math.abs(eye.y) > Math.abs(eye.z)) this.activePlane = this.planes["XZ"];

            }

            if (axis === "Y") {

                this.activePlane = this.planes["XY"];
                if (Math.abs(eye.x) > Math.abs(eye.z)) this.activePlane = this.planes["YZ"];

            }

            if (axis === "Z") {

                this.activePlane = this.planes["XZ"];
                if (Math.abs(eye.x) > Math.abs(eye.y)) this.activePlane = this.planes["YZ"];

            }

            if (axis === "XYZ") this.activePlane = this.planes["XYZE"];

        };

        this.init();

    };

    THREE.TransformGizmoScale.prototype = Object.create(THREE.TransformGizmo.prototype);
    THREE.TransformGizmoScale.prototype.constructor = THREE.TransformGizmoScale;

    THREE.TransformControls = function (camera, domElement) {

        // TODO: Make non-uniform scale and rotate play nice in hierarchies
        // TODO: ADD RXYZ contol

        THREE.Object3D.call(this);

        domElement = (domElement !== undefined) ? domElement : document;

        this.object = undefined;
        this.visible = false;
        this.translationSnap = null;
        this.rotationSnap = null;
        this.space = "world";
        this.size = 1;
        this.axis = null;

        var scope = this;

        var _mode = "translate";
        var _dragging = false;
        var _gizmo = {

            "translate": new THREE.TransformGizmoTranslate(),
            "rotate": new THREE.TransformGizmoRotate(),
            "scale": new THREE.TransformGizmoScale()
        };

        for (var type in _gizmo) {

            var gizmoObj = _gizmo[type];

            gizmoObj.visible = (type === _mode);
            this.add(gizmoObj);

        }

        var changeEvent = { type: "change" };
        var mouseDownEvent = { type: "mouseDown" };
        var mouseUpEvent = { type: "mouseUp", mode: _mode };
        var objectChangeEvent = { type: "objectChange" };

        var ray = new THREE.Raycaster();
        var pointerVector = new THREE.Vector2();

        var point = new THREE.Vector3();
        var offset = new THREE.Vector3();

        var rotation = new THREE.Vector3();
        var offsetRotation = new THREE.Vector3();
        var scale = 1;

        var lookAtMatrix = new THREE.Matrix4();
        var eye = new THREE.Vector3();

        var tempMatrix = new THREE.Matrix4();
        var tempVector = new THREE.Vector3();
        var tempQuaternion = new THREE.Quaternion();
        var unitX = new THREE.Vector3(1, 0, 0);
        var unitY = new THREE.Vector3(0, 1, 0);
        var unitZ = new THREE.Vector3(0, 0, 1);

        var quaternionXYZ = new THREE.Quaternion();
        var quaternionX = new THREE.Quaternion();
        var quaternionY = new THREE.Quaternion();
        var quaternionZ = new THREE.Quaternion();
        var quaternionE = new THREE.Quaternion();

        var oldPosition = new THREE.Vector3();
        var oldScale = new THREE.Vector3();
        var oldRotationMatrix = new THREE.Matrix4();

        var parentRotationMatrix = new THREE.Matrix4();
        var parentScale = new THREE.Vector3();

        var worldPosition = new THREE.Vector3();
        var worldRotation = new THREE.Euler();
        var worldRotationMatrix = new THREE.Matrix4();
        var camPosition = new THREE.Vector3();
        var camRotation = new THREE.Euler();

        domElement.addEventListener("mousedown", onPointerDown, false);
        domElement.addEventListener("touchstart", onPointerDown, false);

        domElement.addEventListener("mousemove", onPointerHover, false);
        domElement.addEventListener("touchmove", onPointerHover, false);

        domElement.addEventListener("mousemove", onPointerMove, false);
        domElement.addEventListener("touchmove", onPointerMove, false);

        domElement.addEventListener("mouseup", onPointerUp, false);
        domElement.addEventListener("mouseout", onPointerUp, false);
        domElement.addEventListener("touchend", onPointerUp, false);
        domElement.addEventListener("touchcancel", onPointerUp, false);
        domElement.addEventListener("touchleave", onPointerUp, false);

        this.dispose = function () {

            domElement.removeEventListener("mousedown", onPointerDown);
            domElement.removeEventListener("touchstart", onPointerDown);

            domElement.removeEventListener("mousemove", onPointerHover);
            domElement.removeEventListener("touchmove", onPointerHover);

            domElement.removeEventListener("mousemove", onPointerMove);
            domElement.removeEventListener("touchmove", onPointerMove);

            domElement.removeEventListener("mouseup", onPointerUp);
            domElement.removeEventListener("mouseout", onPointerUp);
            domElement.removeEventListener("touchend", onPointerUp);
            domElement.removeEventListener("touchcancel", onPointerUp);
            domElement.removeEventListener("touchleave", onPointerUp);

        };

        this.attach = function (object) {

            this.object = object;
            this.visible = true;
            this.update();

        };

        this.detach = function () {

            this.object = undefined;
            this.visible = false;
            this.axis = null;

        };

        this.getMode = function () {

            return _mode;

        };

        this.setMode = function (mode) {

            _mode = mode ? mode : _mode;

            if (_mode === "scale") scope.space = "local";

            for (var type in _gizmo) _gizmo[type].visible = (type === _mode);

            this.update();
            scope.dispatchEvent(changeEvent);

        };

        this.setTranslationSnap = function (translationSnap) {

            scope.translationSnap = translationSnap;

        };

        this.setRotationSnap = function (rotationSnap) {

            scope.rotationSnap = rotationSnap;

        };

        this.setSize = function (size) {

            scope.size = size;
            this.update();
            scope.dispatchEvent(changeEvent);

        };

        this.setSpace = function (space) {

            scope.space = space;
            this.update();
            scope.dispatchEvent(changeEvent);

        };

        this.update = function () {

            if (scope.object === undefined) return;

            scope.object.updateMatrixWorld();
            worldPosition.setFromMatrixPosition(scope.object.matrixWorld);
            worldRotation.setFromRotationMatrix(tempMatrix.extractRotation(scope.object.matrixWorld));

            camera.updateMatrixWorld();
            camPosition.setFromMatrixPosition(camera.matrixWorld);
            camRotation.setFromRotationMatrix(tempMatrix.extractRotation(camera.matrixWorld));

            scale = worldPosition.distanceTo(camPosition) / 6 * scope.size;
            this.position.copy(worldPosition);
            this.scale.set(scale, scale, scale);

            if (camera instanceof THREE.PerspectiveCamera) {

                eye.copy(camPosition).sub(worldPosition).normalize();

            } else if (camera instanceof THREE.OrthographicCamera) {

                eye.copy(camPosition).normalize();

            }

            if (scope.space === "local") {

                _gizmo[_mode].update(worldRotation, eye);

            } else if (scope.space === "world") {

                _gizmo[_mode].update(new THREE.Euler(), eye);

            }

            _gizmo[_mode].highlight(scope.axis);

        };

        function onPointerHover(event) {

            if (scope.object === undefined || _dragging === true || (event.button !== undefined && event.button !== 0)) return;

            var pointer = event.changedTouches ? event.changedTouches[0] : event;

            var intersect = intersectObjects(pointer, _gizmo[_mode].pickers.children);

            var axis = null;

            if (intersect) {

                axis = intersect.object.name;

                event.preventDefault();

            }

            if (scope.axis !== axis) {

                scope.axis = axis;
                scope.update();
                scope.dispatchEvent(changeEvent);

            }

        }

        function onPointerDown(event) {

            if (scope.object === undefined || _dragging === true || (event.button !== undefined && event.button !== 0)) return;

            var pointer = event.changedTouches ? event.changedTouches[0] : event;

            if (pointer.button === 0 || pointer.button === undefined) {

                var intersect = intersectObjects(pointer, _gizmo[_mode].pickers.children);

                if (intersect) {

                    event.preventDefault();
                    event.stopPropagation();

                    scope.axis = intersect.object.name;

                    scope.dispatchEvent(mouseDownEvent);

                    scope.update();

                    eye.copy(camPosition).sub(worldPosition).normalize();

                    _gizmo[_mode].setActivePlane(scope.axis, eye);

                    var planeIntersect = intersectObjects(pointer, [_gizmo[_mode].activePlane]);

                    if (planeIntersect) {

                        oldPosition.copy(scope.object.position);
                        oldScale.copy(scope.object.scale);

                        oldRotationMatrix.extractRotation(scope.object.matrix);
                        worldRotationMatrix.extractRotation(scope.object.matrixWorld);

                        parentRotationMatrix.extractRotation(scope.object.parent.matrixWorld);
                        parentScale.setFromMatrixScale(tempMatrix.getInverse(scope.object.parent.matrixWorld));

                        offset.copy(planeIntersect.point);

                    }

                }

            }

            _dragging = true;

        }

        function onPointerMove(event) {

            if (scope.object === undefined || scope.axis === null || _dragging === false || (event.button !== undefined && event.button !== 0)) return;

            var pointer = event.changedTouches ? event.changedTouches[0] : event;

            var planeIntersect = intersectObjects(pointer, [_gizmo[_mode].activePlane]);

            if (planeIntersect === false) return;

            event.preventDefault();
            event.stopPropagation();

            point.copy(planeIntersect.point);

            if (_mode === "translate") {

                point.sub(offset);
                point.multiply(parentScale);

                if (scope.space === "local") {

                    point.applyMatrix4(tempMatrix.getInverse(worldRotationMatrix));

                    if (scope.axis.search("X") === -1) point.x = 0;
                    if (scope.axis.search("Y") === -1) point.y = 0;
                    if (scope.axis.search("Z") === -1) point.z = 0;

                    point.applyMatrix4(oldRotationMatrix);

                    scope.object.position.copy(oldPosition);
                    scope.object.position.add(point);

                }

                if (scope.space === "world" || scope.axis.search("XYZ") !== -1) {

                    if (scope.axis.search("X") === -1) point.x = 0;
                    if (scope.axis.search("Y") === -1) point.y = 0;
                    if (scope.axis.search("Z") === -1) point.z = 0;

                    point.applyMatrix4(tempMatrix.getInverse(parentRotationMatrix));

                    scope.object.position.copy(oldPosition);
                    scope.object.position.add(point);

                }

                if (scope.translationSnap !== null) {

                    if (scope.space === "local") {

                        scope.object.position.applyMatrix4(tempMatrix.getInverse(worldRotationMatrix));

                    }

                    if (scope.axis.search("X") !== -1) scope.object.position.x = Math.round(scope.object.position.x / scope.translationSnap) * scope.translationSnap;
                    if (scope.axis.search("Y") !== -1) scope.object.position.y = Math.round(scope.object.position.y / scope.translationSnap) * scope.translationSnap;
                    if (scope.axis.search("Z") !== -1) scope.object.position.z = Math.round(scope.object.position.z / scope.translationSnap) * scope.translationSnap;

                    if (scope.space === "local") {

                        scope.object.position.applyMatrix4(worldRotationMatrix);

                    }

                }

            } else if (_mode === "scale") {

                point.sub(offset);
                point.multiply(parentScale);

                if (scope.space === "local") {

                    if (scope.axis === "XYZ") {

                        scale = 1 + ((point.y) / Math.max(oldScale.x, oldScale.y, oldScale.z));

                        scope.object.scale.x = oldScale.x * scale;
                        scope.object.scale.y = oldScale.y * scale;
                        scope.object.scale.z = oldScale.z * scale;

                    } else {

                        point.applyMatrix4(tempMatrix.getInverse(worldRotationMatrix));

                        if (scope.axis === "X") scope.object.scale.x = oldScale.x * (1 + point.x / oldScale.x);
                        if (scope.axis === "Y") scope.object.scale.y = oldScale.y * (1 + point.y / oldScale.y);
                        if (scope.axis === "Z") scope.object.scale.z = oldScale.z * (1 + point.z / oldScale.z);

                    }

                }

            } else if (_mode === "rotate") {

                point.sub(worldPosition);
                point.multiply(parentScale);
                tempVector.copy(offset).sub(worldPosition);
                tempVector.multiply(parentScale);

                if (scope.axis === "E") {

                    point.applyMatrix4(tempMatrix.getInverse(lookAtMatrix));
                    tempVector.applyMatrix4(tempMatrix.getInverse(lookAtMatrix));

                    rotation.set(Math.atan2(point.z, point.y), Math.atan2(point.x, point.z), Math.atan2(point.y, point.x));
                    offsetRotation.set(Math.atan2(tempVector.z, tempVector.y), Math.atan2(tempVector.x, tempVector.z), Math.atan2(tempVector.y, tempVector.x));

                    tempQuaternion.setFromRotationMatrix(tempMatrix.getInverse(parentRotationMatrix));

                    quaternionE.setFromAxisAngle(eye, rotation.z - offsetRotation.z);
                    quaternionXYZ.setFromRotationMatrix(worldRotationMatrix);

                    tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionE);
                    tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionXYZ);

                    scope.object.quaternion.copy(tempQuaternion);

                } else if (scope.axis === "XYZE") {

                    quaternionE.setFromEuler(point.clone().cross(tempVector).normalize()); // rotation axis

                    tempQuaternion.setFromRotationMatrix(tempMatrix.getInverse(parentRotationMatrix));
                    quaternionX.setFromAxisAngle(quaternionE, -point.clone().angleTo(tempVector));
                    quaternionXYZ.setFromRotationMatrix(worldRotationMatrix);

                    tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionX);
                    tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionXYZ);

                    scope.object.quaternion.copy(tempQuaternion);

                } else if (scope.space === "local") {

                    point.applyMatrix4(tempMatrix.getInverse(worldRotationMatrix));

                    tempVector.applyMatrix4(tempMatrix.getInverse(worldRotationMatrix));

                    rotation.set(Math.atan2(point.z, point.y), Math.atan2(point.x, point.z), Math.atan2(point.y, point.x));
                    offsetRotation.set(Math.atan2(tempVector.z, tempVector.y), Math.atan2(tempVector.x, tempVector.z), Math.atan2(tempVector.y, tempVector.x));

                    quaternionXYZ.setFromRotationMatrix(oldRotationMatrix);

                    if (scope.rotationSnap !== null) {

                        quaternionX.setFromAxisAngle(unitX, Math.round((rotation.x - offsetRotation.x) / scope.rotationSnap) * scope.rotationSnap);
                        quaternionY.setFromAxisAngle(unitY, Math.round((rotation.y - offsetRotation.y) / scope.rotationSnap) * scope.rotationSnap);
                        quaternionZ.setFromAxisAngle(unitZ, Math.round((rotation.z - offsetRotation.z) / scope.rotationSnap) * scope.rotationSnap);

                    } else {

                        quaternionX.setFromAxisAngle(unitX, rotation.x - offsetRotation.x);
                        quaternionY.setFromAxisAngle(unitY, rotation.y - offsetRotation.y);
                        quaternionZ.setFromAxisAngle(unitZ, rotation.z - offsetRotation.z);

                    }

                    if (scope.axis === "X") quaternionXYZ.multiplyQuaternions(quaternionXYZ, quaternionX);
                    if (scope.axis === "Y") quaternionXYZ.multiplyQuaternions(quaternionXYZ, quaternionY);
                    if (scope.axis === "Z") quaternionXYZ.multiplyQuaternions(quaternionXYZ, quaternionZ);

                    scope.object.quaternion.copy(quaternionXYZ);

                } else if (scope.space === "world") {

                    rotation.set(Math.atan2(point.z, point.y), Math.atan2(point.x, point.z), Math.atan2(point.y, point.x));
                    offsetRotation.set(Math.atan2(tempVector.z, tempVector.y), Math.atan2(tempVector.x, tempVector.z), Math.atan2(tempVector.y, tempVector.x));

                    tempQuaternion.setFromRotationMatrix(tempMatrix.getInverse(parentRotationMatrix));

                    if (scope.rotationSnap !== null) {

                        quaternionX.setFromAxisAngle(unitX, Math.round((rotation.x - offsetRotation.x) / scope.rotationSnap) * scope.rotationSnap);
                        quaternionY.setFromAxisAngle(unitY, Math.round((rotation.y - offsetRotation.y) / scope.rotationSnap) * scope.rotationSnap);
                        quaternionZ.setFromAxisAngle(unitZ, Math.round((rotation.z - offsetRotation.z) / scope.rotationSnap) * scope.rotationSnap);

                    } else {

                        quaternionX.setFromAxisAngle(unitX, rotation.x - offsetRotation.x);
                        quaternionY.setFromAxisAngle(unitY, rotation.y - offsetRotation.y);
                        quaternionZ.setFromAxisAngle(unitZ, rotation.z - offsetRotation.z);

                    }

                    quaternionXYZ.setFromRotationMatrix(worldRotationMatrix);

                    if (scope.axis === "X") tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionX);
                    if (scope.axis === "Y") tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionY);
                    if (scope.axis === "Z") tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionZ);

                    tempQuaternion.multiplyQuaternions(tempQuaternion, quaternionXYZ);

                    scope.object.quaternion.copy(tempQuaternion);

                }

            }

            scope.update();
            scope.dispatchEvent(changeEvent);
            scope.dispatchEvent(objectChangeEvent);

        }

        function onPointerUp(event) {

            event.preventDefault(); // Prevent MouseEvent on mobile

            if (event.button !== undefined && event.button !== 0) return;

            if (_dragging && (scope.axis !== null)) {

                mouseUpEvent.mode = _mode;
                scope.dispatchEvent(mouseUpEvent);

            }

            _dragging = false;

            if ('TouchEvent' in window && event instanceof TouchEvent) {

                // Force "rollover"

                scope.axis = null;
                scope.update();
                scope.dispatchEvent(changeEvent);

            } else {

                onPointerHover(event);

            }

        }

        function intersectObjects(pointer, objects) {

            var rect = domElement.getBoundingClientRect();
            var x = (pointer.clientX - rect.left) / rect.width;
            var y = (pointer.clientY - rect.top) / rect.height;

            pointerVector.set((x * 2) - 1, -(y * 2) + 1);
            ray.setFromCamera(pointerVector, camera);

            var intersections = ray.intersectObjects(objects, true);
            return intersections[0] ? intersections[0] : false;

        }

    };

    THREE.TransformControls.prototype = Object.create(THREE.Object3D.prototype);
    THREE.TransformControls.prototype.constructor = THREE.TransformControls;

}());