@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
1,455 lines (1,025 loc) • 51 kB
JavaScript
import {
BoxBufferGeometry,
BufferGeometry,
Color,
CylinderBufferGeometry,
DoubleSide,
Euler,
Float32BufferAttribute,
Line,
LineBasicMaterial,
Matrix4,
Mesh,
MeshBasicMaterial,
Object3D,
OctahedronBufferGeometry,
OrthographicCamera,
PerspectiveCamera,
PlaneBufferGeometry,
Quaternion,
Raycaster,
SphereBufferGeometry,
TorusBufferGeometry,
Vector3
} from 'three';
/**
* @author arodic / https://github.com/arodic
* @author Alex Goldring - Adapted to ES6 module
*/
const TransformControls = function (camera, domElement) {
Object3D.call(this);
domElement = (domElement !== undefined) ? domElement : document;
this.visible = false;
var _gizmo = new TransformControlsGizmo();
this.add(_gizmo);
var _plane = new TransformControlsPlane();
this.add(_plane);
var scope = this;
// Define properties with getters/setter
// Setting the defined property will automatically trigger change event
// Defined properties are passed down to gizmo and plane
defineProperty("camera", camera);
defineProperty("object", undefined);
defineProperty("axis", null);
defineProperty("mode", "translate");
defineProperty("translationSnap", null);
defineProperty("rotationSnap", null);
defineProperty("space", "world");
defineProperty("size", 1);
defineProperty("dragging", false);
var changeEvent = { type: "change" };
var mouseDownEvent = { type: "mouseDown" };
var mouseUpEvent = { type: "mouseUp", mode: scope.mode };
var objectChangeEvent = { type: "objectChange" };
// Reusable utility variables
var ray = new Raycaster();
var _tempVector = new Vector3();
var _tempVector2 = new Vector3();
var _tempQuaternion = new Quaternion();
var _unit = {
X: new Vector3(1, 0, 0),
Y: new Vector3(0, 1, 0),
Z: new Vector3(0, 0, 1)
}
var _identityQuaternion = new Quaternion();
var _alignVector = new Vector3();
var pointStart = new Vector3();
var pointEnd = new Vector3();
var rotationAxis = new Vector3();
var rotationAngle = 0;
var cameraPosition = new Vector3();
var cameraQuaternion = new Quaternion();
var cameraScale = new Vector3();
var parentPosition = new Vector3();
var parentQuaternion = new Quaternion();
var parentScale = new Vector3();
var worldPositionStart = new Vector3();
var worldQuaternionStart = new Quaternion();
var worldScaleStart = new Vector3();
var worldPosition = new Vector3();
var worldQuaternion = new Quaternion();
var worldScale = new Vector3();
var eye = new Vector3();
var _positionStart = new Vector3();
var _quaternionStart = new Quaternion();
var _scaleStart = new Vector3();
// TODO: remove properties unused in plane and gizmo
defineProperty("parentQuaternion", parentQuaternion);
defineProperty("worldPosition", worldPosition);
defineProperty("worldPositionStart", worldPositionStart);
defineProperty("worldQuaternion", worldQuaternion);
defineProperty("worldQuaternionStart", worldQuaternionStart);
defineProperty("cameraPosition", cameraPosition);
defineProperty("cameraQuaternion", cameraQuaternion);
defineProperty("pointStart", pointStart);
defineProperty("pointEnd", pointEnd);
defineProperty("rotationAxis", rotationAxis);
defineProperty("rotationAngle", rotationAngle);
defineProperty("eye", eye);
{
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("mouseleave", onPointerUp, false);
domElement.addEventListener("mouseout", onPointerUp, false);
domElement.addEventListener("touchend", onPointerUp, false);
domElement.addEventListener("touchcancel", onPointerUp, false);
domElement.addEventListener("touchleave", onPointerUp, false);
domElement.addEventListener("contextmenu", onContext, 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("mouseleave", onPointerUp);
domElement.removeEventListener("mouseout", onPointerUp);
domElement.removeEventListener("touchend", onPointerUp);
domElement.removeEventListener("touchcancel", onPointerUp);
domElement.removeEventListener("touchleave", onPointerUp);
domElement.removeEventListener("contextmenu", onContext);
};
// Set current object
this.attach = function (object) {
this.object = object;
this.visible = true;
};
// Detatch from object
this.detach = function () {
this.object = undefined;
this.visible = false;
this.axis = null;
};
// Defined getter, setter and store for a property
function defineProperty(propName, defaultValue) {
var propValue = defaultValue;
Object.defineProperty(scope, propName, {
get: function () {
return propValue !== undefined ? propValue : defaultValue;
},
set: function (value) {
if (propValue !== value) {
propValue = value;
_plane[propName] = value;
_gizmo[propName] = value;
scope.dispatchEvent(changeEvent);
}
}
});
scope[propName] = defaultValue;
_plane[propName] = defaultValue;
_gizmo[propName] = defaultValue;
}
// updateMatrixWorld updates key transformation variables
this.updateMatrixWorld = function () {
if (this.object !== undefined) {
this.object.updateMatrixWorld();
this.object.parent.matrixWorld.decompose(parentPosition, parentQuaternion, parentScale);
this.object.matrixWorld.decompose(worldPosition, worldQuaternion, worldScale);
}
this.camera.updateMatrixWorld();
this.camera.matrixWorld.decompose(cameraPosition, cameraQuaternion, cameraScale);
if (this.camera instanceof PerspectiveCamera) {
eye.copy(cameraPosition).sub(worldPosition).normalize();
} else if (this.camera instanceof OrthographicCamera) {
eye.copy(cameraPosition).normalize();
}
Object3D.prototype.updateMatrixWorld.call(this);
};
this.pointerHover = function (pointer) {
if (this.object === undefined || this.dragging === true || (pointer.button !== undefined && pointer.button !== 0)) return;
ray.setFromCamera(pointer, this.camera);
var intersect = ray.intersectObjects(_gizmo.picker[this.mode].children, true)[0] || false;
if (intersect) {
this.axis = intersect.object.name;
} else {
this.axis = null;
}
}
this.pointerDown = function (pointer) {
if (this.object === undefined || this.dragging === true || (pointer.button !== undefined && pointer.button !== 0)) return;
if ((pointer.button === 0 || pointer.button === undefined) && this.axis !== null) {
ray.setFromCamera(pointer, this.camera);
var planeIntersect = ray.intersectObjects([_plane], true)[0] || false;
if (planeIntersect) {
var space = this.space;
if (this.mode === 'scale') {
space = 'local';
} else if (this.axis === 'E' || this.axis === 'XYZE' || this.axis === 'XYZ') {
space = 'world';
}
if (space === 'local' && this.mode === 'rotate') {
var snap = this.rotationSnap;
if (this.axis === 'X' && snap) this.object.rotation.x = Math.round(this.object.rotation.x / snap) * snap;
if (this.axis === 'Y' && snap) this.object.rotation.y = Math.round(this.object.rotation.y / snap) * snap;
if (this.axis === 'Z' && snap) this.object.rotation.z = Math.round(this.object.rotation.z / snap) * snap;
}
this.object.updateMatrixWorld();
this.object.parent.updateMatrixWorld();
_positionStart.copy(this.object.position);
_quaternionStart.copy(this.object.quaternion);
_scaleStart.copy(this.object.scale);
this.object.matrixWorld.decompose(worldPositionStart, worldQuaternionStart, worldScaleStart);
pointStart.copy(planeIntersect.point).sub(worldPositionStart);
if (space === 'local') pointStart.applyQuaternion(worldQuaternionStart.clone().inverse());
}
this.dragging = true;
mouseDownEvent.mode = this.mode;
this.dispatchEvent(mouseDownEvent);
}
}
this.pointerMove = function (pointer) {
var axis = this.axis;
var mode = this.mode;
var object = this.object;
var space = this.space;
if (mode === 'scale') {
space = 'local';
} else if (axis === 'E' || axis === 'XYZE' || axis === 'XYZ') {
space = 'world';
}
if (object === undefined || axis === null || this.dragging === false || (pointer.button !== undefined && pointer.button !== 0)) return;
ray.setFromCamera(pointer, this.camera);
var planeIntersect = ray.intersectObjects([_plane], true)[0] || false;
if (planeIntersect === false) return;
pointEnd.copy(planeIntersect.point).sub(worldPositionStart);
if (space === 'local') pointEnd.applyQuaternion(worldQuaternionStart.clone().inverse());
if (mode === 'translate') {
if (axis.search('X') === -1) {
pointEnd.x = pointStart.x;
}
if (axis.search('Y') === -1) {
pointEnd.y = pointStart.y;
}
if (axis.search('Z') === -1) {
pointEnd.z = pointStart.z;
}
// Apply translate
if (space === 'local') {
object.position.copy(pointEnd).sub(pointStart).applyQuaternion(_quaternionStart);
} else {
object.position.copy(pointEnd).sub(pointStart);
}
object.position.add(_positionStart);
// Apply translation snap
if (this.translationSnap) {
if (space === 'local') {
object.position.applyQuaternion(_tempQuaternion.copy(_quaternionStart).inverse());
if (axis.search('X') !== -1) {
object.position.x = Math.round(object.position.x / this.translationSnap) * this.translationSnap;
}
if (axis.search('Y') !== -1) {
object.position.y = Math.round(object.position.y / this.translationSnap) * this.translationSnap;
}
if (axis.search('Z') !== -1) {
object.position.z = Math.round(object.position.z / this.translationSnap) * this.translationSnap;
}
object.position.applyQuaternion(_quaternionStart);
}
if (space === 'world') {
if (object.parent) {
object.position.add(_tempVector.setFromMatrixPosition(object.parent.matrixWorld));
}
if (axis.search('X') !== -1) {
object.position.x = Math.round(object.position.x / this.translationSnap) * this.translationSnap;
}
if (axis.search('Y') !== -1) {
object.position.y = Math.round(object.position.y / this.translationSnap) * this.translationSnap;
}
if (axis.search('Z') !== -1) {
object.position.z = Math.round(object.position.z / this.translationSnap) * this.translationSnap;
}
if (object.parent) {
object.position.sub(_tempVector.setFromMatrixPosition(object.parent.matrixWorld));
}
}
}
} else if (mode === 'scale') {
if (axis.search('XYZ') !== -1) {
var d = pointEnd.length() / pointStart.length();
if (pointEnd.dot(pointStart) < 0) d *= -1;
_tempVector.set(d, d, d);
} else {
_tempVector.copy(pointEnd).divide(pointStart);
if (axis.search('X') === -1) {
_tempVector.x = 1;
}
if (axis.search('Y') === -1) {
_tempVector.y = 1;
}
if (axis.search('Z') === -1) {
_tempVector.z = 1;
}
}
// Apply scale
object.scale.copy(_scaleStart).multiply(_tempVector);
} else if (mode === 'rotate') {
var ROTATION_SPEED = 20 / worldPosition.distanceTo(_tempVector.setFromMatrixPosition(this.camera.matrixWorld));
var quaternion = this.space === "local" ? worldQuaternion : _identityQuaternion;
var unit = _unit[axis];
if (axis === 'E') {
_tempVector.copy(pointEnd).cross(pointStart);
rotationAxis.copy(eye);
rotationAngle = pointEnd.angleTo(pointStart) * (_tempVector.dot(eye) < 0 ? 1 : -1);
} else if (axis === 'XYZE') {
_tempVector.copy(pointEnd).sub(pointStart).cross(eye).normalize();
rotationAxis.copy(_tempVector);
rotationAngle = pointEnd.sub(pointStart).dot(_tempVector.cross(eye)) * ROTATION_SPEED;
} else if (axis === 'X' || axis === 'Y' || axis === 'Z') {
_alignVector.copy(unit).applyQuaternion(quaternion);
rotationAxis.copy(unit);
_tempVector = unit.clone();
_tempVector2 = pointEnd.clone().sub(pointStart);
if (space === 'local') {
_tempVector.applyQuaternion(quaternion);
_tempVector2.applyQuaternion(worldQuaternionStart);
}
rotationAngle = _tempVector2.dot(_tempVector.cross(eye).normalize()) * ROTATION_SPEED;
}
// Apply rotation snap
if (this.rotationSnap) rotationAngle = Math.round(rotationAngle / this.rotationSnap) * this.rotationSnap;
this.rotationAngle = rotationAngle;
// Apply rotate
if (space === 'local') {
object.quaternion.copy(_quaternionStart);
object.quaternion.multiply(_tempQuaternion.setFromAxisAngle(rotationAxis, rotationAngle));
} else {
object.quaternion.copy(_tempQuaternion.setFromAxisAngle(rotationAxis, rotationAngle));
object.quaternion.multiply(_quaternionStart);
}
}
this.dispatchEvent(changeEvent);
this.dispatchEvent(objectChangeEvent);
}
this.pointerUp = function (pointer) {
if (pointer.button !== undefined && pointer.button !== 0) return;
if (this.dragging && (this.axis !== null)) {
mouseUpEvent.mode = this.mode;
this.dispatchEvent(mouseUpEvent);
}
this.dragging = false;
if (pointer.button === undefined) this.axis = null;
}
// normalize mouse / touch pointer and remap {x,y} to view space.
function getPointer(event) {
var pointer = event.changedTouches ? event.changedTouches[0] : event;
var rect = domElement.getBoundingClientRect();
return {
x: (pointer.clientX - rect.left) / rect.width * 2 - 1,
y: -(pointer.clientY - rect.top) / rect.height * 2 + 1,
button: event.button
}
}
// mouse / touch event handlers
function onContext(event) {
event.preventDefault();
}
function onPointerHover(event) {
// event.preventDefault();
scope.pointerHover(getPointer(event));
}
function onPointerDown(event) {
event.preventDefault();
event.stopPropagation();
scope.pointerHover(getPointer(event));
scope.pointerDown(getPointer(event));
}
function onPointerMove(event) {
event.preventDefault();
event.stopPropagation();
scope.pointerMove(getPointer(event));
}
function onPointerUp(event) {
event.preventDefault(); // Prevent MouseEvent on mobile
scope.pointerUp(getPointer(event));
}
// TODO: depricate
this.getMode = function () {
return scope.mode;
};
this.setMode = function (mode) {
scope.mode = mode;
};
this.setTranslationSnap = function (translationSnap) {
scope.translationSnap = translationSnap;
};
this.setRotationSnap = function (rotationSnap) {
scope.rotationSnap = rotationSnap;
};
this.setSize = function (size) {
scope.size = size;
};
this.setSpace = function (space) {
scope.space = space;
};
this.update = function () {
console.warn('TransformControls: update function has been depricated.');
}
};
TransformControls.prototype = Object.assign(Object.create(Object3D.prototype), {
constructor: TransformControls,
isTransformControls: true
});
const TransformControlsGizmo = function () {
'use strict';
Object3D.call(this);
this.type = 'TransformControlsGizmo';
// shared materials
var gizmoMaterial = new MeshBasicMaterial({
depthTest: false,
depthWrite: false,
transparent: true,
side: DoubleSide,
fog: false
});
var gizmoLineMaterial = new LineBasicMaterial({
depthTest: false,
depthWrite: false,
transparent: true,
linewidth: 1,
fog: false
});
// Make unique material for each axis/color
var matInvisible = gizmoMaterial.clone();
matInvisible.opacity = 0.15;
var matHelper = gizmoMaterial.clone();
matHelper.opacity = 0.33;
var matRed = gizmoMaterial.clone();
matRed.color.set(0xff0000);
var matGreen = gizmoMaterial.clone();
matGreen.color.set(0x00ff00);
var matBlue = gizmoMaterial.clone();
matBlue.color.set(0x0000ff);
var matWhiteTransperent = gizmoMaterial.clone();
matWhiteTransperent.opacity = 0.25;
var matYellowTransparent = matWhiteTransperent.clone();
matYellowTransparent.color.set(0xffff00);
var matCyanTransparent = matWhiteTransperent.clone();
matCyanTransparent.color.set(0x00ffff);
var matMagentaTransparent = matWhiteTransperent.clone();
matMagentaTransparent.color.set(0xff00ff);
var matYellow = gizmoMaterial.clone();
matYellow.color.set(0xffff00);
var matLineRed = gizmoLineMaterial.clone();
matLineRed.color.set(0xff0000);
var matLineGreen = gizmoLineMaterial.clone();
matLineGreen.color.set(0x00ff00);
var matLineBlue = gizmoLineMaterial.clone();
matLineBlue.color.set(0x0000ff);
var matLineCyan = gizmoLineMaterial.clone();
matLineCyan.color.set(0x00ffff);
var matLineMagenta = gizmoLineMaterial.clone();
matLineMagenta.color.set(0xff00ff);
var matLineBlue = gizmoLineMaterial.clone();
matLineBlue.color.set(0x0000ff);
var matLineYellow = gizmoLineMaterial.clone();
matLineYellow.color.set(0xffff00);
var matLineGray = gizmoLineMaterial.clone();
matLineGray.color.set(0x787878);
var matLineYellowTransparent = matLineYellow.clone();
matLineYellowTransparent.opacity = 0.25;
// reusable geometry
var arrowGeometry = new CylinderBufferGeometry(0, 0.05, 0.2, 12, 1, false);
var scaleHandleGeometry = new BoxBufferGeometry(0.125, 0.125, 0.125);
var lineGeometry = new BufferGeometry();
lineGeometry.setAttribute('position', new Float32BufferAttribute([0, 0, 0, 1, 0, 0], 3));
var CircleGeometry = function (radius, arc) {
var geometry = new BufferGeometry();
var vertices = [];
for (var i = 0; i <= 64 * arc; ++i) {
vertices.push(0, Math.cos(i / 32 * Math.PI) * radius, Math.sin(i / 32 * Math.PI) * radius);
}
geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3));
return geometry;
};
// Special geometry for transform helper. If scaled with position vector it spans from [0,0,0] to position
var TranslateHelperGeometry = function (radius, arc) {
var geometry = new BufferGeometry();
geometry.setAttribute('position', new Float32BufferAttribute([0, 0, 0, 1, 1, 1], 3));
return geometry;
};
// Gizmo definitions - custom hierarchy definitions for setupGizmo() function
var gizmoTranslate = {
X: [
[new Mesh(arrowGeometry, matRed), [1, 0, 0], [0, 0, -Math.PI / 2], null, 'fwd'],
[new Mesh(arrowGeometry, matRed), [1, 0, 0], [0, 0, Math.PI / 2], null, 'bwd'],
[new Line(lineGeometry, matLineRed)]
],
Y: [
[new Mesh(arrowGeometry, matGreen), [0, 1, 0], null, null, 'fwd'],
[new Mesh(arrowGeometry, matGreen), [0, 1, 0], [Math.PI, 0, 0], null, 'bwd'],
[new Line(lineGeometry, matLineGreen), null, [0, 0, Math.PI / 2]]
],
Z: [
[new Mesh(arrowGeometry, matBlue), [0, 0, 1], [Math.PI / 2, 0, 0], null, 'fwd'],
[new Mesh(arrowGeometry, matBlue), [0, 0, 1], [-Math.PI / 2, 0, 0], null, 'bwd'],
[new Line(lineGeometry, matLineBlue), null, [0, -Math.PI / 2, 0]]
],
XYZ: [
[new Mesh(new OctahedronBufferGeometry(0.1, 0), matWhiteTransperent), [0, 0, 0], [0, 0, 0]]
],
XY: [
[new Mesh(new PlaneBufferGeometry(0.295, 0.295), matYellowTransparent), [0.15, 0.15, 0]],
[new Line(lineGeometry, matLineYellow), [0.18, 0.3, 0], null, [0.125, 1, 1]],
[new Line(lineGeometry, matLineYellow), [0.3, 0.18, 0], [0, 0, Math.PI / 2], [0.125, 1, 1]]
],
YZ: [
[new Mesh(new PlaneBufferGeometry(0.295, 0.295), matCyanTransparent), [0, 0.15, 0.15], [0, Math.PI / 2, 0]],
[new Line(lineGeometry, matLineCyan), [0, 0.18, 0.3], [0, 0, Math.PI / 2], [0.125, 1, 1]],
[new Line(lineGeometry, matLineCyan), [0, 0.3, 0.18], [0, -Math.PI / 2, 0], [0.125, 1, 1]]
],
XZ: [
[new Mesh(new PlaneBufferGeometry(0.295, 0.295), matMagentaTransparent), [0.15, 0, 0.15], [-Math.PI / 2, 0, 0]],
[new Line(lineGeometry, matLineMagenta), [0.18, 0, 0.3], null, [0.125, 1, 1]],
[new Line(lineGeometry, matLineMagenta), [0.3, 0, 0.18], [0, -Math.PI / 2, 0], [0.125, 1, 1]]
]
};
var pickerTranslate = {
X: [
[new Mesh(new CylinderBufferGeometry(0.2, 0, 1, 4, 1, false), matInvisible), [0.6, 0, 0], [0, 0, -Math.PI / 2]]
],
Y: [
[new Mesh(new CylinderBufferGeometry(0.2, 0, 1, 4, 1, false), matInvisible), [0, 0.6, 0]]
],
Z: [
[new Mesh(new CylinderBufferGeometry(0.2, 0, 1, 4, 1, false), matInvisible), [0, 0, 0.6], [Math.PI / 2, 0, 0]]
],
XYZ: [
[new Mesh(new OctahedronBufferGeometry(0.2, 0), matInvisible)]
],
XY: [
[new Mesh(new PlaneBufferGeometry(0.4, 0.4), matInvisible), [0.2, 0.2, 0]]
],
YZ: [
[new Mesh(new PlaneBufferGeometry(0.4, 0.4), matInvisible), [0, 0.2, 0.2], [0, Math.PI / 2, 0]]
],
XZ: [
[new Mesh(new PlaneBufferGeometry(0.4, 0.4), matInvisible), [0.2, 0, 0.2], [-Math.PI / 2, 0, 0]]
]
};
var helperTranslate = {
START: [
[new Mesh(new OctahedronBufferGeometry(0.01, 2), matHelper), null, null, null, 'helper']
],
END: [
[new Mesh(new OctahedronBufferGeometry(0.01, 2), matHelper), null, null, null, 'helper']
],
DELTA: [
[new Line(TranslateHelperGeometry(), matHelper), null, null, null, 'helper']
],
X: [
[new Line(lineGeometry, matHelper.clone()), [-1e3, 0, 0], null, [1e6, 1, 1], 'helper']
],
Y: [
[new Line(lineGeometry, matHelper.clone()), [0, -1e3, 0], [0, 0, Math.PI / 2], [1e6, 1, 1], 'helper']
],
Z: [
[new Line(lineGeometry, matHelper.clone()), [0, 0, -1e3], [0, -Math.PI / 2, 0], [1e6, 1, 1], 'helper']
]
};
var gizmoRotate = {
X: [
[new Line(CircleGeometry(1, 0.5), matLineRed)],
[new Mesh(new OctahedronBufferGeometry(0.04, 0), matRed), [0, 0, 0.99], null, [1, 3, 1]],
],
Y: [
[new Line(CircleGeometry(1, 0.5), matLineGreen), null, [0, 0, -Math.PI / 2]],
[new Mesh(new OctahedronBufferGeometry(0.04, 0), matGreen), [0, 0, 0.99], null, [3, 1, 1]],
],
Z: [
[new Line(CircleGeometry(1, 0.5), matLineBlue), null, [0, Math.PI / 2, 0]],
[new Mesh(new OctahedronBufferGeometry(0.04, 0), matBlue), [0.99, 0, 0], null, [1, 3, 1]],
],
E: [
[new Line(CircleGeometry(1.25, 1), matLineYellowTransparent), null, [0, Math.PI / 2, 0]],
[new Mesh(new CylinderBufferGeometry(0.03, 0, 0.15, 4, 1, false), matLineYellowTransparent), [1.17, 0, 0], [0, 0, -Math.PI / 2], [1, 1, 0.001]],
[new Mesh(new CylinderBufferGeometry(0.03, 0, 0.15, 4, 1, false), matLineYellowTransparent), [-1.17, 0, 0], [0, 0, Math.PI / 2], [1, 1, 0.001]],
[new Mesh(new CylinderBufferGeometry(0.03, 0, 0.15, 4, 1, false), matLineYellowTransparent), [0, -1.17, 0], [Math.PI, 0, 0], [1, 1, 0.001]],
[new Mesh(new CylinderBufferGeometry(0.03, 0, 0.15, 4, 1, false), matLineYellowTransparent), [0, 1.17, 0], [0, 0, 0], [1, 1, 0.001]],
],
XYZE: [
[new Line(CircleGeometry(1, 1), matLineGray), null, [0, Math.PI / 2, 0]]
]
};
var helperRotate = {
AXIS: [
[new Line(lineGeometry, matHelper.clone()), [-1e3, 0, 0], null, [1e6, 1, 1], 'helper']
]
};
var pickerRotate = {
X: [
[new Mesh(new TorusBufferGeometry(1, 0.1, 4, 24), matInvisible), [0, 0, 0], [0, -Math.PI / 2, -Math.PI / 2]],
],
Y: [
[new Mesh(new TorusBufferGeometry(1, 0.1, 4, 24), matInvisible), [0, 0, 0], [Math.PI / 2, 0, 0]],
],
Z: [
[new Mesh(new TorusBufferGeometry(1, 0.1, 4, 24), matInvisible), [0, 0, 0], [0, 0, -Math.PI / 2]],
],
E: [
[new Mesh(new TorusBufferGeometry(1.25, 0.1, 2, 24), matInvisible)]
],
XYZE: [
[new Mesh(new SphereBufferGeometry(0.7, 10, 8), matInvisible)]
]
};
var gizmoScale = {
X: [
[new Mesh(scaleHandleGeometry, matRed), [0.8, 0, 0], [0, 0, -Math.PI / 2]],
[new Line(lineGeometry, matLineRed), null, null, [0.8, 1, 1]]
],
Y: [
[new Mesh(scaleHandleGeometry, matGreen), [0, 0.8, 0]],
[new Line(lineGeometry, matLineGreen), null, [0, 0, Math.PI / 2], [0.8, 1, 1]]
],
Z: [
[new Mesh(scaleHandleGeometry, matBlue), [0, 0, 0.8], [Math.PI / 2, 0, 0]],
[new Line(lineGeometry, matLineBlue), null, [0, -Math.PI / 2, 0], [0.8, 1, 1]]
],
XY: [
[new Mesh(scaleHandleGeometry, matYellowTransparent), [0.85, 0.85, 0], null, [2, 2, 0.2]],
[new Line(lineGeometry, matLineYellow), [0.855, 0.98, 0], null, [0.125, 1, 1]],
[new Line(lineGeometry, matLineYellow), [0.98, 0.855, 0], [0, 0, Math.PI / 2], [0.125, 1, 1]]
],
YZ: [
[new Mesh(scaleHandleGeometry, matCyanTransparent), [0, 0.85, 0.85], null, [0.2, 2, 2]],
[new Line(lineGeometry, matLineCyan), [0, 0.855, 0.98], [0, 0, Math.PI / 2], [0.125, 1, 1]],
[new Line(lineGeometry, matLineCyan), [0, 0.98, 0.855], [0, -Math.PI / 2, 0], [0.125, 1, 1]]
],
XZ: [
[new Mesh(scaleHandleGeometry, matMagentaTransparent), [0.85, 0, 0.85], null, [2, 0.2, 2]],
[new Line(lineGeometry, matLineMagenta), [0.855, 0, 0.98], null, [0.125, 1, 1]],
[new Line(lineGeometry, matLineMagenta), [0.98, 0, 0.855], [0, -Math.PI / 2, 0], [0.125, 1, 1]]
],
XYZX: [
[new Mesh(new BoxBufferGeometry(0.125, 0.125, 0.125), matWhiteTransperent), [1.1, 0, 0]],
],
XYZY: [
[new Mesh(new BoxBufferGeometry(0.125, 0.125, 0.125), matWhiteTransperent), [0, 1.1, 0]],
],
XYZZ: [
[new Mesh(new BoxBufferGeometry(0.125, 0.125, 0.125), matWhiteTransperent), [0, 0, 1.1]],
]
};
var pickerScale = {
X: [
[new Mesh(new CylinderBufferGeometry(0.2, 0, 0.8, 4, 1, false), matInvisible), [0.5, 0, 0], [0, 0, -Math.PI / 2]]
],
Y: [
[new Mesh(new CylinderBufferGeometry(0.2, 0, 0.8, 4, 1, false), matInvisible), [0, 0.5, 0]]
],
Z: [
[new Mesh(new CylinderBufferGeometry(0.2, 0, 0.8, 4, 1, false), matInvisible), [0, 0, 0.5], [Math.PI / 2, 0, 0]]
],
XY: [
[new Mesh(scaleHandleGeometry, matInvisible), [0.85, 0.85, 0], null, [3, 3, 0.2]],
],
YZ: [
[new Mesh(scaleHandleGeometry, matInvisible), [0, 0.85, 0.85], null, [0.2, 3, 3]],
],
XZ: [
[new Mesh(scaleHandleGeometry, matInvisible), [0.85, 0, 0.85], null, [3, 0.2, 3]],
],
XYZX: [
[new Mesh(new BoxBufferGeometry(0.2, 0.2, 0.2), matInvisible), [1.1, 0, 0]],
],
XYZY: [
[new Mesh(new BoxBufferGeometry(0.2, 0.2, 0.2), matInvisible), [0, 1.1, 0]],
],
XYZZ: [
[new Mesh(new BoxBufferGeometry(0.2, 0.2, 0.2), matInvisible), [0, 0, 1.1]],
]
};
var helperScale = {
X: [
[new Line(lineGeometry, matHelper.clone()), [-1e3, 0, 0], null, [1e6, 1, 1], 'helper']
],
Y: [
[new Line(lineGeometry, matHelper.clone()), [0, -1e3, 0], [0, 0, Math.PI / 2], [1e6, 1, 1], 'helper']
],
Z: [
[new Line(lineGeometry, matHelper.clone()), [0, 0, -1e3], [0, -Math.PI / 2, 0], [1e6, 1, 1], 'helper']
]
};
// Creates an Object3D with gizmos described in custom hierarchy definition.
var setupGizmo = function (gizmoMap) {
var gizmo = new Object3D();
for (var name in gizmoMap) {
for (var i = gizmoMap[name].length; i--;) {
var object = gizmoMap[name][i][0].clone();
var position = gizmoMap[name][i][1];
var rotation = gizmoMap[name][i][2];
var scale = gizmoMap[name][i][3];
var tag = gizmoMap[name][i][4];
// name and tag properties are essential for picking and updating logic.
object.name = name;
object.tag = tag;
if (position) {
object.position.set(position[0], position[1], position[2]);
}
if (rotation) {
object.rotation.set(rotation[0], rotation[1], rotation[2]);
}
if (scale) {
object.scale.set(scale[0], scale[1], scale[2]);
}
object.updateMatrix();
var tempGeometry = object.geometry.clone();
tempGeometry.applyMatrix4(object.matrix);
object.geometry = tempGeometry;
object.position.set(0, 0, 0);
object.rotation.set(0, 0, 0);
object.scale.set(1, 1, 1);
gizmo.add(object);
}
}
return gizmo;
};
// Reusable utility variables
var tempVector = new Vector3(0, 0, 0);
var tempEuler = new Euler();
var alignVector = new Vector3(0, 1, 0);
var zeroVector = new Vector3(0, 0, 0);
var lookAtMatrix = new Matrix4();
var tempQuaternion = new Quaternion();
var tempQuaternion2 = new Quaternion();
var identityQuaternion = new Quaternion();
var unitX = new Vector3(1, 0, 0);
var unitY = new Vector3(0, 1, 0);
var unitZ = new Vector3(0, 0, 1);
// Gizmo creation
this.gizmo = {};
this.picker = {};
this.helper = {};
this.add(this.gizmo["translate"] = setupGizmo(gizmoTranslate));
this.add(this.gizmo["rotate"] = setupGizmo(gizmoRotate));
this.add(this.gizmo["scale"] = setupGizmo(gizmoScale));
this.add(this.picker["translate"] = setupGizmo(pickerTranslate));
this.add(this.picker["rotate"] = setupGizmo(pickerRotate));
this.add(this.picker["scale"] = setupGizmo(pickerScale));
this.add(this.helper["translate"] = setupGizmo(helperTranslate));
this.add(this.helper["rotate"] = setupGizmo(helperRotate));
this.add(this.helper["scale"] = setupGizmo(helperScale));
// Pickers should be hidden always
this.picker["translate"].visible = false;
this.picker["rotate"].visible = false;
this.picker["scale"].visible = false;
// updateMatrixWorld will update transformations and appearance of individual handles
this.updateMatrixWorld = function () {
var space = this.space;
if (this.mode === 'scale') space = 'local'; // scale always oriented to local rotation
var quaternion = space === "local" ? this.worldQuaternion : identityQuaternion;
// Show only gizmos for current transform mode
this.gizmo["translate"].visible = this.mode === "translate";
this.gizmo["rotate"].visible = this.mode === "rotate";
this.gizmo["scale"].visible = this.mode === "scale";
this.helper["translate"].visible = this.mode === "translate";
this.helper["rotate"].visible = this.mode === "rotate";
this.helper["scale"].visible = this.mode === "scale";
var handles = [];
handles = handles.concat(this.picker[this.mode].children);
handles = handles.concat(this.gizmo[this.mode].children);
handles = handles.concat(this.helper[this.mode].children);
for (var i = 0; i < handles.length; i++) {
var handle = handles[i];
// hide aligned to camera
handle.visible = true;
handle.rotation.set(0, 0, 0);
handle.position.copy(this.worldPosition);
var eyeDistance = this.worldPosition.distanceTo(this.cameraPosition);
handle.scale.set(1, 1, 1).multiplyScalar(eyeDistance * this.size / 7);
// TODO: simplify helpers and consider decoupling from gizmo
if (handle.tag === 'helper') {
handle.visible = false;
if (handle.name === 'AXIS') {
handle.position.copy(this.worldPositionStart);
handle.visible = !!this.axis;
if (this.axis === 'X') {
tempQuaternion.setFromEuler(tempEuler.set(0, 0, 0));
handle.quaternion.copy(quaternion).multiply(tempQuaternion);
if (Math.abs(alignVector.copy(unitX).applyQuaternion(quaternion).dot(this.eye)) > 0.9) {
handle.visible = false;
}
}
if (this.axis === 'Y') {
tempQuaternion.setFromEuler(tempEuler.set(0, 0, Math.PI / 2));
handle.quaternion.copy(quaternion).multiply(tempQuaternion);
if (Math.abs(alignVector.copy(unitY).applyQuaternion(quaternion).dot(this.eye)) > 0.9) {
handle.visible = false;
}
}
if (this.axis === 'Z') {
tempQuaternion.setFromEuler(tempEuler.set(0, Math.PI / 2, 0));
handle.quaternion.copy(quaternion).multiply(tempQuaternion);
if (Math.abs(alignVector.copy(unitZ).applyQuaternion(quaternion).dot(this.eye)) > 0.9) {
handle.visible = false;
}
}
if (this.axis === 'XYZE') {
tempQuaternion.setFromEuler(tempEuler.set(0, Math.PI / 2, 0));
alignVector.copy(this.rotationAxis);
handle.quaternion.setFromRotationMatrix(lookAtMatrix.lookAt(zeroVector, alignVector, unitY));
handle.quaternion.multiply(tempQuaternion);
handle.visible = this.dragging;
}
if (this.axis === 'E') {
handle.visible = false;
}
} else if (handle.name === 'START') {
handle.position.copy(this.worldPositionStart);
handle.visible = this.dragging;
} else if (handle.name === 'END') {
handle.position.copy(this.worldPosition);
handle.visible = this.dragging;
} else if (handle.name === 'DELTA') {
handle.position.copy(this.worldPositionStart);
handle.quaternion.copy(this.worldQuaternionStart);
tempVector.set(1e-10, 1e-10, 1e-10).add(this.worldPositionStart).sub(this.worldPosition).multiplyScalar(-1);
tempVector.applyQuaternion(this.worldQuaternionStart.clone().inverse());
handle.scale.copy(tempVector);
handle.visible = this.dragging;
} else {
handle.quaternion.copy(quaternion);
if (this.dragging) {
handle.position.copy(this.worldPositionStart);
} else {
handle.position.copy(this.worldPosition);
}
if (this.axis) {
handle.visible = this.axis.search(handle.name) !== -1;
}
}
// If updating helper, skip rest of the loop
continue;
}
// Align handles to current local or world rotation
handle.quaternion.copy(quaternion);
if (this.mode === 'translate' || this.mode === 'scale') {
// Hide translate and scale axis facing the camera
var AXIS_HIDE_TRESHOLD = 0.99;
var PLANE_HIDE_TRESHOLD = 0.2;
var AXIS_FLIP_TRESHOLD = -0.4;
if (handle.name === 'X' || handle.name === 'XYZX') {
if (Math.abs(alignVector.copy(unitX).applyQuaternion(quaternion).dot(this.eye)) > AXIS_HIDE_TRESHOLD) {
handle.scale.set(1e-10, 1e-10, 1e-10);
handle.visible = false;
}
}
if (handle.name === 'Y' || handle.name === 'XYZY') {
if (Math.abs(alignVector.copy(unitY).applyQuaternion(quaternion).dot(this.eye)) > AXIS_HIDE_TRESHOLD) {
handle.scale.set(1e-10, 1e-10, 1e-10);
handle.visible = false;
}
}
if (handle.name === 'Z' || handle.name === 'XYZZ') {
if (Math.abs(alignVector.copy(unitZ).applyQuaternion(quaternion).dot(this.eye)) > AXIS_HIDE_TRESHOLD) {
handle.scale.set(1e-10, 1e-10, 1e-10);
handle.visible = false;
}
}
if (handle.name === 'XY') {
if (Math.abs(alignVector.copy(unitZ).applyQuaternion(quaternion).dot(this.eye)) < PLANE_HIDE_TRESHOLD) {
handle.scale.set(1e-10, 1e-10, 1e-10);
handle.visible = false;
}
}
if (handle.name === 'YZ') {
if (Math.abs(alignVector.copy(unitX).applyQuaternion(quaternion).dot(this.eye)) < PLANE_HIDE_TRESHOLD) {
handle.scale.set(1e-10, 1e-10, 1e-10);
handle.visible = false;
}
}
if (handle.name === 'XZ') {
if (Math.abs(alignVector.copy(unitY).applyQuaternion(quaternion).dot(this.eye)) < PLANE_HIDE_TRESHOLD) {
handle.scale.set(1e-10, 1e-10, 1e-10);
handle.visible = false;
}
}
// Flip translate and scale axis ocluded behind another axis
if (handle.name.search('X') !== -1) {
if (alignVector.copy(unitX).applyQuaternion(quaternion).dot(this.eye) < AXIS_FLIP_TRESHOLD) {
if (handle.tag === 'fwd') {
handle.visible = false;
} else {
handle.scale.x *= -1;
}
} else if (handle.tag === 'bwd') {
handle.visible = false;
}
}
if (handle.name.search('Y') !== -1) {
if (alignVector.copy(unitY).applyQuaternion(quaternion).dot(this.eye) < AXIS_FLIP_TRESHOLD) {
if (handle.tag === 'fwd') {
handle.visible = false;
} else {
handle.scale.y *= -1;
}
} else if (handle.tag === 'bwd') {
handle.visible = false;
}
}
if (handle.name.search('Z') !== -1) {
if (alignVector.copy(unitZ).applyQuaternion(quaternion).dot(this.eye) < AXIS_FLIP_TRESHOLD) {
if (handle.tag === 'fwd') {
handle.visible = false;
} else {
handle.scale.z *= -1;
}
} else if (handle.tag === 'bwd') {
handle.visible = false;
}
}
} else if (this.mode === 'rotate') {
// Align handles to current local or world rotation
tempQuaternion2.copy(quaternion);
tempQuaternion.copy(quaternion);
tempQuaternion.invert();
alignVector.copy(this.eye).applyQuaternion(tempQuaternion);
if (handle.name.search("E") !== -1) {
handle.quaternion.setFromRotationMatrix(lookAtMatrix.lookAt(this.eye, zeroVector, unitY));
}
if (handle.name === 'X') {
tempQuaternion.setFromAxisAngle(unitX, Math.atan2(-alignVector.y, alignVector.z));
tempQuaternion.multiplyQuaternions(tempQuaternion2, tempQuaternion);
handle.quaternion.copy(tempQuaternion);
}
if (handle.name === 'Y') {
tempQuaternion.setFromAxisAngle(unitY, Math.atan2(alignVector.x, alignVector.z));
tempQuaternion.multiplyQuaternions(tempQuaternion2, tempQuaternion);
handle.quaternion.copy(tempQuaternion);
}
if (handle.name === 'Z') {
tempQuaternion.setFromAxisAngle(unitZ, Math.atan2(alignVector.y, alignVector.x));
tempQuaternion.multiplyQuaternions(tempQuaternion2, tempQuaternion);
handle.quaternion.copy(tempQuaternion);
}
}
// highlight selected axis
handle.material._opacity = handle.material._opacity || handle.material.opacity;
handle.material._color = handle.material._color || handle.material.color.clone();
handle.material.color.copy(handle.material._color);
handle.material.opacity = handle.material._opacity;
if (this.axis) {
if (handle.name === this.axis) {
handle.material.opacity = 1.0;
handle.material.color.lerp(new Color(1, 1, 1), 0.5);
} else if (this.axis.split('').some(function (a) {
return handle.name === a;
})) {
handle.material.opacity = 1.0;
handle.material.color.lerp(new Color(1, 1, 1), 0.5);
} else {
handle.material.opacity *= 0.05;
}
}
}
Object3D.prototype.updateMatrixWorld.call(this);
};
};
TransformControlsGizmo.prototype = Object.assign(Object.create(Object3D.prototype), {
constructor: TransformControlsGizmo,
isTransformControlsGizmo: true
});
const TransformControlsPlane = function () {
'use strict';
Mesh.call(this,
new PlaneBufferGeometry(100000, 100000, 2, 2),
new MeshBasicMaterial({
visible: false,
wireframe: true,
side: DoubleSide,
transparent: true,
opacity: 0.1
})
);
this.type = 'TransformControlsPlane';
var unitX = new Vector3(1, 0, 0);
var unitY = new Vector3(0, 1, 0);
var unitZ = new Vector3(0, 0, 1);
var tempVector = new Vector3();
var dirVector = new Vector3();
var alignVector = new Vector3();
var tempMatrix = new Matrix4();
var camRotation = new Euler();
var identityQuaternion = new Quaternion();
this.updateMatrixWorld = function () {
var space = this.space;
this.position.copy(this.worldPosition);
if (this.mode === 'scale') space = 'local'; // scale always oriented to local rotation
unitX.set(1, 0, 0).applyQuaternion(space === "local" ? this.worldQuaternion : identityQuaternion);
unitY.set(0, 1, 0).applyQuaternion(space === "local" ? this.worldQuaternion : identityQuaternion);
unitZ.set(0, 0, 1).applyQuaternion(space === "local" ? this.worldQuaternion : identityQuaternion);
// Align the plane for current transform mode, axis and space.
alignVector.copy(unitY);
switch (this.mode) {
case 'translate':
case 'scale':
switch (this.axis) {
case 'X':
alignVector.copy(this.eye).cross(unitX);
dirVector.copy(unitX).cross(alignVector);
break;
case 'Y':
alignVector.copy(this.eye).cross(unitY);
dirVector.copy(unitY).cross(alignVector);
break;
case 'Z':
alignVector.copy(this.eye).cross(unitZ);
dirVector.copy(unitZ).cross(alignVector);
break;
case 'XY':
dirVector.copy(unitZ);
break;
case 'YZ':
dirVector.copy(unitX);
break;
case 'XZ':
alignVector.copy(unitZ);
dirVector.copy(unitY);