diff --git a/web/app/3d/main.js b/web/app/3d/main.js index 76e67660..5119c6df 100644 --- a/web/app/3d/main.js +++ b/web/app/3d/main.js @@ -42,6 +42,9 @@ TCAD.App = function() { } } + this.bus.subscribe("craft", function() { + app._refreshSketches(); + }); window.addEventListener('storage', storage_handler, false); }; diff --git a/web/app/3d/viewer.js b/web/app/3d/viewer.js index 8198f9dc..6a08f80e 100644 --- a/web/app/3d/viewer.js +++ b/web/app/3d/viewer.js @@ -51,26 +51,46 @@ TCAD.Viewer = function(bus) { **/ // controls = new THREE.OrbitControls( camera , renderer.domElement); - var controls = new THREE.TrackballControls( camera , renderer.domElement); + var trackballControls = new THREE.TrackballControls( camera , renderer.domElement); // document.addEventListener( 'mousemove', function(){ // controls.update(); // }, false ); - controls.rotateSpeed = 3.8; - controls.zoomSpeed = 1.2; - controls.panSpeed = 0.8; + trackballControls.rotateSpeed = 3.8; + trackballControls.zoomSpeed = 1.2; + trackballControls.panSpeed = 0.8; - controls.noZoom = false; - controls.noPan = false; + trackballControls.noZoom = false; + trackballControls.noPan = false; - controls.staticMoving = true; - controls.dynamicDampingFactor = 0.3; + trackballControls.staticMoving = true; + trackballControls.dynamicDampingFactor = 0.3; - controls.keys = [ 65, 83, 68 ]; - controls.addEventListener( 'change', render ); - this.controls = controls; + trackballControls.keys = [ 65, 83, 68 ]; + trackballControls.addEventListener( 'change', render ); + this.trackballControls = trackballControls; + + var transformControls = new THREE.TransformControls( camera, renderer.domElement ); + transformControls.addEventListener( 'change', render ); + scene.add( transformControls ); + this.transformControls = transformControls; + + function updateTransformControls() { + if (transformControls.object !== undefined) { + if (transformControls.object.parent === undefined) { + transformControls.detach(); + render(); + } + transformControls.update(); + } + } + + function updateControlsAndHelpers() { + trackballControls.update(); + updateTransformControls(); + } /** * TOOLS @@ -110,9 +130,15 @@ TCAD.Viewer = function(bus) { if (scope.selectionMgr.contains(poly)) { scope.toolMgr.handleClick(poly, pickResult); } else { - scope.select(poly); - pickResult.object.geometry.colorsNeedUpdate = true; + if (e.shiftKey) { + scope.transformControls.attach(pickResult.object); + } else { + scope.select(poly); + pickResult.object.geometry.colorsNeedUpdate = true; + } } + } else { + scope.transformControls.detach(); } render(); } @@ -148,7 +174,7 @@ TCAD.Viewer = function(bus) { function animate() { // console.log("animate"); requestAnimationFrame( animate ); - controls.update(); + updateControlsAndHelpers(); } render(); diff --git a/web/app/workbench.js b/web/app/workbench.js index 0a576b14..0657ccb0 100644 --- a/web/app/workbench.js +++ b/web/app/workbench.js @@ -842,21 +842,23 @@ TCAD.craft.cut = function(app, request) { var sketchedPolygons = TCAD.craft.getSketchedPolygons3D(app, face); if (sketchedPolygons == null) return null; - //face.c.__face = undefined; - var work = request.solids[0].csg; - var normal = TCAD.utils.vec(face.csgGroup.plane.normal); var cutter = []; for (var i = 0; i < sketchedPolygons.length; i++) { var extruded = TCAD.geom.extrude(sketchedPolygons[i], normal.multiply( - request.depth)); cutter = cutter.concat(TCAD.craft._makeFromPolygons(extruded)); } + var cutterCSG = CSG.fromPolygons(cutter); - - var cut = work.subtract(CSG.fromPolygons(cutter)); - - var solid = TCAD.utils.createSolidMesh(cut).geometry; - return solid; + //face.c.__face = undefined; + var outSolids = []; + for (var si = 0; si < request.solids.length; si++) { + var work = request.solids[si].csg; + var cut = work.subtract(cutterCSG); + var solidMesh = TCAD.utils.createSolidMesh(cut); + outSolids.push(solidMesh.geometry); + } + return outSolids; }; TCAD.Craft = function(app) { @@ -889,18 +891,18 @@ TCAD.Craft.prototype.modify = function(request) { if (!op) return; var detachedRequest = TCAD.craft.detach(request); - var newGroups = op(this.app, request); + var newSolids = op(this.app, request); - if (newGroups == null) return; - for (var i = 0; i < request.solids.length; i++) { - var solid = request.solids[i]; - this.app.viewer.scene.remove( solid.meshObject ); + if (newSolids == null) return; + var i; + for (i = 0; i < request.solids.length; i++) { + this.app.viewer.scene.remove( request.solids[i].meshObject ); + } + for (i = 0; i < newSolids.length; i++) { + this.app.viewer.scene.add(newSolids[i].meshObject); } - this.app.viewer.scene.add(newGroups.meshObject); this.history.push(detachedRequest); this.app.bus.notify('craft'); - //REMOVE IT - this.app._refreshSketches(); this.app.viewer.render(); }; @@ -909,6 +911,6 @@ TCAD.craft.OPS = { CUT : TCAD.craft.cut, PAD : TCAD.craft.extrude, BOX : function(app, request) { - return TCAD.utils.createCSGBox(request.size).geometry; + return [TCAD.utils.createCSGBox(request.size).geometry]; } }; diff --git a/web/index.html b/web/index.html index ce488e0f..17119553 100644 --- a/web/index.html +++ b/web/index.html @@ -13,8 +13,10 @@ + + diff --git a/web/lib/three/TransformControls.js b/web/lib/three/TransformControls.js new file mode 100644 index 00000000..6a513443 --- /dev/null +++ b/web/lib/three/TransformControls.js @@ -0,0 +1,981 @@ +/** + * @author arodic / https://github.com/arodic + */ +/*jshint sub:true*/ + +(function () { + + 'use strict'; + + var GizmoMaterial = function ( parameters ) { + + THREE.MeshBasicMaterial.call( this ); + + this.depthTest = false; + this.depthWrite = 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 ); + + var GizmoLineMaterial = function ( parameters ) { + + THREE.LineBasicMaterial.call( this ); + + this.depthTest = false; + this.depthWrite = 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 ); + + THREE.TransformGizmo = function () { + + var scope = this; + var showPickers = false; //debug + var showActivePlane = false; //debug + + 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.PlaneGeometry( 50, 50, 2, 2 ); + var planeMaterial = new THREE.MeshBasicMaterial( { wireframe: true } ); + planeMaterial.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]; + planes[i].visible = false; + } + + //// 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; + + 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 = new THREE.Geometry(); + tempGeometry.merge( child.geometry, child.matrix ); + + child.geometry = tempGeometry; + child.position.set( 0, 0, 0 ); + child.rotation.set( 0, 0, 0 ); + child.scale.set( 1, 1, 1 ); + } + }); + + }; + + this.hide = function () { + this.traverse(function( child ) { + child.visible = false; + }); + }; + + this.show = function () { + this.traverse(function( child ) { + child.visible = true; + if (child.parent == scope.pickers ) child.visible = showPickers; + if (child.parent == scope.planes ) child.visible = false; + }); + this.activePlane.visible = showActivePlane; + }; + + 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.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.Geometry(); + var mesh = new THREE.Mesh( new THREE.CylinderGeometry( 0, 0.05, 0.2, 12, 1, false ) ); + mesh.position.y = 0.5; + mesh.updateMatrix(); + + arrowGeometry.merge( mesh.geometry, mesh.matrix ); + + var lineXGeometry = new THREE.Geometry(); + lineXGeometry.vertices.push( new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( 1, 0, 0 ) ); + + var lineYGeometry = new THREE.Geometry(); + lineYGeometry.vertices.push( new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( 0, 1, 0 ) ); + + var lineZGeometry = new THREE.Geometry(); + lineZGeometry.vertices.push( new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( 0, 0, 1 ) ); + + 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.PlaneGeometry( 0.29, 0.29 ), new GizmoMaterial( { color: 0xffff00, opacity: 0.25 } ) ), [ 0.15, 0.15, 0 ] ] + ], + YZ: [ + [ new THREE.Mesh( new THREE.PlaneGeometry( 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.PlaneGeometry( 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.CylinderGeometry( 0.2, 0, 1, 4, 1, false ), new GizmoMaterial( { color: 0xff0000, opacity: 0.25 } ) ), [ 0.6, 0, 0 ], [ 0, 0, -Math.PI/2 ] ] + ], + Y: [ + [ new THREE.Mesh( new THREE.CylinderGeometry( 0.2, 0, 1, 4, 1, false ), new GizmoMaterial( { color: 0x00ff00, opacity: 0.25 } ) ), [ 0, 0.6, 0 ] ] + ], + Z: [ + [ new THREE.Mesh( new THREE.CylinderGeometry( 0.2, 0, 1, 4, 1, false ), new GizmoMaterial( { color: 0x0000ff, opacity: 0.25 } ) ), [ 0, 0, 0.6 ], [ Math.PI/2, 0, 0 ] ] + ], + XYZ: [ + [ new THREE.Mesh( new THREE.OctahedronGeometry( 0.2, 0 ), new GizmoMaterial( { color: 0xffffff, opacity: 0.25 } ) ) ] + ], + XY: [ + [ new THREE.Mesh( new THREE.PlaneGeometry( 0.4, 0.4 ), new GizmoMaterial( { color: 0xffff00, opacity: 0.25 } ) ), [ 0.2, 0.2, 0 ] ] + ], + YZ: [ + [ new THREE.Mesh( new THREE.PlaneGeometry( 0.4, 0.4 ), new GizmoMaterial( { color: 0x00ffff, opacity: 0.25 } ) ), [ 0, 0.2, 0.2 ], [ 0, Math.PI/2, 0 ] ] + ], + XZ: [ + [ new THREE.Mesh( new THREE.PlaneGeometry( 0.4, 0.4 ), new GizmoMaterial( { color: 0xff00ff, opacity: 0.25 } ) ), [ 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.hide(); + this.show(); + + }; + + this.init(); + + }; + + THREE.TransformGizmoTranslate.prototype = Object.create( THREE.TransformGizmo.prototype ); + + THREE.TransformGizmoRotate = function () { + + THREE.TransformGizmo.call( this ); + + var CircleGeometry = function ( radius, facing, arc ) { + + var geometry = new THREE.Geometry(); + arc = arc ? arc : 1; + for ( var i = 0; i <= 64 * arc; ++i ) { + if ( facing == 'x' ) geometry.vertices.push( new THREE.Vector3( 0, Math.cos( i / 32 * Math.PI ), Math.sin( i / 32 * Math.PI ) ).multiplyScalar(radius) ); + if ( facing == 'y' ) geometry.vertices.push( new THREE.Vector3( Math.cos( i / 32 * Math.PI ), 0, Math.sin( i / 32 * Math.PI ) ).multiplyScalar(radius) ); + if ( facing == 'z' ) geometry.vertices.push( new THREE.Vector3( Math.sin( i / 32 * Math.PI ), Math.cos( i / 32 * Math.PI ), 0 ).multiplyScalar(radius) ); + } + + 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.TorusGeometry( 1, 0.12, 4, 12, Math.PI ), new GizmoMaterial( { color: 0xff0000, opacity: 0.25 } ) ), [ 0, 0, 0 ], [ 0, -Math.PI/2, -Math.PI/2 ] ] + ], + Y: [ + [ new THREE.Mesh( new THREE.TorusGeometry( 1, 0.12, 4, 12, Math.PI ), new GizmoMaterial( { color: 0x00ff00, opacity: 0.25 } ) ), [ 0, 0, 0 ], [ Math.PI/2, 0, 0 ] ] + ], + Z: [ + [ new THREE.Mesh( new THREE.TorusGeometry( 1, 0.12, 4, 12, Math.PI ), new GizmoMaterial( { color: 0x0000ff, opacity: 0.25 } ) ), [ 0, 0, 0 ], [ 0, 0, -Math.PI/2 ] ] + ], + E: [ + [ new THREE.Mesh( new THREE.TorusGeometry( 1.25, 0.12, 2, 24 ), new GizmoMaterial( { color: 0xffff00, opacity: 0.25 } ) ) ] + ], + XYZE: [ + [ new THREE.Mesh( new THREE.Geometry() ) ]// TODO + ] + }; + + 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.hide(); + this.show(); + + }; + + this.update = function ( rotation, eye2 ) { + + THREE.TransformGizmo.prototype.update.apply( this, arguments ); + + var group = { + handles: this["handles"], + pickers: this["pickers"], + }; + + 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.TransformGizmoScale = function () { + + THREE.TransformGizmo.call( this ); + + var arrowGeometry = new THREE.Geometry(); + var mesh = new THREE.Mesh( new THREE.BoxGeometry( 0.125, 0.125, 0.125 ) ); + mesh.position.y = 0.5; + mesh.updateMatrix(); + + arrowGeometry.merge( mesh.geometry, mesh.matrix ); + + var lineXGeometry = new THREE.Geometry(); + lineXGeometry.vertices.push( new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( 1, 0, 0 ) ); + + var lineYGeometry = new THREE.Geometry(); + lineYGeometry.vertices.push( new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( 0, 1, 0 ) ); + + var lineZGeometry = new THREE.Geometry(); + lineZGeometry.vertices.push( new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( 0, 0, 1 ) ); + + 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.BoxGeometry( 0.125, 0.125, 0.125 ), new GizmoMaterial( { color: 0xffffff, opacity: 0.25 } ) ) ] + ] + }; + + this.pickerGizmos = { + X: [ + [ new THREE.Mesh( new THREE.CylinderGeometry( 0.2, 0, 1, 4, 1, false ), new GizmoMaterial( { color: 0xff0000, opacity: 0.25 } ) ), [ 0.6, 0, 0 ], [ 0, 0, -Math.PI/2 ] ] + ], + Y: [ + [ new THREE.Mesh( new THREE.CylinderGeometry( 0.2, 0, 1, 4, 1, false ), new GizmoMaterial( { color: 0x00ff00, opacity: 0.25 } ) ), [ 0, 0.6, 0 ] ] + ], + Z: [ + [ new THREE.Mesh( new THREE.CylinderGeometry( 0.2, 0, 1, 4, 1, false ), new GizmoMaterial( { color: 0x0000ff, opacity: 0.25 } ) ), [ 0, 0, 0.6 ], [ Math.PI/2, 0, 0 ] ] + ], + XYZ: [ + [ new THREE.Mesh( new THREE.BoxGeometry( 0.4, 0.4, 0.4 ), new GizmoMaterial( { color: 0xffffff, opacity: 0.25 } ) ) ] + ] + }; + + 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.hide(); + this.show(); + + }; + + this.init(); + + }; + + THREE.TransformGizmoScale.prototype = Object.create( THREE.TransformGizmo.prototype ); + + 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.gizmo = {}; + this.gizmo["translate"] = new THREE.TransformGizmoTranslate(); + this.gizmo["rotate"] = new THREE.TransformGizmoRotate(); + this.gizmo["scale"] = new THREE.TransformGizmoScale(); + + this.add(this.gizmo["translate"]); + this.add(this.gizmo["rotate"]); + this.add(this.gizmo["scale"]); + + this.gizmo["translate"].hide(); + this.gizmo["rotate"].hide(); + this.gizmo["scale"].hide(); + + this.object = undefined; + this.snap = null; + this.space = "world"; + this.size = 1; + this.axis = null; + + var scope = this; + + var _dragging = false; + var _mode = "translate"; + var _plane = "XY"; + + var changeEvent = { type: "change" }; + + var ray = new THREE.Raycaster(); + var projector = new THREE.Projector(); + var pointerVector = new THREE.Vector3(); + + 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.attach = function ( object ) { + + scope.object = object; + + this.gizmo["translate"].hide(); + this.gizmo["rotate"].hide(); + this.gizmo["scale"].hide(); + this.gizmo[_mode].show(); + + scope.update(); + + }; + + this.detach = function ( object ) { + + scope.object = undefined; + this.axis = undefined; + + this.gizmo["translate"].hide(); + this.gizmo["rotate"].hide(); + this.gizmo["scale"].hide(); + + }; + + this.setMode = function ( mode ) { + + _mode = mode ? mode : _mode; + + if ( _mode == "scale" ) scope.space = "local"; + + this.gizmo["translate"].hide(); + this.gizmo["rotate"].hide(); + this.gizmo["scale"].hide(); + this.gizmo[_mode].show(); + + this.update(); + scope.dispatchEvent( changeEvent ); + + }; + + this.setSnap = function ( snap ) { + + scope.snap = snap; + + }; + + 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 ); + + eye.copy( camPosition ).sub( worldPosition ).normalize(); + + if ( scope.space == "local" ) + this.gizmo[_mode].update( worldRotation, eye ); + + else if ( scope.space == "world" ) + this.gizmo[_mode].update( new THREE.Euler(), eye ); + + this.gizmo[_mode].highlight( scope.axis ); + + }; + + function onPointerHover( event ) { + + if ( scope.object === undefined || _dragging === true ) return; + + event.preventDefault(); + + var pointer = event.changedTouches ? event.changedTouches[ 0 ] : event; + + var intersect = intersectObjects( pointer, scope.gizmo[_mode].pickers.children ); + + if ( intersect ) { + + scope.axis = intersect.object.name; + scope.update(); + scope.dispatchEvent( changeEvent ); + + } else if ( scope.axis !== null ) { + + scope.axis = null; + scope.update(); + scope.dispatchEvent( changeEvent ); + + } + + } + + function onPointerDown( event ) { + + if ( scope.object === undefined || _dragging === true ) return; + + event.preventDefault(); + event.stopPropagation(); + + var pointer = event.changedTouches ? event.changedTouches[ 0 ] : event; + + if ( pointer.button === 0 || pointer.button === undefined ) { + + var intersect = intersectObjects( pointer, scope.gizmo[_mode].pickers.children ); + + if ( intersect ) { + + scope.axis = intersect.object.name; + + scope.update(); + + eye.copy( camPosition ).sub( worldPosition ).normalize(); + + scope.gizmo[_mode].setActivePlane( scope.axis, eye ); + + var planeIntersect = intersectObjects( pointer, [scope.gizmo[_mode].activePlane] ); + + 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 ) return; + + event.preventDefault(); + event.stopPropagation(); + + var pointer = event.changedTouches? event.changedTouches[0] : event; + + var planeIntersect = intersectObjects( pointer, [scope.gizmo[_mode].activePlane] ); + + 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.snap !== null ) { + + if ( scope.axis.search("X") != -1 ) scope.object.position.x = Math.round( scope.object.position.x / scope.snap ) * scope.snap; + if ( scope.axis.search("Y") != -1 ) scope.object.position.y = Math.round( scope.object.position.y / scope.snap ) * scope.snap; + if ( scope.axis.search("Z") != -1 ) scope.object.position.z = Math.round( scope.object.position.z / scope.snap ) * scope.snap; + + } + + } else if ( _mode == "scale" ) { + + point.sub( offset ); + point.multiply(parentScale); + + if ( scope.space == "local" ) { + + if ( scope.axis == "XYZ") { + + scale = 1 + ( ( point.y ) / 50 ); + + 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 / 50 ); + if ( scope.axis == "Y" ) scope.object.scale.y = oldScale.y * ( 1 + point.y / 50 ); + if ( scope.axis == "Z" ) scope.object.scale.z = oldScale.z * ( 1 + point.z / 50 ); + + } + + } + + } 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 ); + 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 ) ); + + 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 ); + + } + + function onPointerUp( event ) { + + _dragging = false; + 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, 0.5 ); + + projector.unprojectVector( pointerVector, camera ); + ray.set( camPosition, pointerVector.sub( camPosition ).normalize() ); + + var intersections = ray.intersectObjects( objects, true ); + return intersections[0] ? intersections[0] : false; + + } + + }; + + THREE.TransformControls.prototype = Object.create( THREE.Object3D.prototype ); + +}()); \ No newline at end of file