├── .gitignore ├── examples ├── textures │ ├── concrete-wall.png │ └── window-texture.jpg ├── models │ └── Colossal_Rumbling_Builder.stl ├── js │ ├── Capsule.js │ ├── OctreeCSG │ │ ├── OctreeCSG.worker.js │ │ ├── OctreeCSG.extended.js │ │ └── three-triangle-intersection.js │ ├── stats.module.js │ ├── STLExporter.js │ ├── TessellateModifier.js │ ├── STLLoader.js │ ├── lil-gui.module.min.js │ └── OrbitControls.js ├── basic.html └── realtime1.html ├── LICENSE ├── OctreeCSG ├── OctreeCSG.worker.js ├── OctreeCSG.extended.js └── three-triangle-intersection.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /examples/textures/concrete-wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/OctreeCSG/HEAD/examples/textures/concrete-wall.png -------------------------------------------------------------------------------- /examples/textures/window-texture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/OctreeCSG/HEAD/examples/textures/window-texture.jpg -------------------------------------------------------------------------------- /examples/models/Colossal_Rumbling_Builder.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/OctreeCSG/HEAD/examples/models/Colossal_Rumbling_Builder.stl -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 OctreeCSG Authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/js/Capsule.js: -------------------------------------------------------------------------------- 1 | import { 2 | Vector3 3 | } from 'three'; 4 | 5 | const _v1 = new Vector3(); 6 | const _v2 = new Vector3(); 7 | const _v3 = new Vector3(); 8 | 9 | const EPS = 1e-10; 10 | 11 | class Capsule { 12 | 13 | constructor( start = new Vector3( 0, 0, 0 ), end = new Vector3( 0, 1, 0 ), radius = 1 ) { 14 | 15 | this.start = start; 16 | this.end = end; 17 | this.radius = radius; 18 | 19 | } 20 | 21 | clone() { 22 | 23 | return new Capsule( this.start.clone(), this.end.clone(), this.radius ); 24 | 25 | } 26 | 27 | set( start, end, radius ) { 28 | 29 | this.start.copy( start ); 30 | this.end.copy( end ); 31 | this.radius = radius; 32 | 33 | } 34 | 35 | copy( capsule ) { 36 | 37 | this.start.copy( capsule.start ); 38 | this.end.copy( capsule.end ); 39 | this.radius = capsule.radius; 40 | 41 | } 42 | 43 | getCenter( target ) { 44 | 45 | return target.copy( this.end ).add( this.start ).multiplyScalar( 0.5 ); 46 | 47 | } 48 | 49 | translate( v ) { 50 | 51 | this.start.add( v ); 52 | this.end.add( v ); 53 | 54 | } 55 | 56 | checkAABBAxis( p1x, p1y, p2x, p2y, minx, maxx, miny, maxy, radius ) { 57 | 58 | return ( 59 | ( minx - p1x < radius || minx - p2x < radius ) && 60 | ( p1x - maxx < radius || p2x - maxx < radius ) && 61 | ( miny - p1y < radius || miny - p2y < radius ) && 62 | ( p1y - maxy < radius || p2y - maxy < radius ) 63 | ); 64 | 65 | } 66 | 67 | intersectsBox( box ) { 68 | 69 | return ( 70 | this.checkAABBAxis( 71 | this.start.x, this.start.y, this.end.x, this.end.y, 72 | box.min.x, box.max.x, box.min.y, box.max.y, 73 | this.radius ) && 74 | this.checkAABBAxis( 75 | this.start.x, this.start.z, this.end.x, this.end.z, 76 | box.min.x, box.max.x, box.min.z, box.max.z, 77 | this.radius ) && 78 | this.checkAABBAxis( 79 | this.start.y, this.start.z, this.end.y, this.end.z, 80 | box.min.y, box.max.y, box.min.z, box.max.z, 81 | this.radius ) 82 | ); 83 | 84 | } 85 | 86 | lineLineMinimumPoints( line1, line2 ) { 87 | 88 | const r = _v1.copy( line1.end ).sub( line1.start ); 89 | const s = _v2.copy( line2.end ).sub( line2.start ); 90 | const w = _v3.copy( line2.start ).sub( line1.start ); 91 | 92 | const a = r.dot( s ), 93 | b = r.dot( r ), 94 | c = s.dot( s ), 95 | d = s.dot( w ), 96 | e = r.dot( w ); 97 | 98 | let t1, t2; 99 | const divisor = b * c - a * a; 100 | 101 | if ( Math.abs( divisor ) < EPS ) { 102 | 103 | const d1 = - d / c; 104 | const d2 = ( a - d ) / c; 105 | 106 | if ( Math.abs( d1 - 0.5 ) < Math.abs( d2 - 0.5 ) ) { 107 | 108 | t1 = 0; 109 | t2 = d1; 110 | 111 | } else { 112 | 113 | t1 = 1; 114 | t2 = d2; 115 | 116 | } 117 | 118 | } else { 119 | 120 | t1 = ( d * a + e * c ) / divisor; 121 | t2 = ( t1 * a - d ) / c; 122 | 123 | } 124 | 125 | t2 = Math.max( 0, Math.min( 1, t2 ) ); 126 | t1 = Math.max( 0, Math.min( 1, t1 ) ); 127 | 128 | const point1 = r.multiplyScalar( t1 ).add( line1.start ); 129 | const point2 = s.multiplyScalar( t2 ).add( line2.start ); 130 | 131 | return [ point1, point2 ]; 132 | 133 | } 134 | 135 | } 136 | 137 | export { Capsule }; 138 | -------------------------------------------------------------------------------- /OctreeCSG/OctreeCSG.worker.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../threejs/three.module.js'; 2 | // console.log("GOT HERE"); 3 | onmessage = function (e) { 4 | // let randLimit = Math.round(Math.random() * 1000000000); 5 | // console.log("[WORKER]", randLimit, e); 6 | // let worker_data = { 7 | // type: 'windingNumber', 8 | // point: point, 9 | // triangles: buffer 10 | // } 11 | const { type, point, coplanar, polygonID, triangles } = e.data; 12 | let trianglesArr = new Float32Array(triangles); 13 | // console.log("[WORKER] Checking Polygon ID:", polygonID, point); 14 | if (type === 'windingNumber') { 15 | postMessage({ 16 | type, 17 | result: polyInside_WindingNumber_buffer(trianglesArr, point, coplanar) 18 | }); 19 | } 20 | else { 21 | let a = 0; 22 | // for (let i = 0; i < randLimit; i++) { 23 | // a++; 24 | // } 25 | postMessage("[From Worker] Aloha " + a); 26 | } 27 | } 28 | 29 | //// 30 | const EPSILON = 1e-5; 31 | // Winding Number algorithm adapted from https://github.com/grame-cncm/faust/blob/master-dev/tools/physicalModeling/mesh2faust/vega/libraries/windingNumber/windingNumber.cpp 32 | const _wV1 = new THREE.Vector3(); 33 | const _wV2 = new THREE.Vector3(); 34 | const _wV3 = new THREE.Vector3(); 35 | const _wP = new THREE.Vector3(); 36 | const _wP_EPS_ARR = [ 37 | new THREE.Vector3(EPSILON, 0, 0), 38 | new THREE.Vector3(0, EPSILON, 0), 39 | new THREE.Vector3(0, 0, EPSILON), 40 | new THREE.Vector3(-EPSILON, 0, 0), 41 | new THREE.Vector3(0, -EPSILON, 0), 42 | new THREE.Vector3(0, 0, -EPSILON) 43 | ]; 44 | const _wP_EPS_ARR_COUNT = _wP_EPS_ARR.length; 45 | const _matrix3 = new THREE.Matrix3(); 46 | const wNPI = 4 * Math.PI; 47 | 48 | // function calcDet(a, b, c) { 49 | // return (-a.z * b.y * c.x + 50 | // a.y * b.z * c.x + 51 | // a.z * b.x * c.y + 52 | // a.x * b.z * c.y + 53 | // a.y * b.x * c.z + 54 | // a.x * b.y * c.z ); 55 | // } 56 | function returnXYZ(arr, index) { 57 | return { x: arr[index], y: arr[index + 1], z: arr[index + 2] }; 58 | } 59 | function calcWindingNumber_buffer(trianglesArr, point) { 60 | let wN = 0; 61 | for (let i = 0; i < trianglesArr.length; i += 9) { 62 | _wV1.subVectors(returnXYZ(trianglesArr, i), point); 63 | _wV2.subVectors(returnXYZ(trianglesArr, i + 3), point); 64 | _wV3.subVectors(returnXYZ(trianglesArr, i + 6), point); 65 | let lenA = _wV1.length(); 66 | let lenB = _wV2.length(); 67 | let lenC = _wV3.length(); 68 | _matrix3.set(_wV1.x, _wV1.y, _wV1.z, _wV2.x, _wV2.y, _wV2.z, _wV3.x, _wV3.y, _wV3.z); 69 | let omega = 2 * Math.atan2(_matrix3.determinant(), (lenA * lenB * lenC + _wV1.dot(_wV2) * lenC + _wV2.dot(_wV3) * lenA + _wV3.dot(_wV1) * lenB)); 70 | wN += omega; 71 | } 72 | wN = Math.round(wN / wNPI); 73 | return wN; 74 | } 75 | function polyInside_WindingNumber_buffer(trianglesArr, point, coplanar) { 76 | let result = false; 77 | _wP.copy(point); 78 | let wN = calcWindingNumber_buffer(trianglesArr, _wP); 79 | let coplanarFound = false; 80 | if (wN === 0) { 81 | if (coplanar) { 82 | // console.log("POLYGON IS COPLANAR"); 83 | for (let j = 0; j < _wP_EPS_ARR_COUNT; j++) { 84 | // console.warn("DOES IT GET HERE?"); 85 | _wP.copy(point).add(_wP_EPS_ARR[j]); 86 | wN = calcWindingNumber_buffer(trianglesArr, _wP); 87 | if (wN !== 0) { 88 | // console.warn("GOT HERE"); 89 | result = true; 90 | coplanarFound = true; 91 | break; 92 | } 93 | } 94 | } 95 | } 96 | else { 97 | result = true; 98 | } 99 | // if (result && polygon.coplanar) { 100 | // console.log(`[polyInside_WindingNumber] coplanar polygon found ${coplanarFound ? "IN" : "NOT IN"} coplanar test`); 101 | // } 102 | 103 | return result; 104 | 105 | } 106 | 107 | // export {}; -------------------------------------------------------------------------------- /examples/js/OctreeCSG/OctreeCSG.worker.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../threejs/three.module.js'; 2 | // console.log("GOT HERE"); 3 | onmessage = function (e) { 4 | // let randLimit = Math.round(Math.random() * 1000000000); 5 | // console.log("[WORKER]", randLimit, e); 6 | // let worker_data = { 7 | // type: 'windingNumber', 8 | // point: point, 9 | // triangles: buffer 10 | // } 11 | const { type, point, coplanar, polygonID, triangles } = e.data; 12 | let trianglesArr = new Float32Array(triangles); 13 | // console.log("[WORKER] Checking Polygon ID:", polygonID, point); 14 | if (type === 'windingNumber') { 15 | postMessage({ 16 | type, 17 | result: polyInside_WindingNumber_buffer(trianglesArr, point, coplanar) 18 | }); 19 | } 20 | else { 21 | let a = 0; 22 | // for (let i = 0; i < randLimit; i++) { 23 | // a++; 24 | // } 25 | postMessage("[From Worker] Aloha " + a); 26 | } 27 | } 28 | 29 | //// 30 | const EPSILON = 1e-5; 31 | // Winding Number algorithm adapted from https://github.com/grame-cncm/faust/blob/master-dev/tools/physicalModeling/mesh2faust/vega/libraries/windingNumber/windingNumber.cpp 32 | const _wV1 = new THREE.Vector3(); 33 | const _wV2 = new THREE.Vector3(); 34 | const _wV3 = new THREE.Vector3(); 35 | const _wP = new THREE.Vector3(); 36 | const _wP_EPS_ARR = [ 37 | new THREE.Vector3(EPSILON, 0, 0), 38 | new THREE.Vector3(0, EPSILON, 0), 39 | new THREE.Vector3(0, 0, EPSILON), 40 | new THREE.Vector3(-EPSILON, 0, 0), 41 | new THREE.Vector3(0, -EPSILON, 0), 42 | new THREE.Vector3(0, 0, -EPSILON) 43 | ]; 44 | const _wP_EPS_ARR_COUNT = _wP_EPS_ARR.length; 45 | const _matrix3 = new THREE.Matrix3(); 46 | const wNPI = 4 * Math.PI; 47 | 48 | // function calcDet(a, b, c) { 49 | // return (-a.z * b.y * c.x + 50 | // a.y * b.z * c.x + 51 | // a.z * b.x * c.y + 52 | // a.x * b.z * c.y + 53 | // a.y * b.x * c.z + 54 | // a.x * b.y * c.z ); 55 | // } 56 | function returnXYZ(arr, index) { 57 | return { x: arr[index], y: arr[index + 1], z: arr[index + 2] }; 58 | } 59 | function calcWindingNumber_buffer(trianglesArr, point) { 60 | let wN = 0; 61 | for (let i = 0; i < trianglesArr.length; i += 9) { 62 | _wV1.subVectors(returnXYZ(trianglesArr, i), point); 63 | _wV2.subVectors(returnXYZ(trianglesArr, i + 3), point); 64 | _wV3.subVectors(returnXYZ(trianglesArr, i + 6), point); 65 | let lenA = _wV1.length(); 66 | let lenB = _wV2.length(); 67 | let lenC = _wV3.length(); 68 | _matrix3.set(_wV1.x, _wV1.y, _wV1.z, _wV2.x, _wV2.y, _wV2.z, _wV3.x, _wV3.y, _wV3.z); 69 | let omega = 2 * Math.atan2(_matrix3.determinant(), (lenA * lenB * lenC + _wV1.dot(_wV2) * lenC + _wV2.dot(_wV3) * lenA + _wV3.dot(_wV1) * lenB)); 70 | wN += omega; 71 | } 72 | wN = Math.round(wN / wNPI); 73 | return wN; 74 | } 75 | function polyInside_WindingNumber_buffer(trianglesArr, point, coplanar) { 76 | let result = false; 77 | _wP.copy(point); 78 | let wN = calcWindingNumber_buffer(trianglesArr, _wP); 79 | let coplanarFound = false; 80 | if (wN === 0) { 81 | if (coplanar) { 82 | // console.log("POLYGON IS COPLANAR"); 83 | for (let j = 0; j < _wP_EPS_ARR_COUNT; j++) { 84 | // console.warn("DOES IT GET HERE?"); 85 | _wP.copy(point).add(_wP_EPS_ARR[j]); 86 | wN = calcWindingNumber_buffer(trianglesArr, _wP); 87 | if (wN !== 0) { 88 | // console.warn("GOT HERE"); 89 | result = true; 90 | coplanarFound = true; 91 | break; 92 | } 93 | } 94 | } 95 | } 96 | else { 97 | result = true; 98 | } 99 | // if (result && polygon.coplanar) { 100 | // console.log(`[polyInside_WindingNumber] coplanar polygon found ${coplanarFound ? "IN" : "NOT IN"} coplanar test`); 101 | // } 102 | 103 | return result; 104 | 105 | } 106 | 107 | // export {}; -------------------------------------------------------------------------------- /examples/js/stats.module.js: -------------------------------------------------------------------------------- 1 | var Stats = function () { 2 | 3 | var mode = 0; 4 | 5 | var container = document.createElement( 'div' ); 6 | container.style.cssText = 'position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000'; 7 | container.addEventListener( 'click', function ( event ) { 8 | 9 | event.preventDefault(); 10 | showPanel( ++ mode % container.children.length ); 11 | 12 | }, false ); 13 | 14 | // 15 | 16 | function addPanel( panel ) { 17 | 18 | container.appendChild( panel.dom ); 19 | return panel; 20 | 21 | } 22 | 23 | function showPanel( id ) { 24 | 25 | for ( var i = 0; i < container.children.length; i ++ ) { 26 | 27 | container.children[ i ].style.display = i === id ? 'block' : 'none'; 28 | 29 | } 30 | 31 | mode = id; 32 | 33 | } 34 | 35 | // 36 | 37 | var beginTime = ( performance || Date ).now(), prevTime = beginTime, frames = 0; 38 | 39 | var fpsPanel = addPanel( new Stats.Panel( 'FPS', '#0ff', '#002' ) ); 40 | var msPanel = addPanel( new Stats.Panel( 'MS', '#0f0', '#020' ) ); 41 | 42 | if ( self.performance && self.performance.memory ) { 43 | 44 | var memPanel = addPanel( new Stats.Panel( 'MB', '#f08', '#201' ) ); 45 | 46 | } 47 | 48 | showPanel( 0 ); 49 | 50 | return { 51 | 52 | REVISION: 16, 53 | 54 | dom: container, 55 | 56 | addPanel: addPanel, 57 | showPanel: showPanel, 58 | 59 | begin: function () { 60 | 61 | beginTime = ( performance || Date ).now(); 62 | 63 | }, 64 | 65 | end: function () { 66 | 67 | frames ++; 68 | 69 | var time = ( performance || Date ).now(); 70 | 71 | msPanel.update( time - beginTime, 200 ); 72 | 73 | if ( time >= prevTime + 1000 ) { 74 | 75 | fpsPanel.update( ( frames * 1000 ) / ( time - prevTime ), 100 ); 76 | 77 | prevTime = time; 78 | frames = 0; 79 | 80 | if ( memPanel ) { 81 | 82 | var memory = performance.memory; 83 | memPanel.update( memory.usedJSHeapSize / 1048576, memory.jsHeapSizeLimit / 1048576 ); 84 | 85 | } 86 | 87 | } 88 | 89 | return time; 90 | 91 | }, 92 | 93 | update: function () { 94 | 95 | beginTime = this.end(); 96 | 97 | }, 98 | 99 | // Backwards Compatibility 100 | 101 | domElement: container, 102 | setMode: showPanel 103 | 104 | }; 105 | 106 | }; 107 | 108 | Stats.Panel = function ( name, fg, bg ) { 109 | 110 | var min = Infinity, max = 0, round = Math.round; 111 | var PR = round( window.devicePixelRatio || 1 ); 112 | 113 | var WIDTH = 80 * PR, HEIGHT = 48 * PR, 114 | TEXT_X = 3 * PR, TEXT_Y = 2 * PR, 115 | GRAPH_X = 3 * PR, GRAPH_Y = 15 * PR, 116 | GRAPH_WIDTH = 74 * PR, GRAPH_HEIGHT = 30 * PR; 117 | 118 | var canvas = document.createElement( 'canvas' ); 119 | canvas.width = WIDTH; 120 | canvas.height = HEIGHT; 121 | canvas.style.cssText = 'width:80px;height:48px'; 122 | 123 | var context = canvas.getContext( '2d' ); 124 | context.font = 'bold ' + ( 9 * PR ) + 'px Helvetica,Arial,sans-serif'; 125 | context.textBaseline = 'top'; 126 | 127 | context.fillStyle = bg; 128 | context.fillRect( 0, 0, WIDTH, HEIGHT ); 129 | 130 | context.fillStyle = fg; 131 | context.fillText( name, TEXT_X, TEXT_Y ); 132 | context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT ); 133 | 134 | context.fillStyle = bg; 135 | context.globalAlpha = 0.9; 136 | context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT ); 137 | 138 | return { 139 | 140 | dom: canvas, 141 | 142 | update: function ( value, maxValue ) { 143 | 144 | min = Math.min( min, value ); 145 | max = Math.max( max, value ); 146 | 147 | context.fillStyle = bg; 148 | context.globalAlpha = 1; 149 | context.fillRect( 0, 0, WIDTH, GRAPH_Y ); 150 | context.fillStyle = fg; 151 | context.fillText( round( value ) + ' ' + name + ' (' + round( min ) + '-' + round( max ) + ')', TEXT_X, TEXT_Y ); 152 | 153 | context.drawImage( canvas, GRAPH_X + PR, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT, GRAPH_X, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT ); 154 | 155 | context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, GRAPH_HEIGHT ); 156 | 157 | context.fillStyle = bg; 158 | context.globalAlpha = 0.9; 159 | context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, round( ( 1 - ( value / maxValue ) ) * GRAPH_HEIGHT ) ); 160 | 161 | } 162 | 163 | }; 164 | 165 | }; 166 | 167 | export default Stats; 168 | -------------------------------------------------------------------------------- /examples/js/STLExporter.js: -------------------------------------------------------------------------------- 1 | import { 2 | Vector3 3 | } from 'three'; 4 | 5 | /** 6 | * Usage: 7 | * const exporter = new STLExporter(); 8 | * 9 | * // second argument is a list of options 10 | * const data = exporter.parse( mesh, { binary: true } ); 11 | * 12 | */ 13 | 14 | class STLExporter { 15 | 16 | parse( scene, options = {} ) { 17 | 18 | const binary = options.binary !== undefined ? options.binary : false; 19 | 20 | // 21 | 22 | const objects = []; 23 | let triangles = 0; 24 | 25 | scene.traverse( function ( object ) { 26 | 27 | if ( object.isMesh && object.visible ) { 28 | 29 | const geometry = object.geometry; 30 | 31 | if ( geometry.isBufferGeometry !== true ) { 32 | 33 | throw new Error( 'THREE.STLExporter: Geometry is not of type THREE.BufferGeometry.' ); 34 | 35 | } 36 | 37 | const index = geometry.index; 38 | const positionAttribute = geometry.getAttribute( 'position' ); 39 | 40 | triangles += ( index !== null ) ? ( index.count / 3 ) : ( positionAttribute.count / 3 ); 41 | 42 | objects.push( { 43 | object3d: object, 44 | geometry: geometry 45 | } ); 46 | 47 | } 48 | 49 | } ); 50 | 51 | let output; 52 | let offset = 80; // skip header 53 | 54 | if ( binary === true ) { 55 | 56 | const bufferLength = triangles * 2 + triangles * 3 * 4 * 4 + 80 + 4; 57 | const arrayBuffer = new ArrayBuffer( bufferLength ); 58 | output = new DataView( arrayBuffer ); 59 | output.setUint32( offset, triangles, true ); offset += 4; 60 | 61 | } else { 62 | 63 | output = ''; 64 | output += 'solid exported\n'; 65 | 66 | } 67 | 68 | const vA = new Vector3(); 69 | const vB = new Vector3(); 70 | const vC = new Vector3(); 71 | const cb = new Vector3(); 72 | const ab = new Vector3(); 73 | const normal = new Vector3(); 74 | 75 | for ( let i = 0, il = objects.length; i < il; i ++ ) { 76 | 77 | const object = objects[ i ].object3d; 78 | const geometry = objects[ i ].geometry; 79 | 80 | const index = geometry.index; 81 | const positionAttribute = geometry.getAttribute( 'position' ); 82 | 83 | if ( index !== null ) { 84 | 85 | // indexed geometry 86 | 87 | for ( let j = 0; j < index.count; j += 3 ) { 88 | 89 | const a = index.getX( j + 0 ); 90 | const b = index.getX( j + 1 ); 91 | const c = index.getX( j + 2 ); 92 | 93 | writeFace( a, b, c, positionAttribute, object ); 94 | 95 | } 96 | 97 | } else { 98 | 99 | // non-indexed geometry 100 | 101 | for ( let j = 0; j < positionAttribute.count; j += 3 ) { 102 | 103 | const a = j + 0; 104 | const b = j + 1; 105 | const c = j + 2; 106 | 107 | writeFace( a, b, c, positionAttribute, object ); 108 | 109 | } 110 | 111 | } 112 | 113 | } 114 | 115 | if ( binary === false ) { 116 | 117 | output += 'endsolid exported\n'; 118 | 119 | } 120 | 121 | return output; 122 | 123 | function writeFace( a, b, c, positionAttribute, object ) { 124 | 125 | vA.fromBufferAttribute( positionAttribute, a ); 126 | vB.fromBufferAttribute( positionAttribute, b ); 127 | vC.fromBufferAttribute( positionAttribute, c ); 128 | 129 | if ( object.isSkinnedMesh === true ) { 130 | 131 | object.boneTransform( a, vA ); 132 | object.boneTransform( b, vB ); 133 | object.boneTransform( c, vC ); 134 | 135 | } 136 | 137 | vA.applyMatrix4( object.matrixWorld ); 138 | vB.applyMatrix4( object.matrixWorld ); 139 | vC.applyMatrix4( object.matrixWorld ); 140 | 141 | rotateVector(vA); 142 | rotateVector(vB); 143 | rotateVector(vC); 144 | 145 | writeNormal( vA, vB, vC ); 146 | 147 | writeVertex( vA ); 148 | writeVertex( vB ); 149 | writeVertex( vC ); 150 | 151 | if ( binary === true ) { 152 | 153 | output.setUint16( offset, 0, true ); offset += 2; 154 | 155 | } else { 156 | 157 | output += '\t\tendloop\n'; 158 | output += '\tendfacet\n'; 159 | 160 | } 161 | 162 | } 163 | 164 | function writeNormal( vA, vB, vC ) { 165 | 166 | cb.subVectors( vC, vB ); 167 | ab.subVectors( vA, vB ); 168 | cb.cross( ab ).normalize(); 169 | 170 | normal.copy( cb ).normalize(); 171 | 172 | if ( binary === true ) { 173 | 174 | output.setFloat32( offset, normal.x, true ); offset += 4; 175 | output.setFloat32( offset, normal.y, true ); offset += 4; 176 | output.setFloat32( offset, normal.z, true ); offset += 4; 177 | 178 | } else { 179 | 180 | output += '\tfacet normal ' + normal.x + ' ' + normal.y + ' ' + normal.z + '\n'; 181 | output += '\t\touter loop\n'; 182 | 183 | } 184 | 185 | } 186 | 187 | function writeVertex( vertex ) { 188 | 189 | if ( binary === true ) { 190 | 191 | output.setFloat32( offset, vertex.x, true ); offset += 4; 192 | output.setFloat32( offset, vertex.y, true ); offset += 4; 193 | output.setFloat32( offset, vertex.z, true ); offset += 4; 194 | 195 | } else { 196 | 197 | output += '\t\t\tvertex ' + vertex.x + ' ' + vertex.y + ' ' + vertex.z + '\n'; 198 | 199 | } 200 | 201 | } 202 | 203 | } 204 | 205 | } 206 | function rotateVector(v) { 207 | let tempY = -v.z; 208 | v.z = v.y; 209 | v.y = tempY; 210 | return v; 211 | } 212 | export { STLExporter }; 213 | -------------------------------------------------------------------------------- /OctreeCSG/OctreeCSG.extended.js: -------------------------------------------------------------------------------- 1 | import OctreeCSG from './OctreeCSG.js'; 2 | import { Vector3, Plane, Line3, Sphere } from 'three'; 3 | import { Capsule } from 'threeModules/math/Capsule.js'; 4 | 5 | const _v1 = new Vector3(); 6 | const _v2 = new Vector3(); 7 | const _plane = new Plane(); 8 | const _line1 = new Line3(); 9 | const _line2 = new Line3(); 10 | const _sphere = new Sphere(); 11 | const _capsule = new Capsule(); 12 | 13 | class Octree extends OctreeCSG { 14 | constructor(box, parent) { 15 | super(box, parent); 16 | } 17 | getTriangles(triangles = []) { 18 | let polygons = this.getPolygons(); 19 | polygons.forEach(p => triangles.push(p.triangle)); 20 | return triangles; 21 | } 22 | getRayTriangles(ray, triangles = []) { 23 | let polygons = this.getRayPolygons(ray); 24 | polygons.forEach(p => triangles.push(p.triangle)); 25 | return triangles; 26 | } 27 | triangleCapsuleIntersect(capsule, triangle) { 28 | 29 | triangle.getPlane(_plane); 30 | 31 | const d1 = _plane.distanceToPoint(capsule.start) - capsule.radius; 32 | const d2 = _plane.distanceToPoint(capsule.end) - capsule.radius; 33 | 34 | if ((d1 > 0 && d2 > 0) || (d1 < - capsule.radius && d2 < - capsule.radius)) { 35 | 36 | return false; 37 | 38 | } 39 | 40 | const delta = Math.abs(d1 / (Math.abs(d1) + Math.abs(d2))); 41 | const intersectPoint = _v1.copy(capsule.start).lerp(capsule.end, delta); 42 | 43 | if (triangle.containsPoint(intersectPoint)) { 44 | 45 | return { normal: _plane.normal.clone(), point: intersectPoint.clone(), depth: Math.abs(Math.min(d1, d2)) }; 46 | 47 | } 48 | 49 | const r2 = capsule.radius * capsule.radius; 50 | 51 | const line1 = _line1.set(capsule.start, capsule.end); 52 | 53 | const lines = [ 54 | [triangle.a, triangle.b], 55 | [triangle.b, triangle.c], 56 | [triangle.c, triangle.a] 57 | ]; 58 | 59 | for (let i = 0; i < lines.length; i++) { 60 | 61 | const line2 = _line2.set(lines[i][0], lines[i][1]); 62 | 63 | const [point1, point2] = capsule.lineLineMinimumPoints(line1, line2); 64 | 65 | if (point1.distanceToSquared(point2) < r2) { 66 | 67 | return { normal: point1.clone().sub(point2).normalize(), point: point2.clone(), depth: capsule.radius - point1.distanceTo(point2) }; 68 | 69 | } 70 | 71 | } 72 | 73 | return false; 74 | 75 | } 76 | 77 | triangleSphereIntersect(sphere, triangle) { 78 | 79 | triangle.getPlane(_plane); 80 | 81 | if (!sphere.intersectsPlane(_plane)) return false; 82 | 83 | const depth = Math.abs(_plane.distanceToSphere(sphere)); 84 | const r2 = sphere.radius * sphere.radius - depth * depth; 85 | 86 | const plainPoint = _plane.projectPoint(sphere.center, _v1); 87 | 88 | if (triangle.containsPoint(sphere.center)) { 89 | 90 | return { normal: _plane.normal.clone(), point: plainPoint.clone(), depth: Math.abs(_plane.distanceToSphere(sphere)) }; 91 | 92 | } 93 | 94 | const lines = [ 95 | [triangle.a, triangle.b], 96 | [triangle.b, triangle.c], 97 | [triangle.c, triangle.a] 98 | ]; 99 | 100 | for (let i = 0; i < lines.length; i++) { 101 | 102 | _line1.set(lines[i][0], lines[i][1]); 103 | _line1.closestPointToPoint(plainPoint, true, _v2); 104 | 105 | const d = _v2.distanceToSquared(sphere.center); 106 | 107 | if (d < r2) { 108 | 109 | return { normal: sphere.center.clone().sub(_v2).normalize(), point: _v2.clone(), depth: sphere.radius - Math.sqrt(d) }; 110 | 111 | } 112 | 113 | } 114 | 115 | return false; 116 | 117 | } 118 | 119 | getSphereTriangles(sphere, triangles) { 120 | for (let i = 0; i < this.subTrees.length; i++) { 121 | const subTree = this.subTrees[i]; 122 | if (!sphere.intersectsBox(subTree.box)) continue; 123 | 124 | if (subTree.polygons.length > 0) { 125 | for (let j = 0; j < subTree.polygons.length; j++) { 126 | if (!subTree.polygons[j].valid) continue; 127 | 128 | if (triangles.indexOf(subTree.polygons[j].triangle) === - 1) { 129 | triangles.push(subTree.polygons[j].triangle); 130 | } 131 | } 132 | } 133 | else { 134 | subTree.getSphereTriangles(sphere, triangles); 135 | } 136 | } 137 | } 138 | 139 | getCapsuleTriangles(capsule, triangles) { 140 | for (let i = 0; i < this.subTrees.length; i++) { 141 | const subTree = this.subTrees[i]; 142 | if (!capsule.intersectsBox(subTree.box)) continue; 143 | 144 | if (subTree.polygons.length > 0) { 145 | for (let j = 0; j < subTree.polygons.length; j++) { 146 | if (!subTree.polygons[j].valid) continue; 147 | if (triangles.indexOf(subTree.polygons[j].triangle) === - 1) { 148 | triangles.push(subTree.polygons[j].triangle); 149 | } 150 | } 151 | } 152 | else { 153 | subTree.getCapsuleTriangles(capsule, triangles); 154 | } 155 | } 156 | } 157 | 158 | sphereIntersect(sphere) { 159 | _sphere.copy(sphere); 160 | const triangles = []; 161 | let result, hit = false; 162 | 163 | this.getSphereTriangles(sphere, triangles); 164 | for (let i = 0; i < triangles.length; i++) { 165 | if (result = this.triangleSphereIntersect(_sphere, triangles[i])) { 166 | hit = true; 167 | _sphere.center.add(result.normal.multiplyScalar(result.depth)); 168 | } 169 | } 170 | 171 | if (hit) { 172 | const collisionVector = _sphere.center.clone().sub(sphere.center); 173 | const depth = collisionVector.length(); 174 | return { normal: collisionVector.normalize(), depth: depth }; 175 | } 176 | 177 | return false; 178 | } 179 | 180 | capsuleIntersect(capsule) { 181 | 182 | _capsule.copy(capsule); 183 | 184 | const triangles = []; 185 | let result, hit = false; 186 | 187 | this.getCapsuleTriangles(_capsule, triangles); 188 | 189 | for (let i = 0; i < triangles.length; i++) { 190 | 191 | if (result = this.triangleCapsuleIntersect(_capsule, triangles[i])) { 192 | hit = true; 193 | _capsule.translate(result.normal.multiplyScalar(result.depth)); 194 | } 195 | } 196 | 197 | if (hit) { 198 | const collisionVector = _capsule.getCenter(new Vector3()).sub(capsule.getCenter(_v1)); 199 | const depth = collisionVector.length(); 200 | 201 | return { normal: collisionVector.normalize(), depth: depth }; 202 | } 203 | 204 | return false; 205 | } 206 | fromGraphNode(group) { 207 | group.updateWorldMatrix(true, true); 208 | group.traverse((obj) => { 209 | if (obj.isMesh === true) { 210 | OctreeCSG.fromMesh(obj, undefined, this, false); 211 | } 212 | }); 213 | this.buildTree(); 214 | 215 | return this; 216 | } 217 | } 218 | 219 | export { Octree, OctreeCSG }; -------------------------------------------------------------------------------- /examples/js/OctreeCSG/OctreeCSG.extended.js: -------------------------------------------------------------------------------- 1 | import OctreeCSG from './OctreeCSG.js'; 2 | import { Vector3, Plane, Line3, Sphere } from 'three'; 3 | import { Capsule } from '../Capsule.js'; 4 | 5 | const _v1 = new Vector3(); 6 | const _v2 = new Vector3(); 7 | const _plane = new Plane(); 8 | const _line1 = new Line3(); 9 | const _line2 = new Line3(); 10 | const _sphere = new Sphere(); 11 | const _capsule = new Capsule(); 12 | 13 | class Octree extends OctreeCSG { 14 | constructor(box, parent) { 15 | super(box, parent); 16 | } 17 | getTriangles(triangles = []) { 18 | let polygons = this.getPolygons(); 19 | polygons.forEach(p => triangles.push(p.triangle)); 20 | return triangles; 21 | } 22 | getRayTriangles(ray, triangles = []) { 23 | let polygons = this.getRayPolygons(ray); 24 | polygons.forEach(p => triangles.push(p.triangle)); 25 | return triangles; 26 | } 27 | triangleCapsuleIntersect(capsule, triangle) { 28 | 29 | triangle.getPlane(_plane); 30 | 31 | const d1 = _plane.distanceToPoint(capsule.start) - capsule.radius; 32 | const d2 = _plane.distanceToPoint(capsule.end) - capsule.radius; 33 | 34 | if ((d1 > 0 && d2 > 0) || (d1 < - capsule.radius && d2 < - capsule.radius)) { 35 | 36 | return false; 37 | 38 | } 39 | 40 | const delta = Math.abs(d1 / (Math.abs(d1) + Math.abs(d2))); 41 | const intersectPoint = _v1.copy(capsule.start).lerp(capsule.end, delta); 42 | 43 | if (triangle.containsPoint(intersectPoint)) { 44 | 45 | return { normal: _plane.normal.clone(), point: intersectPoint.clone(), depth: Math.abs(Math.min(d1, d2)) }; 46 | 47 | } 48 | 49 | const r2 = capsule.radius * capsule.radius; 50 | 51 | const line1 = _line1.set(capsule.start, capsule.end); 52 | 53 | const lines = [ 54 | [triangle.a, triangle.b], 55 | [triangle.b, triangle.c], 56 | [triangle.c, triangle.a] 57 | ]; 58 | 59 | for (let i = 0; i < lines.length; i++) { 60 | 61 | const line2 = _line2.set(lines[i][0], lines[i][1]); 62 | 63 | const [point1, point2] = capsule.lineLineMinimumPoints(line1, line2); 64 | 65 | if (point1.distanceToSquared(point2) < r2) { 66 | 67 | return { normal: point1.clone().sub(point2).normalize(), point: point2.clone(), depth: capsule.radius - point1.distanceTo(point2) }; 68 | 69 | } 70 | 71 | } 72 | 73 | return false; 74 | 75 | } 76 | 77 | triangleSphereIntersect(sphere, triangle) { 78 | 79 | triangle.getPlane(_plane); 80 | 81 | if (!sphere.intersectsPlane(_plane)) return false; 82 | 83 | const depth = Math.abs(_plane.distanceToSphere(sphere)); 84 | const r2 = sphere.radius * sphere.radius - depth * depth; 85 | 86 | const plainPoint = _plane.projectPoint(sphere.center, _v1); 87 | 88 | if (triangle.containsPoint(sphere.center)) { 89 | 90 | return { normal: _plane.normal.clone(), point: plainPoint.clone(), depth: Math.abs(_plane.distanceToSphere(sphere)) }; 91 | 92 | } 93 | 94 | const lines = [ 95 | [triangle.a, triangle.b], 96 | [triangle.b, triangle.c], 97 | [triangle.c, triangle.a] 98 | ]; 99 | 100 | for (let i = 0; i < lines.length; i++) { 101 | 102 | _line1.set(lines[i][0], lines[i][1]); 103 | _line1.closestPointToPoint(plainPoint, true, _v2); 104 | 105 | const d = _v2.distanceToSquared(sphere.center); 106 | 107 | if (d < r2) { 108 | 109 | return { normal: sphere.center.clone().sub(_v2).normalize(), point: _v2.clone(), depth: sphere.radius - Math.sqrt(d) }; 110 | 111 | } 112 | 113 | } 114 | 115 | return false; 116 | 117 | } 118 | 119 | getSphereTriangles(sphere, triangles) { 120 | for (let i = 0; i < this.subTrees.length; i++) { 121 | const subTree = this.subTrees[i]; 122 | if (!sphere.intersectsBox(subTree.box)) continue; 123 | 124 | if (subTree.polygons.length > 0) { 125 | for (let j = 0; j < subTree.polygons.length; j++) { 126 | if (!subTree.polygons[j].valid) continue; 127 | 128 | if (triangles.indexOf(subTree.polygons[j].triangle) === - 1) { 129 | triangles.push(subTree.polygons[j].triangle); 130 | } 131 | } 132 | } 133 | else { 134 | subTree.getSphereTriangles(sphere, triangles); 135 | } 136 | } 137 | } 138 | 139 | getCapsuleTriangles(capsule, triangles) { 140 | for (let i = 0; i < this.subTrees.length; i++) { 141 | const subTree = this.subTrees[i]; 142 | if (!capsule.intersectsBox(subTree.box)) continue; 143 | 144 | if (subTree.polygons.length > 0) { 145 | for (let j = 0; j < subTree.polygons.length; j++) { 146 | if (!subTree.polygons[j].valid) continue; 147 | if (triangles.indexOf(subTree.polygons[j].triangle) === - 1) { 148 | triangles.push(subTree.polygons[j].triangle); 149 | } 150 | } 151 | } 152 | else { 153 | subTree.getCapsuleTriangles(capsule, triangles); 154 | } 155 | } 156 | } 157 | 158 | sphereIntersect(sphere) { 159 | _sphere.copy(sphere); 160 | const triangles = []; 161 | let result, hit = false; 162 | 163 | this.getSphereTriangles(sphere, triangles); 164 | for (let i = 0; i < triangles.length; i++) { 165 | if (result = this.triangleSphereIntersect(_sphere, triangles[i])) { 166 | hit = true; 167 | _sphere.center.add(result.normal.multiplyScalar(result.depth)); 168 | } 169 | } 170 | 171 | if (hit) { 172 | const collisionVector = _sphere.center.clone().sub(sphere.center); 173 | const depth = collisionVector.length(); 174 | return { normal: collisionVector.normalize(), depth: depth }; 175 | } 176 | 177 | return false; 178 | } 179 | 180 | capsuleIntersect(capsule) { 181 | 182 | _capsule.copy(capsule); 183 | 184 | const triangles = []; 185 | let result, hit = false; 186 | 187 | this.getCapsuleTriangles(_capsule, triangles); 188 | 189 | for (let i = 0; i < triangles.length; i++) { 190 | 191 | if (result = this.triangleCapsuleIntersect(_capsule, triangles[i])) { 192 | hit = true; 193 | _capsule.translate(result.normal.multiplyScalar(result.depth)); 194 | } 195 | } 196 | 197 | if (hit) { 198 | const collisionVector = _capsule.getCenter(new Vector3()).sub(capsule.getCenter(_v1)); 199 | const depth = collisionVector.length(); 200 | 201 | return { normal: collisionVector.normalize(), depth: depth }; 202 | } 203 | 204 | return false; 205 | } 206 | fromGraphNode(group) { 207 | group.updateWorldMatrix(true, true); 208 | group.traverse((obj) => { 209 | if (obj.isMesh === true) { 210 | OctreeCSG.fromMesh(obj, undefined, this, false); 211 | } 212 | }); 213 | this.buildTree(); 214 | 215 | return this; 216 | } 217 | } 218 | 219 | export { Octree, OctreeCSG }; -------------------------------------------------------------------------------- /examples/js/TessellateModifier.js: -------------------------------------------------------------------------------- 1 | import { 2 | BufferGeometry, 3 | Color, 4 | Float32BufferAttribute, 5 | Vector2, 6 | Vector3 7 | } from 'three'; 8 | 9 | /** 10 | * Break faces with edges longer than maxEdgeLength 11 | */ 12 | 13 | class TessellateModifier { 14 | 15 | constructor( maxEdgeLength = 0.1, maxIterations = 6 ) { 16 | 17 | this.maxEdgeLength = maxEdgeLength; 18 | this.maxIterations = maxIterations; 19 | 20 | } 21 | 22 | modify( geometry ) { 23 | 24 | if ( geometry.index !== null ) { 25 | 26 | geometry = geometry.toNonIndexed(); 27 | 28 | } 29 | 30 | // 31 | 32 | const maxIterations = this.maxIterations; 33 | const maxEdgeLengthSquared = this.maxEdgeLength * this.maxEdgeLength; 34 | 35 | const va = new Vector3(); 36 | const vb = new Vector3(); 37 | const vc = new Vector3(); 38 | const vm = new Vector3(); 39 | const vs = [ va, vb, vc, vm ]; 40 | 41 | const na = new Vector3(); 42 | const nb = new Vector3(); 43 | const nc = new Vector3(); 44 | const nm = new Vector3(); 45 | const ns = [ na, nb, nc, nm ]; 46 | 47 | const ca = new Color(); 48 | const cb = new Color(); 49 | const cc = new Color(); 50 | const cm = new Color(); 51 | const cs = [ ca, cb, cc, cm ]; 52 | 53 | const ua = new Vector2(); 54 | const ub = new Vector2(); 55 | const uc = new Vector2(); 56 | const um = new Vector2(); 57 | const us = [ ua, ub, uc, um ]; 58 | 59 | const u2a = new Vector2(); 60 | const u2b = new Vector2(); 61 | const u2c = new Vector2(); 62 | const u2m = new Vector2(); 63 | const u2s = [ u2a, u2b, u2c, u2m ]; 64 | 65 | const attributes = geometry.attributes; 66 | const hasNormals = attributes.normal !== undefined; 67 | const hasColors = attributes.color !== undefined; 68 | const hasUVs = attributes.uv !== undefined; 69 | const hasUV2s = attributes.uv2 !== undefined; 70 | 71 | let positions = attributes.position.array; 72 | let normals = hasNormals ? attributes.normal.array : null; 73 | let colors = hasColors ? attributes.color.array : null; 74 | let uvs = hasUVs ? attributes.uv.array : null; 75 | let uv2s = hasUV2s ? attributes.uv2.array : null; 76 | 77 | let positions2 = positions; 78 | let normals2 = normals; 79 | let colors2 = colors; 80 | let uvs2 = uvs; 81 | let uv2s2 = uv2s; 82 | 83 | let iteration = 0; 84 | let tessellating = true; 85 | 86 | function addTriangle( a, b, c ) { 87 | 88 | const v1 = vs[ a ]; 89 | const v2 = vs[ b ]; 90 | const v3 = vs[ c ]; 91 | 92 | positions2.push( v1.x, v1.y, v1.z ); 93 | positions2.push( v2.x, v2.y, v2.z ); 94 | positions2.push( v3.x, v3.y, v3.z ); 95 | 96 | if ( hasNormals ) { 97 | 98 | const n1 = ns[ a ]; 99 | const n2 = ns[ b ]; 100 | const n3 = ns[ c ]; 101 | 102 | normals2.push( n1.x, n1.y, n1.z ); 103 | normals2.push( n2.x, n2.y, n2.z ); 104 | normals2.push( n3.x, n3.y, n3.z ); 105 | 106 | } 107 | 108 | if ( hasColors ) { 109 | 110 | const c1 = cs[ a ]; 111 | const c2 = cs[ b ]; 112 | const c3 = cs[ c ]; 113 | 114 | colors2.push( c1.x, c1.y, c1.z ); 115 | colors2.push( c2.x, c2.y, c2.z ); 116 | colors2.push( c3.x, c3.y, c3.z ); 117 | 118 | } 119 | 120 | if ( hasUVs ) { 121 | 122 | const u1 = us[ a ]; 123 | const u2 = us[ b ]; 124 | const u3 = us[ c ]; 125 | 126 | uvs2.push( u1.x, u1.y ); 127 | uvs2.push( u2.x, u2.y ); 128 | uvs2.push( u3.x, u3.y ); 129 | 130 | } 131 | 132 | if ( hasUV2s ) { 133 | 134 | const u21 = u2s[ a ]; 135 | const u22 = u2s[ b ]; 136 | const u23 = u2s[ c ]; 137 | 138 | uv2s2.push( u21.x, u21.y ); 139 | uv2s2.push( u22.x, u22.y ); 140 | uv2s2.push( u23.x, u23.y ); 141 | 142 | } 143 | 144 | } 145 | 146 | while ( tessellating && iteration < maxIterations ) { 147 | 148 | iteration ++; 149 | tessellating = false; 150 | 151 | positions = positions2; 152 | positions2 = []; 153 | 154 | if ( hasNormals ) { 155 | 156 | normals = normals2; 157 | normals2 = []; 158 | 159 | } 160 | 161 | if ( hasColors ) { 162 | 163 | colors = colors2; 164 | colors2 = []; 165 | 166 | } 167 | 168 | if ( hasUVs ) { 169 | 170 | uvs = uvs2; 171 | uvs2 = []; 172 | 173 | } 174 | 175 | if ( hasUV2s ) { 176 | 177 | uv2s = uv2s2; 178 | uv2s2 = []; 179 | 180 | } 181 | 182 | for ( let i = 0, i2 = 0, il = positions.length; i < il; i += 9, i2 += 6 ) { 183 | 184 | va.fromArray( positions, i + 0 ); 185 | vb.fromArray( positions, i + 3 ); 186 | vc.fromArray( positions, i + 6 ); 187 | 188 | if ( hasNormals ) { 189 | 190 | na.fromArray( normals, i + 0 ); 191 | nb.fromArray( normals, i + 3 ); 192 | nc.fromArray( normals, i + 6 ); 193 | 194 | } 195 | 196 | if ( hasColors ) { 197 | 198 | ca.fromArray( colors, i + 0 ); 199 | cb.fromArray( colors, i + 3 ); 200 | cc.fromArray( colors, i + 6 ); 201 | 202 | } 203 | 204 | if ( hasUVs ) { 205 | 206 | ua.fromArray( uvs, i2 + 0 ); 207 | ub.fromArray( uvs, i2 + 2 ); 208 | uc.fromArray( uvs, i2 + 4 ); 209 | 210 | } 211 | 212 | if ( hasUV2s ) { 213 | 214 | u2a.fromArray( uv2s, i2 + 0 ); 215 | u2b.fromArray( uv2s, i2 + 2 ); 216 | u2c.fromArray( uv2s, i2 + 4 ); 217 | 218 | } 219 | 220 | const dab = va.distanceToSquared( vb ); 221 | const dbc = vb.distanceToSquared( vc ); 222 | const dac = va.distanceToSquared( vc ); 223 | 224 | if ( dab > maxEdgeLengthSquared || dbc > maxEdgeLengthSquared || dac > maxEdgeLengthSquared ) { 225 | 226 | tessellating = true; 227 | 228 | if ( dab >= dbc && dab >= dac ) { 229 | 230 | vm.lerpVectors( va, vb, 0.5 ); 231 | if ( hasNormals ) nm.lerpVectors( na, nb, 0.5 ); 232 | if ( hasColors ) cm.lerpColors( ca, cb, 0.5 ); 233 | if ( hasUVs ) um.lerpVectors( ua, ub, 0.5 ); 234 | if ( hasUV2s ) u2m.lerpVectors( u2a, u2b, 0.5 ); 235 | 236 | addTriangle( 0, 3, 2 ); 237 | addTriangle( 3, 1, 2 ); 238 | 239 | } else if ( dbc >= dab && dbc >= dac ) { 240 | 241 | vm.lerpVectors( vb, vc, 0.5 ); 242 | if ( hasNormals ) nm.lerpVectors( nb, nc, 0.5 ); 243 | if ( hasColors ) cm.lerpColors( cb, cc, 0.5 ); 244 | if ( hasUVs ) um.lerpVectors( ub, uc, 0.5 ); 245 | if ( hasUV2s ) u2m.lerpVectors( u2b, u2c, 0.5 ); 246 | 247 | addTriangle( 0, 1, 3 ); 248 | addTriangle( 3, 2, 0 ); 249 | 250 | } else { 251 | 252 | vm.lerpVectors( va, vc, 0.5 ); 253 | if ( hasNormals ) nm.lerpVectors( na, nc, 0.5 ); 254 | if ( hasColors ) cm.lerpColors( ca, cc, 0.5 ); 255 | if ( hasUVs ) um.lerpVectors( ua, uc, 0.5 ); 256 | if ( hasUV2s ) u2m.lerpVectors( u2a, u2c, 0.5 ); 257 | 258 | addTriangle( 0, 1, 3 ); 259 | addTriangle( 3, 1, 2 ); 260 | 261 | } 262 | 263 | } else { 264 | 265 | addTriangle( 0, 1, 2 ); 266 | 267 | } 268 | 269 | } 270 | 271 | } 272 | 273 | const geometry2 = new BufferGeometry(); 274 | 275 | geometry2.setAttribute( 'position', new Float32BufferAttribute( positions2, 3 ) ); 276 | 277 | if ( hasNormals ) { 278 | 279 | geometry2.setAttribute( 'normal', new Float32BufferAttribute( normals2, 3 ) ); 280 | 281 | } 282 | 283 | if ( hasColors ) { 284 | 285 | geometry2.setAttribute( 'color', new Float32BufferAttribute( colors2, 3 ) ); 286 | 287 | } 288 | 289 | if ( hasUVs ) { 290 | 291 | geometry2.setAttribute( 'uv', new Float32BufferAttribute( uvs2, 2 ) ); 292 | 293 | } 294 | 295 | if ( hasUV2s ) { 296 | 297 | geometry2.setAttribute( 'uv2', new Float32BufferAttribute( uv2s2, 2 ) ); 298 | 299 | } 300 | 301 | return geometry2; 302 | 303 | } 304 | 305 | } 306 | 307 | export { TessellateModifier }; 308 | -------------------------------------------------------------------------------- /examples/js/STLLoader.js: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | FileLoader, 5 | Float32BufferAttribute, 6 | Loader, 7 | LoaderUtils, 8 | Vector3 9 | } from 'three'; 10 | 11 | /** 12 | * Description: A THREE loader for STL ASCII files, as created by Solidworks and other CAD programs. 13 | * 14 | * Supports both binary and ASCII encoded files, with automatic detection of type. 15 | * 16 | * The loader returns a non-indexed buffer geometry. 17 | * 18 | * Limitations: 19 | * Binary decoding supports "Magics" color format (http://en.wikipedia.org/wiki/STL_(file_format)#Color_in_binary_STL). 20 | * There is perhaps some question as to how valid it is to always assume little-endian-ness. 21 | * ASCII decoding assumes file is UTF-8. 22 | * 23 | * Usage: 24 | * const loader = new STLLoader(); 25 | * loader.load( './models/stl/slotted_disk.stl', function ( geometry ) { 26 | * scene.add( new THREE.Mesh( geometry ) ); 27 | * }); 28 | * 29 | * For binary STLs geometry might contain colors for vertices. To use it: 30 | * // use the same code to load STL as above 31 | * if (geometry.hasColors) { 32 | * material = new THREE.MeshPhongMaterial({ opacity: geometry.alpha, vertexColors: true }); 33 | * } else { .... } 34 | * const mesh = new THREE.Mesh( geometry, material ); 35 | * 36 | * For ASCII STLs containing multiple solids, each solid is assigned to a different group. 37 | * Groups can be used to assign a different color by defining an array of materials with the same length of 38 | * geometry.groups and passing it to the Mesh constructor: 39 | * 40 | * const mesh = new THREE.Mesh( geometry, material ); 41 | * 42 | * For example: 43 | * 44 | * const materials = []; 45 | * const nGeometryGroups = geometry.groups.length; 46 | * 47 | * const colorMap = ...; // Some logic to index colors. 48 | * 49 | * for (let i = 0; i < nGeometryGroups; i++) { 50 | * 51 | * const material = new THREE.MeshPhongMaterial({ 52 | * color: colorMap[i], 53 | * wireframe: false 54 | * }); 55 | * 56 | * } 57 | * 58 | * materials.push(material); 59 | * const mesh = new THREE.Mesh(geometry, materials); 60 | */ 61 | 62 | 63 | class STLLoader extends Loader { 64 | 65 | constructor( manager ) { 66 | 67 | super( manager ); 68 | 69 | } 70 | 71 | load( url, onLoad, onProgress, onError ) { 72 | 73 | const scope = this; 74 | 75 | const loader = new FileLoader( this.manager ); 76 | loader.setPath( this.path ); 77 | loader.setResponseType( 'arraybuffer' ); 78 | loader.setRequestHeader( this.requestHeader ); 79 | loader.setWithCredentials( this.withCredentials ); 80 | 81 | loader.load( url, function ( text ) { 82 | 83 | try { 84 | 85 | onLoad( scope.parse( text ) ); 86 | 87 | } catch ( e ) { 88 | 89 | if ( onError ) { 90 | 91 | onError( e ); 92 | 93 | } else { 94 | 95 | console.error( e ); 96 | 97 | } 98 | 99 | scope.manager.itemError( url ); 100 | 101 | } 102 | 103 | }, onProgress, onError ); 104 | 105 | } 106 | 107 | parse( data ) { 108 | 109 | function isBinary( data ) { 110 | 111 | const reader = new DataView( data ); 112 | const face_size = ( 32 / 8 * 3 ) + ( ( 32 / 8 * 3 ) * 3 ) + ( 16 / 8 ); 113 | const n_faces = reader.getUint32( 80, true ); 114 | const expect = 80 + ( 32 / 8 ) + ( n_faces * face_size ); 115 | 116 | if ( expect === reader.byteLength ) { 117 | 118 | return true; 119 | 120 | } 121 | 122 | // An ASCII STL data must begin with 'solid ' as the first six bytes. 123 | // However, ASCII STLs lacking the SPACE after the 'd' are known to be 124 | // plentiful. So, check the first 5 bytes for 'solid'. 125 | 126 | // Several encodings, such as UTF-8, precede the text with up to 5 bytes: 127 | // https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding 128 | // Search for "solid" to start anywhere after those prefixes. 129 | 130 | // US-ASCII ordinal values for 's', 'o', 'l', 'i', 'd' 131 | 132 | const solid = [ 115, 111, 108, 105, 100 ]; 133 | 134 | for ( let off = 0; off < 5; off ++ ) { 135 | 136 | // If "solid" text is matched to the current offset, declare it to be an ASCII STL. 137 | 138 | if ( matchDataViewAt( solid, reader, off ) ) return false; 139 | 140 | } 141 | 142 | // Couldn't find "solid" text at the beginning; it is binary STL. 143 | 144 | return true; 145 | 146 | } 147 | 148 | function matchDataViewAt( query, reader, offset ) { 149 | 150 | // Check if each byte in query matches the corresponding byte from the current offset 151 | 152 | for ( let i = 0, il = query.length; i < il; i ++ ) { 153 | 154 | if ( query[ i ] !== reader.getUint8( offset + i ) ) return false; 155 | 156 | } 157 | 158 | return true; 159 | 160 | } 161 | 162 | function parseBinary( data ) { 163 | 164 | const reader = new DataView( data ); 165 | const faces = reader.getUint32( 80, true ); 166 | 167 | let r, g, b, hasColors = false, colors; 168 | let defaultR, defaultG, defaultB, alpha; 169 | 170 | // process STL header 171 | // check for default color in header ("COLOR=rgba" sequence). 172 | 173 | for ( let index = 0; index < 80 - 10; index ++ ) { 174 | 175 | if ( ( reader.getUint32( index, false ) == 0x434F4C4F /*COLO*/ ) && 176 | ( reader.getUint8( index + 4 ) == 0x52 /*'R'*/ ) && 177 | ( reader.getUint8( index + 5 ) == 0x3D /*'='*/ ) ) { 178 | 179 | hasColors = true; 180 | colors = new Float32Array( faces * 3 * 3 ); 181 | 182 | defaultR = reader.getUint8( index + 6 ) / 255; 183 | defaultG = reader.getUint8( index + 7 ) / 255; 184 | defaultB = reader.getUint8( index + 8 ) / 255; 185 | alpha = reader.getUint8( index + 9 ) / 255; 186 | 187 | } 188 | 189 | } 190 | 191 | const dataOffset = 84; 192 | const faceLength = 12 * 4 + 2; 193 | 194 | const geometry = new BufferGeometry(); 195 | 196 | const vertices = new Float32Array( faces * 3 * 3 ); 197 | const normals = new Float32Array( faces * 3 * 3 ); 198 | 199 | for ( let face = 0; face < faces; face ++ ) { 200 | 201 | const start = dataOffset + face * faceLength; 202 | const normalX = reader.getFloat32( start, true ); 203 | const normalY = reader.getFloat32( start + 4, true ); 204 | const normalZ = reader.getFloat32( start + 8, true ); 205 | 206 | if ( hasColors ) { 207 | 208 | const packedColor = reader.getUint16( start + 48, true ); 209 | 210 | if ( ( packedColor & 0x8000 ) === 0 ) { 211 | 212 | // facet has its own unique color 213 | 214 | r = ( packedColor & 0x1F ) / 31; 215 | g = ( ( packedColor >> 5 ) & 0x1F ) / 31; 216 | b = ( ( packedColor >> 10 ) & 0x1F ) / 31; 217 | 218 | } else { 219 | 220 | r = defaultR; 221 | g = defaultG; 222 | b = defaultB; 223 | 224 | } 225 | 226 | } 227 | 228 | for ( let i = 1; i <= 3; i ++ ) { 229 | 230 | const vertexstart = start + i * 12; 231 | const componentIdx = ( face * 3 * 3 ) + ( ( i - 1 ) * 3 ); 232 | 233 | vertices[ componentIdx ] = reader.getFloat32( vertexstart, true ); 234 | vertices[ componentIdx + 2 ] = -reader.getFloat32( vertexstart + 4, true ); 235 | vertices[ componentIdx + 1 ] = reader.getFloat32( vertexstart + 8, true ); 236 | 237 | normals[ componentIdx ] = normalX; 238 | normals[ componentIdx + 1 ] = normalZ; 239 | normals[ componentIdx + 2 ] = -normalY; 240 | 241 | if ( hasColors ) { 242 | 243 | colors[ componentIdx ] = r; 244 | colors[ componentIdx + 1 ] = g; 245 | colors[ componentIdx + 2 ] = b; 246 | 247 | } 248 | 249 | } 250 | 251 | } 252 | 253 | geometry.setAttribute( 'position', new BufferAttribute( vertices, 3 ) ); 254 | geometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) ); 255 | 256 | if ( hasColors ) { 257 | 258 | geometry.setAttribute( 'color', new BufferAttribute( colors, 3 ) ); 259 | geometry.hasColors = true; 260 | geometry.alpha = alpha; 261 | 262 | } 263 | 264 | return geometry; 265 | 266 | } 267 | 268 | function parseASCII( data ) { 269 | 270 | const geometry = new BufferGeometry(); 271 | const patternSolid = /solid([\s\S]*?)endsolid/g; 272 | const patternFace = /facet([\s\S]*?)endfacet/g; 273 | let faceCounter = 0; 274 | 275 | const patternFloat = /[\s]+([+-]?(?:\d*)(?:\.\d*)?(?:[eE][+-]?\d+)?)/.source; 276 | const patternVertex = new RegExp( 'vertex' + patternFloat + patternFloat + patternFloat, 'g' ); 277 | const patternNormal = new RegExp( 'normal' + patternFloat + patternFloat + patternFloat, 'g' ); 278 | 279 | const vertices = []; 280 | const normals = []; 281 | 282 | const normal = new Vector3(); 283 | 284 | let result; 285 | 286 | let groupCount = 0; 287 | let startVertex = 0; 288 | let endVertex = 0; 289 | 290 | while ( ( result = patternSolid.exec( data ) ) !== null ) { 291 | 292 | startVertex = endVertex; 293 | 294 | const solid = result[ 0 ]; 295 | 296 | while ( ( result = patternFace.exec( solid ) ) !== null ) { 297 | 298 | let vertexCountPerFace = 0; 299 | let normalCountPerFace = 0; 300 | 301 | const text = result[ 0 ]; 302 | 303 | while ( ( result = patternNormal.exec( text ) ) !== null ) { 304 | 305 | normal.x = parseFloat( result[ 1 ] ); 306 | normal.y = parseFloat( result[ 2 ] ); 307 | normal.z = parseFloat( result[ 3 ] ); 308 | normalCountPerFace ++; 309 | 310 | } 311 | 312 | while ( ( result = patternVertex.exec( text ) ) !== null ) { 313 | 314 | vertices.push( parseFloat( result[ 1 ] ), parseFloat( result[ 2 ] ), parseFloat( result[ 3 ] ) ); 315 | normals.push( normal.x, normal.y, normal.z ); 316 | vertexCountPerFace ++; 317 | endVertex ++; 318 | 319 | } 320 | 321 | // every face have to own ONE valid normal 322 | 323 | if ( normalCountPerFace !== 1 ) { 324 | 325 | console.error( 'THREE.STLLoader: Something isn\'t right with the normal of face number ' + faceCounter ); 326 | 327 | } 328 | 329 | // each face have to own THREE valid vertices 330 | 331 | if ( vertexCountPerFace !== 3 ) { 332 | 333 | console.error( 'THREE.STLLoader: Something isn\'t right with the vertices of face number ' + faceCounter ); 334 | 335 | } 336 | 337 | faceCounter ++; 338 | 339 | } 340 | 341 | const start = startVertex; 342 | const count = endVertex - startVertex; 343 | 344 | geometry.addGroup( start, count, groupCount ); 345 | groupCount ++; 346 | 347 | } 348 | 349 | geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) ); 350 | geometry.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) ); 351 | 352 | return geometry; 353 | 354 | } 355 | 356 | function ensureString( buffer ) { 357 | 358 | if ( typeof buffer !== 'string' ) { 359 | 360 | return LoaderUtils.decodeText( new Uint8Array( buffer ) ); 361 | 362 | } 363 | 364 | return buffer; 365 | 366 | } 367 | 368 | function ensureBinary( buffer ) { 369 | 370 | if ( typeof buffer === 'string' ) { 371 | 372 | const array_buffer = new Uint8Array( buffer.length ); 373 | for ( let i = 0; i < buffer.length; i ++ ) { 374 | 375 | array_buffer[ i ] = buffer.charCodeAt( i ) & 0xff; // implicitly assumes little-endian 376 | 377 | } 378 | 379 | return array_buffer.buffer || array_buffer; 380 | 381 | } else { 382 | 383 | return buffer; 384 | 385 | } 386 | 387 | } 388 | 389 | // start 390 | 391 | const binData = ensureBinary( data ); 392 | 393 | return isBinary( binData ) ? parseBinary( binData ) : parseASCII( ensureString( data ) ); 394 | 395 | } 396 | 397 | } 398 | 399 | export { STLLoader }; 400 | -------------------------------------------------------------------------------- /examples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OctreeCSG Example - Basic 5 | 6 | 32 | 33 | 34 | 35 |
36 |
37 | CSG Stats:
38 |
CSG Operation: 
0ms
39 |
40 |
Triangles: 
41 |
42 |
43 | 44 | 45 | 59 | 296 | 297 | 298 | -------------------------------------------------------------------------------- /examples/realtime1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OctreeCSG Example - Real-Time CSG 1 5 | 6 | 38 | 39 | 40 | 41 | 42 | 43 |
44 |
45 | OctreeCSG - Real-Time CSG
46 | Drag the window over the wall 47 |
48 | 49 | 61 | 298 | 299 | 300 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OctreeCSG 2 | Constructive Solid Geometry (CSG) library for use with [three.js](https://github.com/mrdoob/three.js)\ 3 | The OctreeCSG library is using the [Octree](https://en.wikipedia.org/wiki/Octree) data structure to store the geometry data for the [CSG](https://en.wikipedia.org/wiki/Constructive_solid_geometry) operations 4 |
5 | 6 | All the code examples below can be tested live in [3dd.dev](https://3dd.dev) 7 | 8 | ### Table of Contents 9 | - [Usage](#usage) 10 | - [Basic Operations](#basic-operations) 11 | - [OctreeCSG.meshUnion](#mesh-union-octreecsgmeshunion) 12 | - [OctreeCSG.meshSubtract](#mesh-subtract-octreecsgmeshsubtract) 13 | - [OctreeCSG.meshIntersect](#mesh-intersect-octreecsgmeshintersect) 14 | - [Advanced Operations](#advanced-operations) 15 | - [OctreeCSG.fromMesh](#octreecsgfrommesh) 16 | - [OctreeCSG.toMesh](#octreecsgtomesh) 17 | - [OctreeCSG.union](#octreecsgunion) 18 | - [OctreeCSG.subtract](#octreecsgsubtract) 19 | - [OctreeCSG.intersect](#octreecsgintersect) 20 | - [OctreeCSG.operation](#octreecsgoperation) 21 | - [Array Operations](#array-operations) 22 | - [Asynchronous Operations](#asynchronous-operations) 23 | - [OctreeCSG Flags](#octreecsg-flags) 24 | - [Examples](#examples) 25 | - [Resources](#resources) 26 | 27 | ## Usage 28 | OctreeCSG comes as a Javascript Module and can be imported with the following command: 29 | ```js 30 | import OctreeCSG from './OctreeCSG/OctreeCSG.js'; 31 | ``` 32 |
33 | 34 | ## Basic Operations 35 | OctreeCSG provides basic boolean operations (union, subtract and intersect) for ease of use. 36 | The basic operations expects the same type of parameters: 37 | | Parameter | Description | 38 | | --- | --- | 39 | | mesh1 | First mesh | 40 | | mesh2 | Second mesh | 41 | | targetMaterial | (Optional) The material to use for the final mesh, can be a single material or an array of two materials. **Default**: A clone of the material of the first mesh | 42 | 43 | ### Mesh Union (OctreeCSG.meshUnion) 44 | ```js 45 | const geometry = new THREE.BoxGeometry(10, 10, 10); 46 | const material1 = new THREE.MeshStandardMaterial({ color: 0xff0000 }); 47 | const material2 = new THREE.MeshStandardMaterial({ color: 0x0000ff }); 48 | const mesh1 = new THREE.Mesh(geometry, material1); 49 | const mesh2 = new THREE.Mesh(geometry.clone(), material2); 50 | mesh2.position.set(5, -5, 5); 51 | 52 | const resultMesh = OctreeCSG.meshUnion(mesh1, mesh2); 53 | scene.add(resultMesh); 54 | ``` 55 | 56 | ### Mesh Subtract (OctreeCSG.meshSubtract) 57 | ```js 58 | const geometry = new THREE.BoxGeometry(10, 10, 10); 59 | const material1 = new THREE.MeshStandardMaterial({ color: 0xff0000 }); 60 | const material2 = new THREE.MeshStandardMaterial({ color: 0x0000ff }); 61 | const mesh1 = new THREE.Mesh(geometry, material1); 62 | const mesh2 = new THREE.Mesh(geometry.clone(), material2); 63 | mesh2.position.set(5, -5, 5); 64 | 65 | const resultMesh = OctreeCSG.meshSubtract(mesh1, mesh2); 66 | scene.add(resultMesh); 67 | ``` 68 | 69 | ### Mesh Intersect (OctreeCSG.meshIntersect) 70 | ```js 71 | const geometry = new THREE.BoxGeometry(10, 10, 10); 72 | const material1 = new THREE.MeshStandardMaterial({ color: 0xff0000 }); 73 | const material2 = new THREE.MeshStandardMaterial({ color: 0x0000ff }); 74 | const mesh1 = new THREE.Mesh(geometry, material1); 75 | const mesh2 = new THREE.Mesh(geometry.clone(), material2); 76 | mesh2.position.set(5, -5, 5); 77 | 78 | const resultMesh = OctreeCSG.meshIntersect(mesh1, mesh2); 79 | scene.add(resultMesh); 80 | ``` 81 |

82 | 83 | ## Advanced Operations 84 | ### OctreeCSG.fromMesh 85 | Converts a three.js mesh to an Octree 86 | | Parameter | Description | 87 | | --- | --- | 88 | | obj | three.js mesh | 89 | | objectIndex | (Optional) Used for specifying the geometry group index in the result mesh. **Default**: Input mesh's groups if there are any | 90 | | octree | (Optional) Target octree to use. **Default**: new Octree | 91 | | buildTargetOctree | (Optional) Specifies if to build the target Octree tree or return a flat Octree (true / flase). **Default**: true | 92 | 93 | ### OctreeCSG.toMesh 94 | Converts an Octree to a three.js mesh 95 | | Parameter | Description | 96 | | --- | --- | 97 | | octree | Octree object | 98 | | material | Material object or an array of materials to use for the new three.js mesh | 99 |
100 | 101 | ### OctreeCSG.union: 102 | Merges two Octrees (octreeA and octreeB) to one Octree 103 | 104 | | Parameter | Description | 105 | | --- | --- | 106 | | octreeA | First octree object | 107 | | octreeB | Second octree object | 108 | | buildTargetOctree | (Optional) Specifies if to build the target Octree tree or return a flat Octree (true / flase). **Default**: true | 109 | ```js 110 | const geometry = new THREE.BoxGeometry(10, 10, 10); 111 | const material1 = new THREE.MeshStandardMaterial({ color: 0xff0000 }); 112 | const material2 = new THREE.MeshStandardMaterial({ color: 0x0000ff }); 113 | const mesh1 = new THREE.Mesh(geometry, material1); 114 | const mesh2 = new THREE.Mesh(geometry.clone(), material2); 115 | mesh2.position.set(5, -5, 5); 116 | const octreeA = OctreeCSG.fromMesh(mesh1); 117 | const octreeB = OctreeCSG.fromMesh(mesh2); 118 | 119 | const resultOctree = OctreeCSG.union(octreeA, octreeB); 120 | 121 | const resultMesh = OctreeCSG.toMesh(resultOctree, mesh1.material.clone()); 122 | scene.add(resultMesh); 123 | ``` 124 |
125 | 126 | ### OctreeCSG.subtract: 127 | Subtracts octreeB from octreeA and returns the result Octree 128 | 129 | | Parameter | Description | 130 | | --- | --- | 131 | | octreeA | First octree object | 132 | | octreeB | Second octree object | 133 | | buildTargetOctree | (Optional) Specifies if to build the target Octree tree or return a flat Octree (true / flase). **Default**: true | 134 | ```js 135 | const geometry = new THREE.BoxGeometry(10, 10, 10); 136 | const material1 = new THREE.MeshStandardMaterial({ color: 0xff0000 }); 137 | const material2 = new THREE.MeshStandardMaterial({ color: 0x0000ff }); 138 | const mesh1 = new THREE.Mesh(geometry, material1); 139 | const mesh2 = new THREE.Mesh(geometry.clone(), material2); 140 | mesh2.position.set(5, -5, 5); 141 | const octreeA = OctreeCSG.fromMesh(mesh1); 142 | const octreeB = OctreeCSG.fromMesh(mesh2); 143 | 144 | const resultOctree = OctreeCSG.subtract(octreeA, octreeB); 145 | 146 | const resultMesh = OctreeCSG.toMesh(resultOctree, mesh1.material.clone()); 147 | scene.add(resultMesh); 148 | ``` 149 |
150 | 151 | ### OctreeCSG.intersect: 152 | Returns the intersection of octreeA and octreeB 153 | 154 | | Parameter | Description | 155 | | --- | --- | 156 | | octreeA | First octree object | 157 | | octreeB | Second octree object | 158 | | buildTargetOctree | (Optional) Specifies if to build the target Octree tree or return a flat Octree (true / flase). **Default**: true | 159 | ```js 160 | const geometry = new THREE.BoxGeometry(10, 10, 10); 161 | const material1 = new THREE.MeshStandardMaterial({ color: 0xff0000 }); 162 | const material2 = new THREE.MeshStandardMaterial({ color: 0x0000ff }); 163 | const mesh1 = new THREE.Mesh(geometry, material1); 164 | const mesh2 = new THREE.Mesh(geometry.clone(), material2); 165 | mesh2.position.set(5, -5, 5); 166 | const octreeA = OctreeCSG.fromMesh(mesh1); 167 | const octreeB = OctreeCSG.fromMesh(mesh2); 168 | 169 | const resultOctree = OctreeCSG.intersect(octreeA, octreeB); 170 | 171 | const resultMesh = OctreeCSG.toMesh(resultOctree, mesh1.material.clone()); 172 | scene.add(resultMesh); 173 | ``` 174 |
175 | 176 | 177 | 178 | ### OctreeCSG.operation 179 | CSG Hierarchy of Operations (syntax may change), provides a simple method to combine several CSG operations into one 180 | 181 | | Parameter | Description | 182 | | --- | --- | 183 | | obj | Input object with the CSG hierarchy | 184 | | returnOctrees | (Optional) Specifies whether to return the Octrees as part of the result or not (true / false). **Default**: false | 185 | 186 | Input object structure: 187 | | Key | Expected Value | 188 | | --- | --- | 189 | | op | Type of operation to perform as string, options: union, subtract and intersect | 190 | | material | (Optional) Used only in the root level of the object, if a material is provided the returned object will be a three.js mesh instead of an Octree. Value can be a single material or an array of materials | 191 | | objA | First object, can be a three.js mesh, Octree or a sub-structure of the CSG operation | 192 | | objB | Second object, can be a three.js mesh, Octree or a sub-structure of the CSG operation | 193 | ```js 194 | let baseMaterial = new THREE.MeshBasicMaterial({ color: 0x0000ff }); 195 | let cubeGeometry = new THREE.BoxGeometry(10, 10, 10); 196 | let sphereGeometry = new THREE.SphereGeometry(6.5, 64, 32); 197 | let baseCylinderGeometry = new THREE.CylinderGeometry(3, 3, 20, 64); 198 | 199 | let cubeMesh = new THREE.Mesh(cubeGeometry, baseMaterial.clone()); 200 | let sphereMesh = new THREE.Mesh(sphereGeometry, baseMaterial.clone()); 201 | let cylinderMesh1 = new THREE.Mesh(baseCylinderGeometry.clone(), baseMaterial.clone()); 202 | let cylinderMesh2 = new THREE.Mesh(baseCylinderGeometry.clone(), baseMaterial.clone()); 203 | let cylinderMesh3 = new THREE.Mesh(baseCylinderGeometry.clone(), baseMaterial.clone()); 204 | 205 | cubeMesh.material.color.set(0xff0000); 206 | sphereMesh.material.color.set(0x0000ff); 207 | cylinderMesh1.material.color.set(0x00ff00); 208 | cylinderMesh2.material.color.set(0x00ff00); 209 | cylinderMesh3.material.color.set(0x00ff00); 210 | cylinderMesh2.rotation.set(0, 0, THREE.MathUtils.degToRad(90)); 211 | cylinderMesh3.rotation.set(THREE.MathUtils.degToRad(90), 0, 0); 212 | 213 | let result = OctreeCSG.operation({ 214 | op: "subtract", 215 | material: [cubeMesh.material, sphereMesh.material, cylinderMesh1.material, cylinderMesh2.material, cylinderMesh3.material], 216 | objA: { 217 | op: "intersect", 218 | objA: cubeMesh, 219 | objB: sphereMesh 220 | }, 221 | objB: { 222 | op: "union", 223 | objA: { 224 | op: "union", 225 | objA: cylinderMesh1, 226 | objB: cylinderMesh2, 227 | }, 228 | objB: cylinderMesh3 229 | } 230 | }); 231 | scene.add(result); 232 | ``` 233 |
234 | 235 | ## Array Operations 236 | OctreeCSG provides 3 methods to perform CSG operations on an array of meshes / octrees 237 | 238 | | Parameter | Description | 239 | | --- | --- | 240 | | objArr | An array of meshes or octrees to perform the CSG operation on | 241 | | materialIndexMax | (Optional) Can be used to specify the maximum number of groups in the result Octree. **Default**: Infinity | 242 | 243 | List of Methods: 244 | - OctreeCSG.unionArray - Union operation on an array of meshes 245 | - OctreeCSG.subtractArray - Subtract operation on an array of meshes 246 | - OctreeCSG.intersectArray - Intersect operation on an array of meshes 247 |
248 | 249 | ## Asynchronous Operations 250 | OctreeCSG provides asynchronous CSG methods for all the advanced CSG operations. 251 | 252 | List of Methods: 253 | - OctreeCSG.async.union 254 | - OctreeCSG.async.subtract 255 | - OctreeCSG.async.intersect 256 | - OctreeCSG.async.operation 257 | - OctreeCSG.async.unionArray 258 | - OctreeCSG.async.subtractArray 259 | - OctreeCSG.async.intersectArray 260 |
261 | 262 | ## OctreeCSG Flags 263 | The following flags and variables control how OctreeCSG operates. 264 | 265 | | Flag / Variable | Default Value | Description | 266 | | --- | --- | --- | 267 | | OctreeCSG.useOctreeRay | true | Determines if to use OctreeCSG's ray intersection logic or use three.js's intersection logic (Raycaster.intersectObject). **Options**: true, false | 268 | | OctreeCSG.rayIntersectTriangleType | MollerTrumbore | Determines which ray-triangle intersection algorithm to use. three.js's ray-triangle intersection algorithm proved to be not accurate enough for CSG operations during testing so the [Möller–Trumbore ray-triangle intersection algorithm](https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm) was implemented. **Options**: MollerTrumbore, regular (uses three.js's Ray.intersectTriangle) | 269 | | OctreeCSG.useWindingNumber | false | Determines if to use the ray-triangle intersection algorithm or the [Winding number algorithm](https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm). The Winding number alogirthm can be more accurate than the ray-triangle algorithm on some occasions at the cost of performance. **Options**: true, false | 270 | | OctreeCSG.maxLevel | 16 | Maximum number of sub-Octree levels in the tree | 271 | | OctreeCSG.polygonsPerTree | 100 | Minimum number of polygons (triangles) in a sub-Octree before a split is needed | 272 |
273 | 274 | ## Examples 275 | - [CSG Operations on basic geometries](https://giladdarshan.github.io/OctreeCSG/examples/basic.html) 276 | - [Real-Time CSG](https://giladdarshan.github.io/OctreeCSG/examples/realtime1.html) - Demonstrating the use of async CSG operations (OctreeCSG.async.operation & OctreeCSG.async.unionArray) in real-time CSG 277 | 278 | More examples coming soon. 279 |
280 | 281 | ## Resources 282 | - The Polygon, Vertex and Plane classes were adapted from [THREE-CSGMesh](https://github.com/manthrax/THREE-CSGMesh) 283 | - The Winding number algorithm is based on this [code](https://github.com/grame-cncm/faust/blob/master-dev/tools/physicalModeling/mesh2faust/vega/libraries/windingNumber/windingNumber.cpp) 284 | - The Möller–Trumbore ray-triangle intersection algorithm is based on this [code](https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm#C++_implementation) 285 | - The triangle-triangle intersection logic and algorithm is based on this [code](https://github.com/benardp/contours/blob/master/freestyle/view_map/triangle_triangle_intersection.c) 286 | -------------------------------------------------------------------------------- /OctreeCSG/three-triangle-intersection.js: -------------------------------------------------------------------------------- 1 | import { Vector2, Vector3 } from 'three'; 2 | const _v1 = new Vector3(); 3 | const _v2 = new Vector3(); 4 | const _v3 = new Vector3(); 5 | 6 | // https://github.com/benardp/contours/blob/master/freestyle/view_map/triangle_triangle_intersection.c 7 | function triangleIntersectsTriangle(triangleA, triangleB, additions = { coplanar: false, source: new Vector3(), target: new Vector3() }) { 8 | let p1 = triangleA.a; 9 | let q1 = triangleA.b; 10 | let r1 = triangleA.c; 11 | 12 | let p2 = triangleB.a; 13 | let q2 = triangleB.b; 14 | let r2 = triangleB.c; 15 | 16 | // Compute distance signs of p1, q1 and r1 17 | // to the plane of triangleB (p2,q2,r2) 18 | 19 | // _v1.copy(triangleB.a).sub(triangleB.c); 20 | // _v2.copy(triangleB.b).sub(triangleB.c); 21 | _v1.copy(p2).sub(r2); 22 | _v2.copy(q2).sub(r2); 23 | let N2 = (new Vector3()).copy(_v1).cross(_v2); 24 | 25 | _v1.copy(p1).sub(r2); 26 | let dp1 = _v1.dot(N2); 27 | _v1.copy(q1).sub(r2); 28 | let dq1 = _v1.dot(N2); 29 | _v1.copy(r1).sub(r2); 30 | let dr1 = _v1.dot(N2); 31 | 32 | if (((dp1 * dq1) > 0) && ((dp1 * dr1) > 0)) { 33 | // console.log("test 1 out"); 34 | return false; 35 | } 36 | 37 | // Compute distance signs of p2, q2 and r2 38 | // to the plane of triangleA (p1,q1,r1) 39 | _v1.copy(q1).sub(p1); 40 | _v2.copy(r1).sub(p1); 41 | let N1 = (new Vector3()).copy(_v1).cross(_v2); 42 | 43 | _v1.copy(p2).sub(r1); 44 | let dp2 = _v1.dot(N1); 45 | _v1.copy(q2).sub(r1); 46 | let dq2 = _v1.dot(N1); 47 | _v1.copy(r2).sub(r1); 48 | let dr2 = _v1.dot(N1); 49 | 50 | if (((dp2 * dq2) > 0) & ((dp2 * dr2) > 0)) { 51 | // console.log("test 2 out"); 52 | return false; 53 | } 54 | 55 | 56 | // test 57 | // if (zero_test(dp1) || zero_test(dq1) || zero_test(dr1) || zero_test(dp2) || zero_test(dq2) || zero_test(dr2)) { 58 | // additions.coplanar = 1; 59 | // return false; 60 | // } 61 | 62 | additions.N2 = N2; 63 | additions.N1 = N1; 64 | 65 | if (dp1 > 0) { 66 | if (dq1 > 0) { 67 | return tri_tri_intersection(r1, p1, q1, p2, r2, q2, dp2, dr2, dq2, additions); 68 | } 69 | else if (dr1 > 0) { 70 | return tri_tri_intersection(q1, r1, p1, p2, r2, q2, dp2, dr2, dq2, additions); 71 | } 72 | else { 73 | return tri_tri_intersection(p1, q1, r1, p2, q2, r2, dp2, dq2, dr2, additions); 74 | } 75 | } 76 | else if (dp1 < 0) { 77 | if (dq1 < 0) { 78 | return tri_tri_intersection(r1, p1, q1, p2, q2, r2, dp2, dq2, dr2, additions); 79 | } 80 | else if (dr1 < 0) { 81 | return tri_tri_intersection(q1, r1, p1, p2, q2, r2, dp2, dq2, dr2, additions); 82 | } 83 | else { 84 | return tri_tri_intersection(p1, q1, r1, p2, r2, q2, dp2, dr2, dq2, additions); 85 | } 86 | } 87 | else { 88 | if (dq1 < 0) { 89 | if (dr1 >= 0) { 90 | return tri_tri_intersection(q1, r1, p1, p2, r2, q2, dp2, dr2, dq2, additions); 91 | } 92 | else { 93 | return tri_tri_intersection(p1, q1, r1, p2, q2, r2, dp2, dq2, dr2, additions); 94 | } 95 | } 96 | else if (dq1 > 0) { 97 | if (dr1 > 0) { 98 | return tri_tri_intersection(p1, q1, r1, p2, r2, q2, dp2, dr2, dq2, additions); 99 | } 100 | else { 101 | return tri_tri_intersection(q1, r1, p1, p2, q2, r2, dp2, dq2, dr2, additions); 102 | } 103 | } 104 | else { 105 | if (dr1 > 0) { 106 | return tri_tri_intersection(r1, p1, q1, p2, q2, r2, dp2, dq2, dr2, additions); 107 | } 108 | else if (dr1 < 0) { 109 | return tri_tri_intersection(r1, p1, q1, p2, r2, q2, dp2, dr2, dq2, additions); 110 | } 111 | else { 112 | // triangles are co-planar 113 | additions.coplanar = true; 114 | return coplanar_tri_tri3d(p1, q1, r1, p2, q2, r2, N1, N2); 115 | } 116 | } 117 | } 118 | 119 | 120 | } 121 | 122 | function zero_test(x) { 123 | return (x == 0); 124 | } 125 | function tri_tri_intersection(p1, q1, r1, p2, q2, r2, dp2, dq2, dr2, additions) { 126 | if (dp2 > 0) { 127 | if (dq2 > 0) { 128 | return construct_intersection(p1, r1, q1, r2, p2, q2, additions); 129 | } 130 | else if (dr2 > 0) { 131 | return construct_intersection(p1, r1, q1, q2, r2, p2, additions); 132 | } 133 | else { 134 | return construct_intersection(p1, q1, r1, p2, q2, r2, additions); 135 | } 136 | } 137 | else if (dp2 < 0) { 138 | if (dq2 < 0) { 139 | return construct_intersection(p1, q1, r1, r2, p2, q2, additions); 140 | } 141 | else if (dr2 < 0) { 142 | return construct_intersection(p1, q1, r1, q2, r2, p2, additions); 143 | } 144 | else { 145 | return construct_intersection(p1, r1, q1, p2, q2, r2, additions); 146 | } 147 | } 148 | else { 149 | if (dq2 < 0) { 150 | if (dr2 >= 0) { 151 | return construct_intersection(p1, r1, q1, q2, r2, p2, additions); 152 | } 153 | else { 154 | return construct_intersection(p1, q1, r1, p2, q2, r2, additions); 155 | } 156 | } 157 | else if (dq2 > 0) { 158 | if (dr2 > 0) { 159 | return construct_intersection(p1, r1, q1, p2, q2, r2, additions); 160 | } 161 | else { 162 | return construct_intersection(p1, q1, r1, q2, r2, p2, additions); 163 | } 164 | } 165 | else { 166 | if (dr2 > 0) { 167 | return construct_intersection(p1, q1, r1, r2, p2, q2, additions); 168 | } 169 | else if (dr2 < 0) { 170 | return construct_intersection(p1, r1, q1, r2, p2, q2, additions); 171 | } 172 | else { 173 | additions.coplanar = true; 174 | // return coplanar_tri_tri3d(p1, q1, r1, p2, q2, r2, additions); 175 | return coplanar_tri_tri3d(p1, q1, r1, p2, q2, r2, additions.N1, additions.N2); 176 | } 177 | } 178 | } 179 | } 180 | 181 | function coplanar_tri_tri3d(p1, q1, r1, p2, q2, r2, normal_1, normal_2) { 182 | let P1 = new Vector2(), Q1 = new Vector2(), R1 = new Vector2(); 183 | let P2 = new Vector2(), Q2 = new Vector2(), R2 = new Vector2(); 184 | let n_x, n_y, n_z; 185 | 186 | n_x = normal_1.x < 0 ? -normal_1.x : normal_1.x; 187 | n_y = normal_1.y < 0 ? -normal_1.y : normal_1.y; 188 | n_z = normal_1.z < 0 ? -normal_1.z : normal_1.z; 189 | 190 | /* Projection of the triangles in 3D onto 2D such that the area of 191 | the projection is maximized. */ 192 | 193 | if ((n_x > n_z) && (n_x >= n_y)) { // Project onto plane YZ 194 | P1.z = p1.z, P1.y = p1.y; 195 | Q1.z = q1.z, Q1.y = q1.y; 196 | R1.z = r1.z, R1.y = r1.y; 197 | 198 | P2.z = p2.z, P2.y = p2.y; 199 | Q2.z = q2.z, Q2.y = q2.y; 200 | R2.z = r2.z, R2.y = r2.y; 201 | } 202 | else if ((n_y > n_z) && (n_y >= n_x)) { // Project onto plane XZ 203 | P1.x = p1.x, P1.z = p1.z; 204 | Q1.x = q1.x, Q1.z = q1.z; 205 | R1.x = r1.x, R1.z = r1.z; 206 | 207 | P2.x = p2.x, P2.z = p2.z; 208 | Q2.x = q2.x, Q2.z = q2.z; 209 | R2.x = r2.x, R2.z = r2.z; 210 | } 211 | else { // Project onto plane XY 212 | P1.x = p1.x, P1.y = p1.y; 213 | Q1.x = q1.x, Q1.y = q1.y; 214 | R1.x = r1.x, R1.y = r1.y; 215 | 216 | P2.x = p2.x, P2.y = p2.y; 217 | Q2.x = q2.x, Q2.y = q2.y; 218 | R2.x = r2.x, R2.y = r2.y; 219 | } 220 | 221 | return tri_tri_overlap_test_2d(P1, Q1, R1, P2, Q2, R2); 222 | 223 | } 224 | 225 | function tri_tri_overlap_test_2d(p1, q1, r1, p2, q2, r2) { 226 | if (ORIENT_2D(p1, q1, r1) < 0) { 227 | if (ORIENT_2D(p2, q2, r2) < 0) { 228 | return ccw_tri_tri_intersection_2d(p1, r1, q1, p2, r2, q2); 229 | } 230 | else { 231 | return ccw_tri_tri_intersection_2d(p1, r1, q1, p2, q2, r2); 232 | } 233 | } 234 | else { 235 | if (ORIENT_2D(p2, q2, r2) < 0) { 236 | return ccw_tri_tri_intersection_2d(p1, q1, r1, p2, r2, q2); 237 | } 238 | else { 239 | return ccw_tri_tri_intersection_2d(p1, q1, r1, p2, q2, r2); 240 | } 241 | } 242 | } 243 | 244 | function ORIENT_2D(a, b, c) { 245 | return ((a.x - c.x) * (b.y - c.y) - (a.y - c.y) * (b.x - c.x)); 246 | } 247 | 248 | function ccw_tri_tri_intersection_2d(p1, q1, r1, p2, q2, r2) { 249 | if (ORIENT_2D(p2, q2, p1) >= 0) { 250 | if (ORIENT_2D(q2, r2, p1) >= 0) { 251 | if (ORIENT_2D(r2, p2, p1) >= 0) { 252 | return true; 253 | } 254 | else { 255 | return intersection_test_edge(p1, q1, r1, p2, q2, r2); 256 | } 257 | } 258 | else { 259 | if (ORIENT_2D(r2, p2, p1) >= 0) { 260 | return intersection_test_edge(p1, q1, r1, r2, p2, q2); 261 | } 262 | else { 263 | return intersection_test_vertex(p1, q1, r1, p2, q2, r2) 264 | } 265 | } 266 | } 267 | else { 268 | if (ORIENT_2D(q2, r2, p1) >= 0) { 269 | if (ORIENT_2D(r2, p2, p1) >= 0) { 270 | return intersection_test_edge(p1, q1, r2, q2, r2, p2); 271 | } 272 | else { 273 | return intersection_test_vertex(p1, q1, r1, q2, r2, p2); 274 | } 275 | } 276 | else { 277 | return intersection_test_vertex(p1, q1, r1, r2, p2, q2); 278 | } 279 | } 280 | } 281 | 282 | function intersection_test_edge(P1, Q1, R1, P2, Q2, R2) { 283 | if (ORIENT_2D(R2, P2, Q1) >= 0) { 284 | if (ORIENT_2D(P1, P2, Q1) >= 0) { 285 | if (ORIENT_2D(P1, Q1, R2) >= 0) { 286 | return true; 287 | } 288 | else { 289 | return false; 290 | } 291 | } 292 | else { 293 | if (ORIENT_2D(Q1, R1, P2) >= 0) { 294 | if (ORIENT_2D(R1, P1, P2) >= 0) { 295 | return true; 296 | } 297 | else { 298 | return false; 299 | } 300 | } 301 | else { 302 | return false; 303 | } 304 | } 305 | } else { 306 | if (ORIENT_2D(R2, P2, R1) >= 0) { 307 | if (ORIENT_2D(P1, P2, R1) >= 0) { 308 | if (ORIENT_2D(P1, R1, R2) >= 0) { 309 | return true; 310 | } 311 | else { 312 | if (ORIENT_2D(Q1, R1, R2) >= 0) { 313 | return true; 314 | } 315 | else { 316 | return false; 317 | } 318 | } 319 | } 320 | else { 321 | return false; 322 | } 323 | } 324 | else { 325 | return false; 326 | } 327 | } 328 | } 329 | 330 | function intersection_test_vertex(P1, Q1, R1, P2, Q2, R2) { 331 | if (ORIENT_2D(R2, P2, Q1) >= 0) { 332 | if (ORIENT_2D(R2, Q2, Q1) <= 0) { 333 | if (ORIENT_2D(P1, P2, Q1) > 0) { 334 | if (ORIENT_2D(P1, Q2, Q1) <= 0) { 335 | return true; 336 | } 337 | else { 338 | return false; 339 | } 340 | } 341 | else { 342 | if (ORIENT_2D(P1, P2, R1) >= 0) { 343 | if (ORIENT_2D(Q1, R1, P2) >= 0) { 344 | return true; 345 | } 346 | else { 347 | return false; 348 | } 349 | } 350 | else { 351 | return false; 352 | } 353 | } 354 | } 355 | else { 356 | if (ORIENT_2D(P1, Q2, Q1) <= 0) { 357 | if (ORIENT_2D(R2, Q2, R1) <= 0) { 358 | if (ORIENT_2D(Q1, R1, Q2) >= 0) { 359 | return true; 360 | } 361 | else { 362 | return false; 363 | } 364 | } 365 | else { 366 | return false; 367 | } 368 | } 369 | else { 370 | return false; 371 | } 372 | } 373 | } 374 | else { 375 | if (ORIENT_2D(R2, P2, R1) >= 0) { 376 | if (ORIENT_2D(Q1, R1, R2) >= 0) { 377 | if (ORIENT_2D(P1, P2, R1) >= 0) { 378 | return true; 379 | } 380 | else { 381 | return false; 382 | } 383 | } 384 | else { 385 | if (ORIENT_2D(Q1, R1, Q2) >= 0) { 386 | if (ORIENT_2D(R2, R1, Q2) >= 0) { 387 | return true; 388 | } 389 | else { 390 | return false; 391 | } 392 | } 393 | else { 394 | return false; 395 | } 396 | } 397 | } 398 | else { 399 | return false; 400 | } 401 | } 402 | }; 403 | function construct_intersection(p1, q1, r1, p2, q2, r2, additions) { 404 | let alpha; 405 | let N = new Vector3(); 406 | _v1.subVectors(q1, p1); 407 | _v2.subVectors(r2, p1); 408 | N.copy(_v1).cross(_v2); 409 | _v3.subVectors(p2, p1); 410 | if (_v3.dot(N) > 0) { 411 | _v1.subVectors(r1, p1); 412 | N.copy(_v1).cross(_v2); 413 | if (_v3.dot(N) <= 0) { 414 | _v2.subVectors(q2, p1); 415 | N.copy(_v1).cross(_v2); 416 | if (_v3.dot(N) > 0) { 417 | _v1.subVectors(p1, p2); 418 | _v2.subVectors(p1, r1); 419 | alpha = _v1.dot(additions.N2) / _v2.dot(additions.N2); 420 | _v1.copy(_v2).multiplyScalar(alpha); 421 | additions.source.subVectors(p1, _v1); 422 | _v1.subVectors(p2, p1); 423 | _v2.subVectors(p2, r2); 424 | alpha = _v1.dot(additions.N1) / _v2.dot(additions.N1); 425 | _v1.copy(_v2).multiplyScalar(alpha); 426 | additions.target.subVectors(p2, _v1); 427 | return true; 428 | } 429 | else { 430 | _v1.subVectors(p2, p1); 431 | _v2.subVectors(p2, q2); 432 | alpha = _v1.dot(additions.N1) / _v2.dot(additions.N1); 433 | _v1.copy(_v2).multiplyScalar(alpha); 434 | additions.source.subVectors(p2, _v1); 435 | _v1.subVectors(p2, p1); 436 | _v2.subVectors(p2, r2); 437 | alpha = _v1.dot(additions.N1) / _v2.dot(additions.N1); 438 | _v1.copy(_v2).multiplyScalar(alpha); 439 | additions.target.subVectors(p2, _v1); 440 | return true; 441 | } 442 | } 443 | else { 444 | return false; 445 | } 446 | } 447 | else { 448 | _v2.subVectors(q2, p1); 449 | N.copy(_v1).cross(_v2); 450 | if (_v3.dot(N) < 0) { 451 | return false; 452 | } 453 | else { 454 | _v1.subVectors(r1, p1); 455 | N.copy(_v1).cross(_v2); 456 | if (_v3.dot(N) >= 0) { 457 | _v1.subVectors(p1, p2); 458 | _v2.subVectors(p1, r1); 459 | alpha = _v1.dot(additions.N2) / _v2.dot(additions.N2); 460 | _v1.copy(_v2).multiplyScalar(alpha); 461 | additions.source.subVectors(p1, _v1); 462 | _v1.subVectors(p1, p2); 463 | _v2.subVectors(p1, q1); 464 | alpha = _v1.dot(additions.N2) / _v2.dot(additions.N2); 465 | _v1.copy(_v2).multiplyScalar(alpha); 466 | additions.target.subVectors(p1, _v1); 467 | return true; 468 | } 469 | else { 470 | _v1.subVectors(p2, p1); 471 | _v2.subVectors(p2, q2); 472 | alpha = _v1.dot(additions.N1) / _v2.dot(additions.N1); 473 | _v1.copy(_v2).multiplyScalar(alpha); 474 | additions.source.subVectors(p2, _v1); 475 | _v1.subVectors(p1, p2); 476 | _v2.subVectors(p1, q1); 477 | alpha = _v1.dot(additions.N2) / _v2.dot(additions.N2); 478 | _v1.copy(_v2).multiplyScalar(alpha); 479 | additions.target.subVectors(p1, _v1); 480 | return true; 481 | } 482 | } 483 | } 484 | } 485 | function pointOnLine(line, point) { 486 | let ab = _v1.copy(line.end).sub(line.start); 487 | let ac = _v2.copy(point).sub(line.start); 488 | let area = _v3.copy(ab).cross(ac).length(); 489 | let CD = area / ab.length(); 490 | return CD; 491 | } 492 | function lineIntersects(line1, line2, points) { 493 | const r = (new Vector3()).copy(line1.end).sub(line1.start); 494 | const s = (new Vector3()).copy(line2.end).sub(line2.start); 495 | const q = (new Vector3()).copy(line1.start).sub(line2.start); 496 | // const w = _v3.copy( line2.start ).sub( line1.start ); 497 | 498 | let dotqr = q.dot(r); 499 | let dotqs = q.dot(s); 500 | let dotrs = r.dot(s); 501 | let dotrr = r.dot(r); 502 | let dotss = s.dot(s); 503 | 504 | let denom = (dotrr * dotss) - (dotrs * dotrs); 505 | let numer = (dotqs * dotrs) - (dotqr * dotss); 506 | 507 | let t = numer / denom; 508 | let u = (dotqs + t * dotrs) / dotss; 509 | 510 | let p0 = r.multiplyScalar(t).add(line1.start); 511 | let p1 = s.multiplyScalar(u).add(line2.start); 512 | 513 | let onSegment = false; 514 | let intersects = false; 515 | 516 | if ((0 <= t) && (t <= 1) && (0<= u) && (u<=1)) { 517 | onSegment = true; 518 | } 519 | let p0p1Length = _v1.copy(p0).sub(p1).length(); 520 | if (p0p1Length <= 1e-5) { 521 | intersects = true; 522 | } 523 | // console.log("lineIntersects?", intersects, onSegment, p0, p1, denom, numer, t, u); 524 | if (!(intersects && onSegment)) { 525 | // return []; 526 | return false; 527 | } 528 | points && points.push(p0, p1); 529 | // return [p0, p1]; 530 | return true; 531 | } 532 | function getLines(triangle) { 533 | return [ 534 | { start: triangle.a, end: triangle.b }, 535 | { start: triangle.b, end: triangle.c }, 536 | { start: triangle.c, end: triangle.a } 537 | ]; 538 | } 539 | 540 | function checkTrianglesIntersection(triangle1, triangle2, additions = { coplanar: false, source: new Vector3(), target: new Vector3() }) { 541 | // let additions = { 542 | // coplanar: false, 543 | // source: new Vector3(), 544 | // target: new Vector3() 545 | // }; 546 | let triangleIntersects = triangleIntersectsTriangle(triangle1, triangle2, additions); 547 | // console.log("??? 1", triangleIntersects, additions); 548 | additions.triangleCheck = triangleIntersects; 549 | if (!triangleIntersects && additions.coplanar) { 550 | // console.log("check failed, checking lines"); 551 | let triangle1Lines = getLines(triangle1); 552 | let triangle2Lines = getLines(triangle2); 553 | let intersects = false; 554 | for (let i = 0; i < 3; i++) { 555 | intersects = false; 556 | for (let j = 0; j < 3; j++) { 557 | intersects = lineIntersects(triangle1Lines[i], triangle2Lines[j]); 558 | if (intersects) { 559 | break; 560 | } 561 | } 562 | if (intersects) { 563 | break; 564 | } 565 | } 566 | return intersects; 567 | } 568 | return triangleIntersects; 569 | } 570 | export { triangleIntersectsTriangle, checkTrianglesIntersection, getLines, lineIntersects }; 571 | -------------------------------------------------------------------------------- /examples/js/OctreeCSG/three-triangle-intersection.js: -------------------------------------------------------------------------------- 1 | import { Vector2, Vector3 } from 'three'; 2 | const _v1 = new Vector3(); 3 | const _v2 = new Vector3(); 4 | const _v3 = new Vector3(); 5 | 6 | // https://github.com/benardp/contours/blob/master/freestyle/view_map/triangle_triangle_intersection.c 7 | function triangleIntersectsTriangle(triangleA, triangleB, additions = { coplanar: false, source: new Vector3(), target: new Vector3() }) { 8 | let p1 = triangleA.a; 9 | let q1 = triangleA.b; 10 | let r1 = triangleA.c; 11 | 12 | let p2 = triangleB.a; 13 | let q2 = triangleB.b; 14 | let r2 = triangleB.c; 15 | 16 | // Compute distance signs of p1, q1 and r1 17 | // to the plane of triangleB (p2,q2,r2) 18 | 19 | // _v1.copy(triangleB.a).sub(triangleB.c); 20 | // _v2.copy(triangleB.b).sub(triangleB.c); 21 | _v1.copy(p2).sub(r2); 22 | _v2.copy(q2).sub(r2); 23 | let N2 = (new Vector3()).copy(_v1).cross(_v2); 24 | 25 | _v1.copy(p1).sub(r2); 26 | let dp1 = _v1.dot(N2); 27 | _v1.copy(q1).sub(r2); 28 | let dq1 = _v1.dot(N2); 29 | _v1.copy(r1).sub(r2); 30 | let dr1 = _v1.dot(N2); 31 | 32 | if (((dp1 * dq1) > 0) && ((dp1 * dr1) > 0)) { 33 | // console.log("test 1 out"); 34 | return false; 35 | } 36 | 37 | // Compute distance signs of p2, q2 and r2 38 | // to the plane of triangleA (p1,q1,r1) 39 | _v1.copy(q1).sub(p1); 40 | _v2.copy(r1).sub(p1); 41 | let N1 = (new Vector3()).copy(_v1).cross(_v2); 42 | 43 | _v1.copy(p2).sub(r1); 44 | let dp2 = _v1.dot(N1); 45 | _v1.copy(q2).sub(r1); 46 | let dq2 = _v1.dot(N1); 47 | _v1.copy(r2).sub(r1); 48 | let dr2 = _v1.dot(N1); 49 | 50 | if (((dp2 * dq2) > 0) & ((dp2 * dr2) > 0)) { 51 | // console.log("test 2 out"); 52 | return false; 53 | } 54 | 55 | 56 | // test 57 | // if (zero_test(dp1) || zero_test(dq1) || zero_test(dr1) || zero_test(dp2) || zero_test(dq2) || zero_test(dr2)) { 58 | // additions.coplanar = 1; 59 | // return false; 60 | // } 61 | 62 | additions.N2 = N2; 63 | additions.N1 = N1; 64 | 65 | if (dp1 > 0) { 66 | if (dq1 > 0) { 67 | return tri_tri_intersection(r1, p1, q1, p2, r2, q2, dp2, dr2, dq2, additions); 68 | } 69 | else if (dr1 > 0) { 70 | return tri_tri_intersection(q1, r1, p1, p2, r2, q2, dp2, dr2, dq2, additions); 71 | } 72 | else { 73 | return tri_tri_intersection(p1, q1, r1, p2, q2, r2, dp2, dq2, dr2, additions); 74 | } 75 | } 76 | else if (dp1 < 0) { 77 | if (dq1 < 0) { 78 | return tri_tri_intersection(r1, p1, q1, p2, q2, r2, dp2, dq2, dr2, additions); 79 | } 80 | else if (dr1 < 0) { 81 | return tri_tri_intersection(q1, r1, p1, p2, q2, r2, dp2, dq2, dr2, additions); 82 | } 83 | else { 84 | return tri_tri_intersection(p1, q1, r1, p2, r2, q2, dp2, dr2, dq2, additions); 85 | } 86 | } 87 | else { 88 | if (dq1 < 0) { 89 | if (dr1 >= 0) { 90 | return tri_tri_intersection(q1, r1, p1, p2, r2, q2, dp2, dr2, dq2, additions); 91 | } 92 | else { 93 | return tri_tri_intersection(p1, q1, r1, p2, q2, r2, dp2, dq2, dr2, additions); 94 | } 95 | } 96 | else if (dq1 > 0) { 97 | if (dr1 > 0) { 98 | return tri_tri_intersection(p1, q1, r1, p2, r2, q2, dp2, dr2, dq2, additions); 99 | } 100 | else { 101 | return tri_tri_intersection(q1, r1, p1, p2, q2, r2, dp2, dq2, dr2, additions); 102 | } 103 | } 104 | else { 105 | if (dr1 > 0) { 106 | return tri_tri_intersection(r1, p1, q1, p2, q2, r2, dp2, dq2, dr2, additions); 107 | } 108 | else if (dr1 < 0) { 109 | return tri_tri_intersection(r1, p1, q1, p2, r2, q2, dp2, dr2, dq2, additions); 110 | } 111 | else { 112 | // triangles are co-planar 113 | additions.coplanar = true; 114 | return coplanar_tri_tri3d(p1, q1, r1, p2, q2, r2, N1, N2); 115 | } 116 | } 117 | } 118 | 119 | 120 | } 121 | 122 | function zero_test(x) { 123 | return (x == 0); 124 | } 125 | function tri_tri_intersection(p1, q1, r1, p2, q2, r2, dp2, dq2, dr2, additions) { 126 | if (dp2 > 0) { 127 | if (dq2 > 0) { 128 | return construct_intersection(p1, r1, q1, r2, p2, q2, additions); 129 | } 130 | else if (dr2 > 0) { 131 | return construct_intersection(p1, r1, q1, q2, r2, p2, additions); 132 | } 133 | else { 134 | return construct_intersection(p1, q1, r1, p2, q2, r2, additions); 135 | } 136 | } 137 | else if (dp2 < 0) { 138 | if (dq2 < 0) { 139 | return construct_intersection(p1, q1, r1, r2, p2, q2, additions); 140 | } 141 | else if (dr2 < 0) { 142 | return construct_intersection(p1, q1, r1, q2, r2, p2, additions); 143 | } 144 | else { 145 | return construct_intersection(p1, r1, q1, p2, q2, r2, additions); 146 | } 147 | } 148 | else { 149 | if (dq2 < 0) { 150 | if (dr2 >= 0) { 151 | return construct_intersection(p1, r1, q1, q2, r2, p2, additions); 152 | } 153 | else { 154 | return construct_intersection(p1, q1, r1, p2, q2, r2, additions); 155 | } 156 | } 157 | else if (dq2 > 0) { 158 | if (dr2 > 0) { 159 | return construct_intersection(p1, r1, q1, p2, q2, r2, additions); 160 | } 161 | else { 162 | return construct_intersection(p1, q1, r1, q2, r2, p2, additions); 163 | } 164 | } 165 | else { 166 | if (dr2 > 0) { 167 | return construct_intersection(p1, q1, r1, r2, p2, q2, additions); 168 | } 169 | else if (dr2 < 0) { 170 | return construct_intersection(p1, r1, q1, r2, p2, q2, additions); 171 | } 172 | else { 173 | additions.coplanar = true; 174 | // return coplanar_tri_tri3d(p1, q1, r1, p2, q2, r2, additions); 175 | return coplanar_tri_tri3d(p1, q1, r1, p2, q2, r2, additions.N1, additions.N2); 176 | } 177 | } 178 | } 179 | } 180 | 181 | function coplanar_tri_tri3d(p1, q1, r1, p2, q2, r2, normal_1, normal_2) { 182 | let P1 = new Vector2(), Q1 = new Vector2(), R1 = new Vector2(); 183 | let P2 = new Vector2(), Q2 = new Vector2(), R2 = new Vector2(); 184 | let n_x, n_y, n_z; 185 | 186 | n_x = normal_1.x < 0 ? -normal_1.x : normal_1.x; 187 | n_y = normal_1.y < 0 ? -normal_1.y : normal_1.y; 188 | n_z = normal_1.z < 0 ? -normal_1.z : normal_1.z; 189 | 190 | /* Projection of the triangles in 3D onto 2D such that the area of 191 | the projection is maximized. */ 192 | 193 | if ((n_x > n_z) && (n_x >= n_y)) { // Project onto plane YZ 194 | P1.z = p1.z, P1.y = p1.y; 195 | Q1.z = q1.z, Q1.y = q1.y; 196 | R1.z = r1.z, R1.y = r1.y; 197 | 198 | P2.z = p2.z, P2.y = p2.y; 199 | Q2.z = q2.z, Q2.y = q2.y; 200 | R2.z = r2.z, R2.y = r2.y; 201 | } 202 | else if ((n_y > n_z) && (n_y >= n_x)) { // Project onto plane XZ 203 | P1.x = p1.x, P1.z = p1.z; 204 | Q1.x = q1.x, Q1.z = q1.z; 205 | R1.x = r1.x, R1.z = r1.z; 206 | 207 | P2.x = p2.x, P2.z = p2.z; 208 | Q2.x = q2.x, Q2.z = q2.z; 209 | R2.x = r2.x, R2.z = r2.z; 210 | } 211 | else { // Project onto plane XY 212 | P1.x = p1.x, P1.y = p1.y; 213 | Q1.x = q1.x, Q1.y = q1.y; 214 | R1.x = r1.x, R1.y = r1.y; 215 | 216 | P2.x = p2.x, P2.y = p2.y; 217 | Q2.x = q2.x, Q2.y = q2.y; 218 | R2.x = r2.x, R2.y = r2.y; 219 | } 220 | 221 | return tri_tri_overlap_test_2d(P1, Q1, R1, P2, Q2, R2); 222 | 223 | } 224 | 225 | function tri_tri_overlap_test_2d(p1, q1, r1, p2, q2, r2) { 226 | if (ORIENT_2D(p1, q1, r1) < 0) { 227 | if (ORIENT_2D(p2, q2, r2) < 0) { 228 | return ccw_tri_tri_intersection_2d(p1, r1, q1, p2, r2, q2); 229 | } 230 | else { 231 | return ccw_tri_tri_intersection_2d(p1, r1, q1, p2, q2, r2); 232 | } 233 | } 234 | else { 235 | if (ORIENT_2D(p2, q2, r2) < 0) { 236 | return ccw_tri_tri_intersection_2d(p1, q1, r1, p2, r2, q2); 237 | } 238 | else { 239 | return ccw_tri_tri_intersection_2d(p1, q1, r1, p2, q2, r2); 240 | } 241 | } 242 | } 243 | 244 | function ORIENT_2D(a, b, c) { 245 | return ((a.x - c.x) * (b.y - c.y) - (a.y - c.y) * (b.x - c.x)); 246 | } 247 | 248 | function ccw_tri_tri_intersection_2d(p1, q1, r1, p2, q2, r2) { 249 | if (ORIENT_2D(p2, q2, p1) >= 0) { 250 | if (ORIENT_2D(q2, r2, p1) >= 0) { 251 | if (ORIENT_2D(r2, p2, p1) >= 0) { 252 | return true; 253 | } 254 | else { 255 | return intersection_test_edge(p1, q1, r1, p2, q2, r2); 256 | } 257 | } 258 | else { 259 | if (ORIENT_2D(r2, p2, p1) >= 0) { 260 | return intersection_test_edge(p1, q1, r1, r2, p2, q2); 261 | } 262 | else { 263 | return intersection_test_vertex(p1, q1, r1, p2, q2, r2) 264 | } 265 | } 266 | } 267 | else { 268 | if (ORIENT_2D(q2, r2, p1) >= 0) { 269 | if (ORIENT_2D(r2, p2, p1) >= 0) { 270 | return intersection_test_edge(p1, q1, r2, q2, r2, p2); 271 | } 272 | else { 273 | return intersection_test_vertex(p1, q1, r1, q2, r2, p2); 274 | } 275 | } 276 | else { 277 | return intersection_test_vertex(p1, q1, r1, r2, p2, q2); 278 | } 279 | } 280 | } 281 | 282 | function intersection_test_edge(P1, Q1, R1, P2, Q2, R2) { 283 | if (ORIENT_2D(R2, P2, Q1) >= 0) { 284 | if (ORIENT_2D(P1, P2, Q1) >= 0) { 285 | if (ORIENT_2D(P1, Q1, R2) >= 0) { 286 | return true; 287 | } 288 | else { 289 | return false; 290 | } 291 | } 292 | else { 293 | if (ORIENT_2D(Q1, R1, P2) >= 0) { 294 | if (ORIENT_2D(R1, P1, P2) >= 0) { 295 | return true; 296 | } 297 | else { 298 | return false; 299 | } 300 | } 301 | else { 302 | return false; 303 | } 304 | } 305 | } else { 306 | if (ORIENT_2D(R2, P2, R1) >= 0) { 307 | if (ORIENT_2D(P1, P2, R1) >= 0) { 308 | if (ORIENT_2D(P1, R1, R2) >= 0) { 309 | return true; 310 | } 311 | else { 312 | if (ORIENT_2D(Q1, R1, R2) >= 0) { 313 | return true; 314 | } 315 | else { 316 | return false; 317 | } 318 | } 319 | } 320 | else { 321 | return false; 322 | } 323 | } 324 | else { 325 | return false; 326 | } 327 | } 328 | } 329 | 330 | function intersection_test_vertex(P1, Q1, R1, P2, Q2, R2) { 331 | if (ORIENT_2D(R2, P2, Q1) >= 0) { 332 | if (ORIENT_2D(R2, Q2, Q1) <= 0) { 333 | if (ORIENT_2D(P1, P2, Q1) > 0) { 334 | if (ORIENT_2D(P1, Q2, Q1) <= 0) { 335 | return true; 336 | } 337 | else { 338 | return false; 339 | } 340 | } 341 | else { 342 | if (ORIENT_2D(P1, P2, R1) >= 0) { 343 | if (ORIENT_2D(Q1, R1, P2) >= 0) { 344 | return true; 345 | } 346 | else { 347 | return false; 348 | } 349 | } 350 | else { 351 | return false; 352 | } 353 | } 354 | } 355 | else { 356 | if (ORIENT_2D(P1, Q2, Q1) <= 0) { 357 | if (ORIENT_2D(R2, Q2, R1) <= 0) { 358 | if (ORIENT_2D(Q1, R1, Q2) >= 0) { 359 | return true; 360 | } 361 | else { 362 | return false; 363 | } 364 | } 365 | else { 366 | return false; 367 | } 368 | } 369 | else { 370 | return false; 371 | } 372 | } 373 | } 374 | else { 375 | if (ORIENT_2D(R2, P2, R1) >= 0) { 376 | if (ORIENT_2D(Q1, R1, R2) >= 0) { 377 | if (ORIENT_2D(P1, P2, R1) >= 0) { 378 | return true; 379 | } 380 | else { 381 | return false; 382 | } 383 | } 384 | else { 385 | if (ORIENT_2D(Q1, R1, Q2) >= 0) { 386 | if (ORIENT_2D(R2, R1, Q2) >= 0) { 387 | return true; 388 | } 389 | else { 390 | return false; 391 | } 392 | } 393 | else { 394 | return false; 395 | } 396 | } 397 | } 398 | else { 399 | return false; 400 | } 401 | } 402 | }; 403 | function construct_intersection(p1, q1, r1, p2, q2, r2, additions) { 404 | let alpha; 405 | let N = new Vector3(); 406 | _v1.subVectors(q1, p1); 407 | _v2.subVectors(r2, p1); 408 | N.copy(_v1).cross(_v2); 409 | _v3.subVectors(p2, p1); 410 | if (_v3.dot(N) > 0) { 411 | _v1.subVectors(r1, p1); 412 | N.copy(_v1).cross(_v2); 413 | if (_v3.dot(N) <= 0) { 414 | _v2.subVectors(q2, p1); 415 | N.copy(_v1).cross(_v2); 416 | if (_v3.dot(N) > 0) { 417 | _v1.subVectors(p1, p2); 418 | _v2.subVectors(p1, r1); 419 | alpha = _v1.dot(additions.N2) / _v2.dot(additions.N2); 420 | _v1.copy(_v2).multiplyScalar(alpha); 421 | additions.source.subVectors(p1, _v1); 422 | _v1.subVectors(p2, p1); 423 | _v2.subVectors(p2, r2); 424 | alpha = _v1.dot(additions.N1) / _v2.dot(additions.N1); 425 | _v1.copy(_v2).multiplyScalar(alpha); 426 | additions.target.subVectors(p2, _v1); 427 | return true; 428 | } 429 | else { 430 | _v1.subVectors(p2, p1); 431 | _v2.subVectors(p2, q2); 432 | alpha = _v1.dot(additions.N1) / _v2.dot(additions.N1); 433 | _v1.copy(_v2).multiplyScalar(alpha); 434 | additions.source.subVectors(p2, _v1); 435 | _v1.subVectors(p2, p1); 436 | _v2.subVectors(p2, r2); 437 | alpha = _v1.dot(additions.N1) / _v2.dot(additions.N1); 438 | _v1.copy(_v2).multiplyScalar(alpha); 439 | additions.target.subVectors(p2, _v1); 440 | return true; 441 | } 442 | } 443 | else { 444 | return false; 445 | } 446 | } 447 | else { 448 | _v2.subVectors(q2, p1); 449 | N.copy(_v1).cross(_v2); 450 | if (_v3.dot(N) < 0) { 451 | return false; 452 | } 453 | else { 454 | _v1.subVectors(r1, p1); 455 | N.copy(_v1).cross(_v2); 456 | if (_v3.dot(N) >= 0) { 457 | _v1.subVectors(p1, p2); 458 | _v2.subVectors(p1, r1); 459 | alpha = _v1.dot(additions.N2) / _v2.dot(additions.N2); 460 | _v1.copy(_v2).multiplyScalar(alpha); 461 | additions.source.subVectors(p1, _v1); 462 | _v1.subVectors(p1, p2); 463 | _v2.subVectors(p1, q1); 464 | alpha = _v1.dot(additions.N2) / _v2.dot(additions.N2); 465 | _v1.copy(_v2).multiplyScalar(alpha); 466 | additions.target.subVectors(p1, _v1); 467 | return true; 468 | } 469 | else { 470 | _v1.subVectors(p2, p1); 471 | _v2.subVectors(p2, q2); 472 | alpha = _v1.dot(additions.N1) / _v2.dot(additions.N1); 473 | _v1.copy(_v2).multiplyScalar(alpha); 474 | additions.source.subVectors(p2, _v1); 475 | _v1.subVectors(p1, p2); 476 | _v2.subVectors(p1, q1); 477 | alpha = _v1.dot(additions.N2) / _v2.dot(additions.N2); 478 | _v1.copy(_v2).multiplyScalar(alpha); 479 | additions.target.subVectors(p1, _v1); 480 | return true; 481 | } 482 | } 483 | } 484 | } 485 | function pointOnLine(line, point) { 486 | let ab = _v1.copy(line.end).sub(line.start); 487 | let ac = _v2.copy(point).sub(line.start); 488 | let area = _v3.copy(ab).cross(ac).length(); 489 | let CD = area / ab.length(); 490 | return CD; 491 | } 492 | function lineIntersects(line1, line2, points) { 493 | const r = (new Vector3()).copy(line1.end).sub(line1.start); 494 | const s = (new Vector3()).copy(line2.end).sub(line2.start); 495 | const q = (new Vector3()).copy(line1.start).sub(line2.start); 496 | // const w = _v3.copy( line2.start ).sub( line1.start ); 497 | 498 | let dotqr = q.dot(r); 499 | let dotqs = q.dot(s); 500 | let dotrs = r.dot(s); 501 | let dotrr = r.dot(r); 502 | let dotss = s.dot(s); 503 | 504 | let denom = (dotrr * dotss) - (dotrs * dotrs); 505 | let numer = (dotqs * dotrs) - (dotqr * dotss); 506 | 507 | let t = numer / denom; 508 | let u = (dotqs + t * dotrs) / dotss; 509 | 510 | let p0 = r.multiplyScalar(t).add(line1.start); 511 | let p1 = s.multiplyScalar(u).add(line2.start); 512 | 513 | let onSegment = false; 514 | let intersects = false; 515 | 516 | if ((0 <= t) && (t <= 1) && (0<= u) && (u<=1)) { 517 | onSegment = true; 518 | } 519 | let p0p1Length = _v1.copy(p0).sub(p1).length(); 520 | if (p0p1Length <= 1e-5) { 521 | intersects = true; 522 | } 523 | // console.log("lineIntersects?", intersects, onSegment, p0, p1, denom, numer, t, u); 524 | if (!(intersects && onSegment)) { 525 | // return []; 526 | return false; 527 | } 528 | points && points.push(p0, p1); 529 | // return [p0, p1]; 530 | return true; 531 | } 532 | function getLines(triangle) { 533 | return [ 534 | { start: triangle.a, end: triangle.b }, 535 | { start: triangle.b, end: triangle.c }, 536 | { start: triangle.c, end: triangle.a } 537 | ]; 538 | } 539 | 540 | function checkTrianglesIntersection(triangle1, triangle2, additions = { coplanar: false, source: new Vector3(), target: new Vector3() }) { 541 | // let additions = { 542 | // coplanar: false, 543 | // source: new Vector3(), 544 | // target: new Vector3() 545 | // }; 546 | let triangleIntersects = triangleIntersectsTriangle(triangle1, triangle2, additions); 547 | // console.log("??? 1", triangleIntersects, additions); 548 | additions.triangleCheck = triangleIntersects; 549 | if (!triangleIntersects && additions.coplanar) { 550 | // console.log("check failed, checking lines"); 551 | let triangle1Lines = getLines(triangle1); 552 | let triangle2Lines = getLines(triangle2); 553 | let intersects = false; 554 | for (let i = 0; i < 3; i++) { 555 | intersects = false; 556 | for (let j = 0; j < 3; j++) { 557 | intersects = lineIntersects(triangle1Lines[i], triangle2Lines[j]); 558 | if (intersects) { 559 | break; 560 | } 561 | } 562 | if (intersects) { 563 | break; 564 | } 565 | } 566 | return intersects; 567 | } 568 | return triangleIntersects; 569 | } 570 | export { triangleIntersectsTriangle, checkTrianglesIntersection, getLines, lineIntersects }; 571 | -------------------------------------------------------------------------------- /examples/js/lil-gui.module.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * lil-gui 3 | * https://lil-gui.georgealways.com 4 | * @version 0.16.0 5 | * @author George Michael Brower 6 | * @license MIT 7 | */ 8 | class t{constructor(i,e,s,n,r="div"){this.parent=i,this.object=e,this.property=s,this._disabled=!1,this.initialValue=this.getValue(),this.domElement=document.createElement("div"),this.domElement.classList.add("controller"),this.domElement.classList.add(n),this.$name=document.createElement("div"),this.$name.classList.add("name"),t.nextNameID=t.nextNameID||0,this.$name.id="lil-gui-name-"+ ++t.nextNameID,this.$widget=document.createElement(r),this.$widget.classList.add("widget"),this.$disable=this.$widget,this.domElement.appendChild(this.$name),this.domElement.appendChild(this.$widget),this.parent.children.push(this),this.parent.controllers.push(this),this.parent.$children.appendChild(this.domElement),this._listenCallback=this._listenCallback.bind(this),this.name(s)}name(t){return this._name=t,this.$name.innerHTML=t,this}onChange(t){return this._onChange=t,this}_callOnChange(){this.parent._callOnChange(this),void 0!==this._onChange&&this._onChange.call(this,this.getValue()),this._changed=!0}onFinishChange(t){return this._onFinishChange=t,this}_callOnFinishChange(){this._changed&&(this.parent._callOnFinishChange(this),void 0!==this._onFinishChange&&this._onFinishChange.call(this,this.getValue())),this._changed=!1}reset(){return this.setValue(this.initialValue),this._callOnFinishChange(),this}enable(t=!0){return this.disable(!t)}disable(t=!0){return t===this._disabled||(this._disabled=t,this.domElement.classList.toggle("disabled",t),this.$disable.toggleAttribute("disabled",t)),this}options(t){const i=this.parent.add(this.object,this.property,t);return i.name(this._name),this.destroy(),i}min(t){return this}max(t){return this}step(t){return this}listen(t=!0){return this._listening=t,void 0!==this._listenCallbackID&&(cancelAnimationFrame(this._listenCallbackID),this._listenCallbackID=void 0),this._listening&&this._listenCallback(),this}_listenCallback(){this._listenCallbackID=requestAnimationFrame(this._listenCallback),this.updateDisplay()}getValue(){return this.object[this.property]}setValue(t){return this.object[this.property]=t,this._callOnChange(),this.updateDisplay(),this}updateDisplay(){return this}load(t){return this.setValue(t),this._callOnFinishChange(),this}save(){return this.getValue()}destroy(){this.parent.children.splice(this.parent.children.indexOf(this),1),this.parent.controllers.splice(this.parent.controllers.indexOf(this),1),this.parent.$children.removeChild(this.domElement)}}class i extends t{constructor(t,i,e){super(t,i,e,"boolean","label"),this.$input=document.createElement("input"),this.$input.setAttribute("type","checkbox"),this.$input.setAttribute("aria-labelledby",this.$name.id),this.$widget.appendChild(this.$input),this.$input.addEventListener("change",()=>{this.setValue(this.$input.checked),this._callOnFinishChange()}),this.$disable=this.$input,this.updateDisplay()}updateDisplay(){return this.$input.checked=this.getValue(),this}}function e(t){let i,e;return(i=t.match(/(#|0x)?([a-f0-9]{6})/i))?e=i[2]:(i=t.match(/rgb\(\s*(\d*)\s*,\s*(\d*)\s*,\s*(\d*)\s*\)/))?e=parseInt(i[1]).toString(16).padStart(2,0)+parseInt(i[2]).toString(16).padStart(2,0)+parseInt(i[3]).toString(16).padStart(2,0):(i=t.match(/^#?([a-f0-9])([a-f0-9])([a-f0-9])$/i))&&(e=i[1]+i[1]+i[2]+i[2]+i[3]+i[3]),!!e&&"#"+e}const s={isPrimitive:!0,match:t=>"string"==typeof t,fromHexString:e,toHexString:e},n={isPrimitive:!0,match:t=>"number"==typeof t,fromHexString:t=>parseInt(t.substring(1),16),toHexString:t=>"#"+t.toString(16).padStart(6,0)},r={isPrimitive:!1,match:Array.isArray,fromHexString(t,i,e=1){const s=n.fromHexString(t);i[0]=(s>>16&255)/255*e,i[1]=(s>>8&255)/255*e,i[2]=(255&s)/255*e},toHexString:([t,i,e],s=1)=>n.toHexString(t*(s=255/s)<<16^i*s<<8^e*s<<0)},l={isPrimitive:!1,match:t=>Object(t)===t,fromHexString(t,i,e=1){const s=n.fromHexString(t);i.r=(s>>16&255)/255*e,i.g=(s>>8&255)/255*e,i.b=(255&s)/255*e},toHexString:({r:t,g:i,b:e},s=1)=>n.toHexString(t*(s=255/s)<<16^i*s<<8^e*s<<0)},o=[s,n,r,l];class a extends t{constructor(t,i,s,n){var r;super(t,i,s,"color"),this.$input=document.createElement("input"),this.$input.setAttribute("type","color"),this.$input.setAttribute("tabindex",-1),this.$input.setAttribute("aria-labelledby",this.$name.id),this.$text=document.createElement("input"),this.$text.setAttribute("type","text"),this.$text.setAttribute("spellcheck","false"),this.$text.setAttribute("aria-labelledby",this.$name.id),this.$display=document.createElement("div"),this.$display.classList.add("display"),this.$display.appendChild(this.$input),this.$widget.appendChild(this.$display),this.$widget.appendChild(this.$text),this._format=(r=this.initialValue,o.find(t=>t.match(r))),this._rgbScale=n,this._initialValueHexString=this.save(),this._textFocused=!1,this.$input.addEventListener("input",()=>{this._setValueFromHexString(this.$input.value)}),this.$input.addEventListener("blur",()=>{this._callOnFinishChange()}),this.$text.addEventListener("input",()=>{const t=e(this.$text.value);t&&this._setValueFromHexString(t)}),this.$text.addEventListener("focus",()=>{this._textFocused=!0,this.$text.select()}),this.$text.addEventListener("blur",()=>{this._textFocused=!1,this.updateDisplay(),this._callOnFinishChange()}),this.$disable=this.$text,this.updateDisplay()}reset(){return this._setValueFromHexString(this._initialValueHexString),this}_setValueFromHexString(t){if(this._format.isPrimitive){const i=this._format.fromHexString(t);this.setValue(i)}else this._format.fromHexString(t,this.getValue(),this._rgbScale),this._callOnChange(),this.updateDisplay()}save(){return this._format.toHexString(this.getValue(),this._rgbScale)}load(t){return this._setValueFromHexString(t),this._callOnFinishChange(),this}updateDisplay(){return this.$input.value=this._format.toHexString(this.getValue(),this._rgbScale),this._textFocused||(this.$text.value=this.$input.value.substring(1)),this.$display.style.backgroundColor=this.$input.value,this}}class h extends t{constructor(t,i,e){super(t,i,e,"function"),this.$button=document.createElement("button"),this.$button.appendChild(this.$name),this.$widget.appendChild(this.$button),this.$button.addEventListener("click",t=>{t.preventDefault(),this.getValue().call(this.object)}),this.$button.addEventListener("touchstart",()=>{}),this.$disable=this.$button}}class d extends t{constructor(t,i,e,s,n,r){super(t,i,e,"number"),this._initInput(),this.min(s),this.max(n);const l=void 0!==r;this.step(l?r:this._getImplicitStep(),l),this.updateDisplay()}min(t){return this._min=t,this._onUpdateMinMax(),this}max(t){return this._max=t,this._onUpdateMinMax(),this}step(t,i=!0){return this._step=t,this._stepExplicit=i,this}updateDisplay(){const t=this.getValue();if(this._hasSlider){let i=(t-this._min)/(this._max-this._min);i=Math.max(0,Math.min(i,1)),this.$fill.style.width=100*i+"%"}return this._inputFocused||(this.$input.value=t),this}_initInput(){this.$input=document.createElement("input"),this.$input.setAttribute("type","number"),this.$input.setAttribute("step","any"),this.$input.setAttribute("aria-labelledby",this.$name.id),this.$widget.appendChild(this.$input),this.$disable=this.$input;const t=t=>{const i=parseFloat(this.$input.value);isNaN(i)||(this._snapClampSetValue(i+t),this.$input.value=this.getValue())};let i,e,s,n,r,l=!1;const o=t=>{if(l){const s=t.clientX-i,n=t.clientY-e;Math.abs(n)>5?(t.preventDefault(),this.$input.blur(),l=!1,this._setDraggingStyle(!0,"vertical")):Math.abs(s)>5&&a()}if(!l){const i=t.clientY-s;r-=i*this._step*this._arrowKeyMultiplier(t),n+r>this._max?r=this._max-n:n+r{this._setDraggingStyle(!1,"vertical"),this._callOnFinishChange(),window.removeEventListener("mousemove",o),window.removeEventListener("mouseup",a)};this.$input.addEventListener("input",()=>{const t=parseFloat(this.$input.value);isNaN(t)||this.setValue(this._clamp(t))}),this.$input.addEventListener("keydown",i=>{"Enter"===i.code&&this.$input.blur(),"ArrowUp"===i.code&&(i.preventDefault(),t(this._step*this._arrowKeyMultiplier(i))),"ArrowDown"===i.code&&(i.preventDefault(),t(this._step*this._arrowKeyMultiplier(i)*-1))}),this.$input.addEventListener("wheel",i=>{this._inputFocused&&(i.preventDefault(),t(this._step*this._normalizeMouseWheel(i)))}),this.$input.addEventListener("mousedown",t=>{i=t.clientX,e=s=t.clientY,l=!0,n=this.getValue(),r=0,window.addEventListener("mousemove",o),window.addEventListener("mouseup",a)}),this.$input.addEventListener("focus",()=>{this._inputFocused=!0}),this.$input.addEventListener("blur",()=>{this._inputFocused=!1,this.updateDisplay(),this._callOnFinishChange()})}_initSlider(){this._hasSlider=!0,this.$slider=document.createElement("div"),this.$slider.classList.add("slider"),this.$fill=document.createElement("div"),this.$fill.classList.add("fill"),this.$slider.appendChild(this.$fill),this.$widget.insertBefore(this.$slider,this.$input),this.domElement.classList.add("hasSlider");const t=t=>{const i=this.$slider.getBoundingClientRect();let e=(s=t,n=i.left,r=i.right,l=this._min,o=this._max,(s-n)/(r-n)*(o-l)+l);var s,n,r,l,o;this._snapClampSetValue(e)},i=i=>{t(i.clientX)},e=()=>{this._callOnFinishChange(),this._setDraggingStyle(!1),window.removeEventListener("mousemove",i),window.removeEventListener("mouseup",e)};let s,n,r=!1;const l=i=>{i.preventDefault(),this._setDraggingStyle(!0),t(i.touches[0].clientX),r=!1},o=i=>{if(r){const t=i.touches[0].clientX-s,e=i.touches[0].clientY-n;Math.abs(t)>Math.abs(e)?l(i):(window.removeEventListener("touchmove",o),window.removeEventListener("touchend",a))}else i.preventDefault(),t(i.touches[0].clientX)},a=()=>{this._callOnFinishChange(),this._setDraggingStyle(!1),window.removeEventListener("touchmove",o),window.removeEventListener("touchend",a)},h=this._callOnFinishChange.bind(this);let d;this.$slider.addEventListener("mousedown",s=>{this._setDraggingStyle(!0),t(s.clientX),window.addEventListener("mousemove",i),window.addEventListener("mouseup",e)}),this.$slider.addEventListener("touchstart",t=>{t.touches.length>1||(this._hasScrollBar?(s=t.touches[0].clientX,n=t.touches[0].clientY,r=!0):l(t),window.addEventListener("touchmove",o),window.addEventListener("touchend",a))}),this.$slider.addEventListener("wheel",t=>{if(Math.abs(t.deltaX)this._max&&(t=this._max),t}_snapClampSetValue(t){this.setValue(this._clamp(this._snap(t)))}get _hasScrollBar(){const t=this.parent.root.$children;return t.scrollHeight>t.clientHeight}get _hasMin(){return void 0!==this._min}get _hasMax(){return void 0!==this._max}}class c extends t{constructor(t,i,e,s){super(t,i,e,"option"),this.$select=document.createElement("select"),this.$select.setAttribute("aria-labelledby",this.$name.id),this.$display=document.createElement("div"),this.$display.classList.add("display"),this._values=Array.isArray(s)?s:Object.values(s),this._names=Array.isArray(s)?s:Object.keys(s),this._names.forEach(t=>{const i=document.createElement("option");i.innerHTML=t,this.$select.appendChild(i)}),this.$select.addEventListener("change",()=>{this.setValue(this._values[this.$select.selectedIndex]),this._callOnFinishChange()}),this.$select.addEventListener("focus",()=>{this.$display.classList.add("focus")}),this.$select.addEventListener("blur",()=>{this.$display.classList.remove("focus")}),this.$widget.appendChild(this.$select),this.$widget.appendChild(this.$display),this.$disable=this.$select,this.updateDisplay()}updateDisplay(){const t=this.getValue(),i=this._values.indexOf(t);return this.$select.selectedIndex=i,this.$display.innerHTML=-1===i?t:this._names[i],this}}class u extends t{constructor(t,i,e){super(t,i,e,"string"),this.$input=document.createElement("input"),this.$input.setAttribute("type","text"),this.$input.setAttribute("aria-labelledby",this.$name.id),this.$input.addEventListener("input",()=>{this.setValue(this.$input.value)}),this.$input.addEventListener("keydown",t=>{"Enter"===t.code&&this.$input.blur()}),this.$input.addEventListener("blur",()=>{this._callOnFinishChange()}),this.$widget.appendChild(this.$input),this.$disable=this.$input,this.updateDisplay()}updateDisplay(){return this.$input.value=this.getValue(),this}}let p=!1;class g{constructor({parent:t,autoPlace:i=void 0===t,container:e,width:s,title:n="Controls",injectStyles:r=!0,touchStyles:l=!0}={}){if(this.parent=t,this.root=t?t.root:this,this.children=[],this.controllers=[],this.folders=[],this._closed=!1,this._hidden=!1,this.domElement=document.createElement("div"),this.domElement.classList.add("lil-gui"),this.$title=document.createElement("div"),this.$title.classList.add("title"),this.$title.setAttribute("role","button"),this.$title.setAttribute("aria-expanded",!0),this.$title.setAttribute("tabindex",0),this.$title.addEventListener("click",()=>this.openAnimated(this._closed)),this.$title.addEventListener("keydown",t=>{"Enter"!==t.code&&"Space"!==t.code||(t.preventDefault(),this.$title.click())}),this.$title.addEventListener("touchstart",()=>{}),this.$children=document.createElement("div"),this.$children.classList.add("children"),this.domElement.appendChild(this.$title),this.domElement.appendChild(this.$children),this.title(n),l&&this.domElement.classList.add("allow-touch-styles"),this.parent)return this.parent.children.push(this),this.parent.folders.push(this),void this.parent.$children.appendChild(this.domElement);this.domElement.classList.add("root"),!p&&r&&(!function(t){const i=document.createElement("style");i.innerHTML=t;const e=document.querySelector("head link[rel=stylesheet], head style");e?document.head.insertBefore(i,e):document.head.appendChild(i)}('.lil-gui{--background-color:#1f1f1f;--text-color:#ebebeb;--title-background-color:#111;--title-text-color:#ebebeb;--widget-color:#424242;--hover-color:#4f4f4f;--focus-color:#595959;--number-color:#2cc9ff;--string-color:#a2db3c;--font-size:11px;--input-font-size:11px;--font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;--font-family-mono:Menlo,Monaco,Consolas,"Droid Sans Mono",monospace;--padding:4px;--spacing:4px;--widget-height:20px;--name-width:45%;--slider-knob-width:2px;--slider-input-width:27%;--color-input-width:27%;--slider-input-min-width:45px;--color-input-min-width:45px;--folder-indent:7px;--widget-padding:0 0 0 3px;--widget-border-radius:2px;--checkbox-size:calc(var(--widget-height)*0.75);--scrollbar-width:5px;background-color:var(--background-color);color:var(--text-color);font-family:var(--font-family);font-size:var(--font-size);font-style:normal;font-weight:400;line-height:1;text-align:left;touch-action:manipulation;user-select:none;-webkit-user-select:none}.lil-gui,.lil-gui *{box-sizing:border-box;margin:0;padding:0}.lil-gui.root{display:flex;flex-direction:column;width:var(--width,245px)}.lil-gui.root>.title{background:var(--title-background-color);color:var(--title-text-color)}.lil-gui.root>.children{overflow-x:hidden;overflow-y:auto}.lil-gui.root>.children::-webkit-scrollbar{background:var(--background-color);height:var(--scrollbar-width);width:var(--scrollbar-width)}.lil-gui.root>.children::-webkit-scrollbar-thumb{background:var(--focus-color);border-radius:var(--scrollbar-width)}.lil-gui.force-touch-styles{--widget-height:28px;--padding:6px;--spacing:6px;--font-size:13px;--input-font-size:16px;--folder-indent:10px;--scrollbar-width:7px;--slider-input-min-width:50px;--color-input-min-width:65px}.lil-gui.autoPlace{max-height:100%;position:fixed;right:15px;top:0;z-index:1001}.lil-gui .controller{align-items:center;display:flex;margin:var(--spacing) 0;padding:0 var(--padding)}.lil-gui .controller.disabled{opacity:.5}.lil-gui .controller.disabled,.lil-gui .controller.disabled *{pointer-events:none!important}.lil-gui .controller>.name{flex-shrink:0;line-height:var(--widget-height);min-width:var(--name-width);padding-right:var(--spacing);white-space:pre}.lil-gui .controller .widget{align-items:center;display:flex;min-height:var(--widget-height);position:relative;width:100%}.lil-gui .controller.string input{color:var(--string-color)}.lil-gui .controller.boolean .widget{cursor:pointer}.lil-gui .controller.color .display{border-radius:var(--widget-border-radius);height:var(--widget-height);position:relative;width:100%}.lil-gui .controller.color input[type=color]{cursor:pointer;height:100%;opacity:0;width:100%}.lil-gui .controller.color input[type=text]{flex-shrink:0;font-family:var(--font-family-mono);margin-left:var(--spacing);min-width:var(--color-input-min-width);width:var(--color-input-width)}.lil-gui .controller.option select{max-width:100%;opacity:0;position:absolute;width:100%}.lil-gui .controller.option .display{background:var(--widget-color);border-radius:var(--widget-border-radius);height:var(--widget-height);line-height:var(--widget-height);max-width:100%;overflow:hidden;padding-left:.55em;padding-right:1.75em;pointer-events:none;position:relative;word-break:break-all}.lil-gui .controller.option .display.active{background:var(--focus-color)}.lil-gui .controller.option .display:after{bottom:0;content:"↕";font-family:lil-gui;padding-right:.375em;position:absolute;right:0;top:0}.lil-gui .controller.option .widget,.lil-gui .controller.option select{cursor:pointer}.lil-gui .controller.number input{color:var(--number-color)}.lil-gui .controller.number.hasSlider input{flex-shrink:0;margin-left:var(--spacing);min-width:var(--slider-input-min-width);width:var(--slider-input-width)}.lil-gui .controller.number .slider{background-color:var(--widget-color);border-radius:var(--widget-border-radius);cursor:ew-resize;height:var(--widget-height);overflow:hidden;padding-right:var(--slider-knob-width);touch-action:pan-y;width:100%}.lil-gui .controller.number .slider.active{background-color:var(--focus-color)}.lil-gui .controller.number .slider.active .fill{opacity:.95}.lil-gui .controller.number .fill{border-right:var(--slider-knob-width) solid var(--number-color);box-sizing:content-box;height:100%}.lil-gui-dragging .lil-gui{--hover-color:var(--widget-color)}.lil-gui-dragging *{cursor:ew-resize!important}.lil-gui-dragging.lil-gui-vertical *{cursor:ns-resize!important}.lil-gui .title{--title-height:calc(var(--widget-height) + var(--spacing)*1.25);-webkit-tap-highlight-color:transparent;text-decoration-skip:objects;cursor:pointer;font-weight:600;height:var(--title-height);line-height:calc(var(--title-height) - 4px);outline:none;padding:0 var(--padding)}.lil-gui .title:before{content:"▾";display:inline-block;font-family:lil-gui;padding-right:2px}.lil-gui .title:active{background:var(--title-background-color);opacity:.75}.lil-gui.root>.title:focus{text-decoration:none!important}.lil-gui.closed>.title:before{content:"▸"}.lil-gui.closed>.children{opacity:0;transform:translateY(-7px)}.lil-gui.closed:not(.transition)>.children{display:none}.lil-gui.transition>.children{overflow:hidden;pointer-events:none;transition-duration:.3s;transition-property:height,opacity,transform;transition-timing-function:cubic-bezier(.2,.6,.35,1)}.lil-gui .children:empty:before{content:"Empty";display:block;font-style:italic;height:var(--widget-height);line-height:var(--widget-height);margin:var(--spacing) 0;opacity:.5;padding:0 var(--padding)}.lil-gui.root>.children>.lil-gui>.title{border-width:0;border-bottom:1px solid var(--widget-color);border-left:0 solid var(--widget-color);border-right:0 solid var(--widget-color);border-top:1px solid var(--widget-color);transition:border-color .3s}.lil-gui.root>.children>.lil-gui.closed>.title{border-bottom-color:transparent}.lil-gui+.controller{border-top:1px solid var(--widget-color);margin-top:0;padding-top:var(--spacing)}.lil-gui .lil-gui .lil-gui>.title{border:none}.lil-gui .lil-gui .lil-gui>.children{border:none;border-left:2px solid var(--widget-color);margin-left:var(--folder-indent)}.lil-gui .lil-gui .controller{border:none}.lil-gui input{-webkit-tap-highlight-color:transparent;background:var(--widget-color);border:0;border-radius:var(--widget-border-radius);color:var(--text-color);font-family:var(--font-family);font-size:var(--input-font-size);height:var(--widget-height);outline:none;width:100%}.lil-gui input:disabled{opacity:1}.lil-gui input[type=number],.lil-gui input[type=text]{padding:var(--widget-padding)}.lil-gui input[type=number]:focus,.lil-gui input[type=text]:focus{background:var(--focus-color)}.lil-gui input::-webkit-inner-spin-button,.lil-gui input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.lil-gui input[type=number]{-moz-appearance:textfield}.lil-gui input[type=checkbox]{appearance:none;-webkit-appearance:none;border-radius:var(--widget-border-radius);cursor:pointer;height:var(--checkbox-size);text-align:center;width:var(--checkbox-size)}.lil-gui input[type=checkbox]:checked:before{content:"✓";font-family:lil-gui;font-size:var(--checkbox-size);line-height:var(--checkbox-size)}.lil-gui button{-webkit-tap-highlight-color:transparent;background:var(--widget-color);border:1px solid var(--widget-color);border-radius:var(--widget-border-radius);color:var(--text-color);cursor:pointer;font-family:var(--font-family);font-size:var(--font-size);height:var(--widget-height);line-height:calc(var(--widget-height) - 4px);outline:none;text-align:center;text-transform:none;width:100%}.lil-gui button:active{background:var(--focus-color)}@font-face{font-family:lil-gui;src:url("data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAUsAAsAAAAACJwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAAH4AAADAImwmYE9TLzIAAAGIAAAAPwAAAGBKqH5SY21hcAAAAcgAAAD0AAACrukyyJBnbHlmAAACvAAAAF8AAACEIZpWH2hlYWQAAAMcAAAAJwAAADZfcj2zaGhlYQAAA0QAAAAYAAAAJAC5AHhobXR4AAADXAAAABAAAABMAZAAAGxvY2EAAANsAAAAFAAAACgCEgIybWF4cAAAA4AAAAAeAAAAIAEfABJuYW1lAAADoAAAASIAAAIK9SUU/XBvc3QAAATEAAAAZgAAAJCTcMc2eJxVjbEOgjAURU+hFRBK1dGRL+ALnAiToyMLEzFpnPz/eAshwSa97517c/MwwJmeB9kwPl+0cf5+uGPZXsqPu4nvZabcSZldZ6kfyWnomFY/eScKqZNWupKJO6kXN3K9uCVoL7iInPr1X5baXs3tjuMqCtzEuagm/AAlzQgPAAB4nGNgYRBlnMDAysDAYM/gBiT5oLQBAwuDJAMDEwMrMwNWEJDmmsJwgCFeXZghBcjlZMgFCzOiKOIFAB71Bb8AeJy1kjFuwkAQRZ+DwRAwBtNQRUGKQ8OdKCAWUhAgKLhIuAsVSpWz5Bbkj3dEgYiUIszqWdpZe+Z7/wB1oCYmIoboiwiLT2WjKl/jscrHfGg/pKdMkyklC5Zs2LEfHYpjcRoPzme9MWWmk3dWbK9ObkWkikOetJ554fWyoEsmdSlt+uR0pCJR34b6t/TVg1SY3sYvdf8vuiKrpyaDXDISiegp17p7579Gp3p++y7HPAiY9pmTibljrr85qSidtlg4+l25GLCaS8e6rRxNBmsnERunKbaOObRz7N72ju5vdAjYpBXHgJylOAVsMseDAPEP8LYoUHicY2BiAAEfhiAGJgZWBgZ7RnFRdnVJELCQlBSRlATJMoLV2DK4glSYs6ubq5vbKrJLSbGrgEmovDuDJVhe3VzcXFwNLCOILB/C4IuQ1xTn5FPilBTj5FPmBAB4WwoqAHicY2BkYGAA4sk1sR/j+W2+MnAzpDBgAyEMQUCSg4EJxAEAwUgFHgB4nGNgZGBgSGFggJMhDIwMqEAYAByHATJ4nGNgAIIUNEwmAABl3AGReJxjYAACIQYlBiMGJ3wQAEcQBEV4nGNgZGBgEGZgY2BiAAEQyQWEDAz/wXwGAAsPATIAAHicXdBNSsNAHAXwl35iA0UQXYnMShfS9GPZA7T7LgIu03SSpkwzYTIt1BN4Ak/gKTyAeCxfw39jZkjymzcvAwmAW/wgwHUEGDb36+jQQ3GXGot79L24jxCP4gHzF/EIr4jEIe7wxhOC3g2TMYy4Q7+Lu/SHuEd/ivt4wJd4wPxbPEKMX3GI5+DJFGaSn4qNzk8mcbKSR6xdXdhSzaOZJGtdapd4vVPbi6rP+cL7TGXOHtXKll4bY1Xl7EGnPtp7Xy2n00zyKLVHfkHBa4IcJ2oD3cgggWvt/V/FbDrUlEUJhTn/0azVWbNTNr0Ens8de1tceK9xZmfB1CPjOmPH4kitmvOubcNpmVTN3oFJyjzCvnmrwhJTzqzVj9jiSX911FjeAAB4nG3HMRKCMBBA0f0giiKi4DU8k0V2GWbIZDOh4PoWWvq6J5V8If9NVNQcaDhyouXMhY4rPTcG7jwYmXhKq8Wz+p762aNaeYXom2n3m2dLTVgsrCgFJ7OTmIkYbwIbC6vIB7WmFfAAAA==") format("woff")}@media (pointer:coarse){.lil-gui.allow-touch-styles{--widget-height:28px;--padding:6px;--spacing:6px;--font-size:13px;--input-font-size:16px;--folder-indent:10px;--scrollbar-width:7px;--slider-input-min-width:50px;--color-input-min-width:65px}}@media (hover:hover){.lil-gui .controller.color .display:hover:before{border:1px solid #fff9;border-radius:var(--widget-border-radius);bottom:0;content:" ";display:block;left:0;position:absolute;right:0;top:0}.lil-gui .controller.option .display.focus{background:var(--focus-color)}.lil-gui .controller.option .widget:hover .display{background:var(--hover-color)}.lil-gui .controller.number .slider:hover{background-color:var(--hover-color)}body:not(.lil-gui-dragging) .lil-gui .title:hover{background:var(--title-background-color);opacity:.85}.lil-gui .title:focus{text-decoration:underline var(--focus-color)}.lil-gui input:hover{background:var(--hover-color)}.lil-gui input:active{background:var(--focus-color)}.lil-gui input[type=checkbox]:focus{box-shadow:inset 0 0 0 1px var(--focus-color)}.lil-gui button:hover{background:var(--hover-color);border-color:var(--hover-color)}.lil-gui button:focus{border-color:var(--focus-color)}}'),p=!0),e?e.appendChild(this.domElement):i&&(this.domElement.classList.add("autoPlace"),document.body.appendChild(this.domElement)),s&&this.domElement.style.setProperty("--width",s+"px"),this.domElement.addEventListener("keydown",t=>t.stopPropagation()),this.domElement.addEventListener("keyup",t=>t.stopPropagation())}add(t,e,s,n,r){if(Object(s)===s)return new c(this,t,e,s);const l=t[e];switch(typeof l){case"number":return new d(this,t,e,s,n,r);case"boolean":return new i(this,t,e);case"string":return new u(this,t,e);case"function":return new h(this,t,e)}console.error("gui.add failed\n\tproperty:",e,"\n\tobject:",t,"\n\tvalue:",l)}addColor(t,i,e=1){return new a(this,t,i,e)}addFolder(t){return new g({parent:this,title:t})}load(t,i=!0){return t.controllers&&this.controllers.forEach(i=>{i instanceof h||i._name in t.controllers&&i.load(t.controllers[i._name])}),i&&t.folders&&this.folders.forEach(i=>{i._title in t.folders&&i.load(t.folders[i._title])}),this}save(t=!0){const i={controllers:{},folders:{}};return this.controllers.forEach(t=>{if(!(t instanceof h)){if(t._name in i.controllers)throw new Error(`Cannot save GUI with duplicate property "${t._name}"`);i.controllers[t._name]=t.save()}}),t&&this.folders.forEach(t=>{if(t._title in i.folders)throw new Error(`Cannot save GUI with duplicate folder "${t._title}"`);i.folders[t._title]=t.save()}),i}open(t=!0){return this._closed=!t,this.$title.setAttribute("aria-expanded",!this._closed),this.domElement.classList.toggle("closed",this._closed),this}close(){return this.open(!1)}show(t=!0){return this._hidden=!t,this.domElement.style.display=this._hidden?"none":"",this}hide(){return this.show(!1)}openAnimated(t=!0){return this._closed=!t,this.$title.setAttribute("aria-expanded",!this._closed),requestAnimationFrame(()=>{const i=this.$children.clientHeight;this.$children.style.height=i+"px",this.domElement.classList.add("transition");const e=t=>{t.target===this.$children&&(this.$children.style.height="",this.domElement.classList.remove("transition"),this.$children.removeEventListener("transitionend",e))};this.$children.addEventListener("transitionend",e);const s=t?this.$children.scrollHeight:0;this.domElement.classList.toggle("closed",!t),requestAnimationFrame(()=>{this.$children.style.height=s+"px"})}),this}title(t){return this._title=t,this.$title.innerHTML=t,this}reset(t=!0){return(t?this.controllersRecursive():this.controllers).forEach(t=>t.reset()),this}onChange(t){return this._onChange=t,this}_callOnChange(t){this.parent&&this.parent._callOnChange(t),void 0!==this._onChange&&this._onChange.call(this,{object:t.object,property:t.property,value:t.getValue(),controller:t})}onFinishChange(t){return this._onFinishChange=t,this}_callOnFinishChange(t){this.parent&&this.parent._callOnFinishChange(t),void 0!==this._onFinishChange&&this._onFinishChange.call(this,{object:t.object,property:t.property,value:t.getValue(),controller:t})}destroy(){this.parent&&(this.parent.children.splice(this.parent.children.indexOf(this),1),this.parent.folders.splice(this.parent.folders.indexOf(this),1)),this.domElement.parentElement&&this.domElement.parentElement.removeChild(this.domElement),Array.from(this.children).forEach(t=>t.destroy())}controllersRecursive(){let t=Array.from(this.controllers);return this.folders.forEach(i=>{t=t.concat(i.controllersRecursive())}),t}foldersRecursive(){let t=Array.from(this.folders);return this.folders.forEach(i=>{t=t.concat(i.foldersRecursive())}),t}}export default g;export{i as BooleanController,a as ColorController,t as Controller,h as FunctionController,g as GUI,d as NumberController,c as OptionController,u as StringController}; 9 | -------------------------------------------------------------------------------- /examples/js/OrbitControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | MOUSE, 4 | Quaternion, 5 | Spherical, 6 | TOUCH, 7 | Vector2, 8 | Vector3 9 | } from 'three'; 10 | 11 | // This set of controls performs orbiting, dollying (zooming), and panning. 12 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 13 | // 14 | // Orbit - left mouse / touch: one-finger move 15 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 16 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 17 | 18 | const _changeEvent = { type: 'change' }; 19 | const _startEvent = { type: 'start' }; 20 | const _endEvent = { type: 'end' }; 21 | 22 | class OrbitControls extends EventDispatcher { 23 | 24 | constructor( object, domElement ) { 25 | 26 | super(); 27 | 28 | this.object = object; 29 | this.domElement = domElement; 30 | this.domElement.style.touchAction = 'none'; // disable touch scroll 31 | 32 | // Set to false to disable this control 33 | this.enabled = true; 34 | 35 | // "target" sets the location of focus, where the object orbits around 36 | this.target = new Vector3(); 37 | 38 | // How far you can dolly in and out ( PerspectiveCamera only ) 39 | this.minDistance = 0; 40 | this.maxDistance = Infinity; 41 | 42 | // How far you can zoom in and out ( OrthographicCamera only ) 43 | this.minZoom = 0; 44 | this.maxZoom = Infinity; 45 | 46 | // How far you can orbit vertically, upper and lower limits. 47 | // Range is 0 to Math.PI radians. 48 | this.minPolarAngle = 0; // radians 49 | this.maxPolarAngle = Math.PI; // radians 50 | 51 | // How far you can orbit horizontally, upper and lower limits. 52 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) 53 | this.minAzimuthAngle = - Infinity; // radians 54 | this.maxAzimuthAngle = Infinity; // radians 55 | 56 | // Set to true to enable damping (inertia) 57 | // If damping is enabled, you must call controls.update() in your animation loop 58 | this.enableDamping = false; 59 | this.dampingFactor = 0.05; 60 | 61 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 62 | // Set to false to disable zooming 63 | this.enableZoom = true; 64 | this.zoomSpeed = 1.0; 65 | 66 | // Set to false to disable rotating 67 | this.enableRotate = true; 68 | this.rotateSpeed = 1.0; 69 | 70 | // Set to false to disable panning 71 | this.enablePan = true; 72 | this.panSpeed = 1.0; 73 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up 74 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 75 | 76 | // Set to true to automatically rotate around the target 77 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 78 | this.autoRotate = false; 79 | this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 80 | 81 | // The four arrow keys 82 | this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }; 83 | 84 | // Mouse buttons 85 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; 86 | 87 | // Touch fingers 88 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; 89 | 90 | // for reset 91 | this.target0 = this.target.clone(); 92 | this.position0 = this.object.position.clone(); 93 | this.zoom0 = this.object.zoom; 94 | 95 | // the target DOM element for key events 96 | this._domElementKeyEvents = null; 97 | 98 | // 99 | // public methods 100 | // 101 | 102 | this.getPolarAngle = function () { 103 | 104 | return spherical.phi; 105 | 106 | }; 107 | 108 | this.getAzimuthalAngle = function () { 109 | 110 | return spherical.theta; 111 | 112 | }; 113 | 114 | this.getDistance = function () { 115 | 116 | return this.object.position.distanceTo( this.target ); 117 | 118 | }; 119 | 120 | this.listenToKeyEvents = function ( domElement ) { 121 | 122 | domElement.addEventListener( 'keydown', onKeyDown ); 123 | this._domElementKeyEvents = domElement; 124 | 125 | }; 126 | 127 | this.saveState = function () { 128 | 129 | scope.target0.copy( scope.target ); 130 | scope.position0.copy( scope.object.position ); 131 | scope.zoom0 = scope.object.zoom; 132 | 133 | }; 134 | 135 | this.reset = function () { 136 | 137 | scope.target.copy( scope.target0 ); 138 | scope.object.position.copy( scope.position0 ); 139 | scope.object.zoom = scope.zoom0; 140 | 141 | scope.object.updateProjectionMatrix(); 142 | scope.dispatchEvent( _changeEvent ); 143 | 144 | scope.update(); 145 | 146 | state = STATE.NONE; 147 | 148 | }; 149 | 150 | // this method is exposed, but perhaps it would be better if we can make it private... 151 | this.update = function () { 152 | 153 | const offset = new Vector3(); 154 | 155 | // so camera.up is the orbit axis 156 | const quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); 157 | const quatInverse = quat.clone().invert(); 158 | 159 | const lastPosition = new Vector3(); 160 | const lastQuaternion = new Quaternion(); 161 | 162 | const twoPI = 2 * Math.PI; 163 | 164 | return function update() { 165 | 166 | const position = scope.object.position; 167 | 168 | offset.copy( position ).sub( scope.target ); 169 | 170 | // rotate offset to "y-axis-is-up" space 171 | offset.applyQuaternion( quat ); 172 | 173 | // angle from z-axis around y-axis 174 | spherical.setFromVector3( offset ); 175 | 176 | if ( scope.autoRotate && state === STATE.NONE ) { 177 | 178 | rotateLeft( getAutoRotationAngle() ); 179 | 180 | } 181 | 182 | if ( scope.enableDamping ) { 183 | 184 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 185 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 186 | 187 | } else { 188 | 189 | spherical.theta += sphericalDelta.theta; 190 | spherical.phi += sphericalDelta.phi; 191 | 192 | } 193 | 194 | // restrict theta to be between desired limits 195 | 196 | let min = scope.minAzimuthAngle; 197 | let max = scope.maxAzimuthAngle; 198 | 199 | if ( isFinite( min ) && isFinite( max ) ) { 200 | 201 | if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; 202 | 203 | if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; 204 | 205 | if ( min <= max ) { 206 | 207 | spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); 208 | 209 | } else { 210 | 211 | spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ? 212 | Math.max( min, spherical.theta ) : 213 | Math.min( max, spherical.theta ); 214 | 215 | } 216 | 217 | } 218 | 219 | // restrict phi to be between desired limits 220 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 221 | 222 | spherical.makeSafe(); 223 | 224 | 225 | spherical.radius *= scale; 226 | 227 | // restrict radius to be between desired limits 228 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); 229 | 230 | // move target to panned location 231 | 232 | if ( scope.enableDamping === true ) { 233 | 234 | scope.target.addScaledVector( panOffset, scope.dampingFactor ); 235 | 236 | } else { 237 | 238 | scope.target.add( panOffset ); 239 | 240 | } 241 | 242 | offset.setFromSpherical( spherical ); 243 | 244 | // rotate offset back to "camera-up-vector-is-up" space 245 | offset.applyQuaternion( quatInverse ); 246 | 247 | position.copy( scope.target ).add( offset ); 248 | 249 | scope.object.lookAt( scope.target ); 250 | 251 | if ( scope.enableDamping === true ) { 252 | 253 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 254 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 255 | 256 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 257 | 258 | } else { 259 | 260 | sphericalDelta.set( 0, 0, 0 ); 261 | 262 | panOffset.set( 0, 0, 0 ); 263 | 264 | } 265 | 266 | scale = 1; 267 | 268 | // update condition is: 269 | // min(camera displacement, camera rotation in radians)^2 > EPS 270 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 271 | 272 | if ( zoomChanged || 273 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 274 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 275 | 276 | scope.dispatchEvent( _changeEvent ); 277 | 278 | lastPosition.copy( scope.object.position ); 279 | lastQuaternion.copy( scope.object.quaternion ); 280 | zoomChanged = false; 281 | 282 | return true; 283 | 284 | } 285 | 286 | return false; 287 | 288 | }; 289 | 290 | }(); 291 | 292 | this.dispose = function () { 293 | 294 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); 295 | 296 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); 297 | scope.domElement.removeEventListener( 'pointercancel', onPointerCancel ); 298 | scope.domElement.removeEventListener( 'wheel', onMouseWheel ); 299 | 300 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 301 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 302 | 303 | 304 | if ( scope._domElementKeyEvents !== null ) { 305 | 306 | scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 307 | 308 | } 309 | 310 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 311 | 312 | }; 313 | 314 | // 315 | // internals 316 | // 317 | 318 | const scope = this; 319 | 320 | const STATE = { 321 | NONE: - 1, 322 | ROTATE: 0, 323 | DOLLY: 1, 324 | PAN: 2, 325 | TOUCH_ROTATE: 3, 326 | TOUCH_PAN: 4, 327 | TOUCH_DOLLY_PAN: 5, 328 | TOUCH_DOLLY_ROTATE: 6 329 | }; 330 | 331 | let state = STATE.NONE; 332 | 333 | const EPS = 0.000001; 334 | 335 | // current position in spherical coordinates 336 | const spherical = new Spherical(); 337 | const sphericalDelta = new Spherical(); 338 | 339 | let scale = 1; 340 | const panOffset = new Vector3(); 341 | let zoomChanged = false; 342 | 343 | const rotateStart = new Vector2(); 344 | const rotateEnd = new Vector2(); 345 | const rotateDelta = new Vector2(); 346 | 347 | const panStart = new Vector2(); 348 | const panEnd = new Vector2(); 349 | const panDelta = new Vector2(); 350 | 351 | const dollyStart = new Vector2(); 352 | const dollyEnd = new Vector2(); 353 | const dollyDelta = new Vector2(); 354 | 355 | const pointers = []; 356 | const pointerPositions = {}; 357 | 358 | function getAutoRotationAngle() { 359 | 360 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 361 | 362 | } 363 | 364 | function getZoomScale() { 365 | 366 | return Math.pow( 0.95, scope.zoomSpeed ); 367 | 368 | } 369 | 370 | function rotateLeft( angle ) { 371 | 372 | sphericalDelta.theta -= angle; 373 | 374 | } 375 | 376 | function rotateUp( angle ) { 377 | 378 | sphericalDelta.phi -= angle; 379 | 380 | } 381 | 382 | const panLeft = function () { 383 | 384 | const v = new Vector3(); 385 | 386 | return function panLeft( distance, objectMatrix ) { 387 | 388 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 389 | v.multiplyScalar( - distance ); 390 | 391 | panOffset.add( v ); 392 | 393 | }; 394 | 395 | }(); 396 | 397 | const panUp = function () { 398 | 399 | const v = new Vector3(); 400 | 401 | return function panUp( distance, objectMatrix ) { 402 | 403 | if ( scope.screenSpacePanning === true ) { 404 | 405 | v.setFromMatrixColumn( objectMatrix, 1 ); 406 | 407 | } else { 408 | 409 | v.setFromMatrixColumn( objectMatrix, 0 ); 410 | v.crossVectors( scope.object.up, v ); 411 | 412 | } 413 | 414 | v.multiplyScalar( distance ); 415 | 416 | panOffset.add( v ); 417 | 418 | }; 419 | 420 | }(); 421 | 422 | // deltaX and deltaY are in pixels; right and down are positive 423 | const pan = function () { 424 | 425 | const offset = new Vector3(); 426 | 427 | return function pan( deltaX, deltaY ) { 428 | 429 | const element = scope.domElement; 430 | 431 | if ( scope.object.isPerspectiveCamera ) { 432 | 433 | // perspective 434 | const position = scope.object.position; 435 | offset.copy( position ).sub( scope.target ); 436 | let targetDistance = offset.length(); 437 | 438 | // half of the fov is center to top of screen 439 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 440 | 441 | // we use only clientHeight here so aspect ratio does not distort speed 442 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 443 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 444 | 445 | } else if ( scope.object.isOrthographicCamera ) { 446 | 447 | // orthographic 448 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 449 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 450 | 451 | } else { 452 | 453 | // camera neither orthographic nor perspective 454 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 455 | scope.enablePan = false; 456 | 457 | } 458 | 459 | }; 460 | 461 | }(); 462 | 463 | function dollyOut( dollyScale ) { 464 | 465 | if ( scope.object.isPerspectiveCamera ) { 466 | 467 | scale /= dollyScale; 468 | 469 | } else if ( scope.object.isOrthographicCamera ) { 470 | 471 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 472 | scope.object.updateProjectionMatrix(); 473 | zoomChanged = true; 474 | 475 | } else { 476 | 477 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 478 | scope.enableZoom = false; 479 | 480 | } 481 | 482 | } 483 | 484 | function dollyIn( dollyScale ) { 485 | 486 | if ( scope.object.isPerspectiveCamera ) { 487 | 488 | scale *= dollyScale; 489 | 490 | } else if ( scope.object.isOrthographicCamera ) { 491 | 492 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 493 | scope.object.updateProjectionMatrix(); 494 | zoomChanged = true; 495 | 496 | } else { 497 | 498 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 499 | scope.enableZoom = false; 500 | 501 | } 502 | 503 | } 504 | 505 | // 506 | // event callbacks - update the object state 507 | // 508 | 509 | function handleMouseDownRotate( event ) { 510 | 511 | rotateStart.set( event.clientX, event.clientY ); 512 | 513 | } 514 | 515 | function handleMouseDownDolly( event ) { 516 | 517 | dollyStart.set( event.clientX, event.clientY ); 518 | 519 | } 520 | 521 | function handleMouseDownPan( event ) { 522 | 523 | panStart.set( event.clientX, event.clientY ); 524 | 525 | } 526 | 527 | function handleMouseMoveRotate( event ) { 528 | 529 | rotateEnd.set( event.clientX, event.clientY ); 530 | 531 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 532 | 533 | const element = scope.domElement; 534 | 535 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 536 | 537 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 538 | 539 | rotateStart.copy( rotateEnd ); 540 | 541 | scope.update(); 542 | 543 | } 544 | 545 | function handleMouseMoveDolly( event ) { 546 | 547 | dollyEnd.set( event.clientX, event.clientY ); 548 | 549 | dollyDelta.subVectors( dollyEnd, dollyStart ); 550 | 551 | if ( dollyDelta.y > 0 ) { 552 | 553 | dollyOut( getZoomScale() ); 554 | 555 | } else if ( dollyDelta.y < 0 ) { 556 | 557 | dollyIn( getZoomScale() ); 558 | 559 | } 560 | 561 | dollyStart.copy( dollyEnd ); 562 | 563 | scope.update(); 564 | 565 | } 566 | 567 | function handleMouseMovePan( event ) { 568 | 569 | panEnd.set( event.clientX, event.clientY ); 570 | 571 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 572 | 573 | pan( panDelta.x, panDelta.y ); 574 | 575 | panStart.copy( panEnd ); 576 | 577 | scope.update(); 578 | 579 | } 580 | 581 | function handleMouseWheel( event ) { 582 | 583 | if ( event.deltaY < 0 ) { 584 | 585 | dollyIn( getZoomScale() ); 586 | 587 | } else if ( event.deltaY > 0 ) { 588 | 589 | dollyOut( getZoomScale() ); 590 | 591 | } 592 | 593 | scope.update(); 594 | 595 | } 596 | 597 | function handleKeyDown( event ) { 598 | 599 | let needsUpdate = false; 600 | 601 | switch ( event.code ) { 602 | 603 | case scope.keys.UP: 604 | pan( 0, scope.keyPanSpeed ); 605 | needsUpdate = true; 606 | break; 607 | 608 | case scope.keys.BOTTOM: 609 | pan( 0, - scope.keyPanSpeed ); 610 | needsUpdate = true; 611 | break; 612 | 613 | case scope.keys.LEFT: 614 | pan( scope.keyPanSpeed, 0 ); 615 | needsUpdate = true; 616 | break; 617 | 618 | case scope.keys.RIGHT: 619 | pan( - scope.keyPanSpeed, 0 ); 620 | needsUpdate = true; 621 | break; 622 | 623 | } 624 | 625 | if ( needsUpdate ) { 626 | 627 | // prevent the browser from scrolling on cursor keys 628 | event.preventDefault(); 629 | 630 | scope.update(); 631 | 632 | } 633 | 634 | 635 | } 636 | 637 | function handleTouchStartRotate() { 638 | 639 | if ( pointers.length === 1 ) { 640 | 641 | rotateStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); 642 | 643 | } else { 644 | 645 | const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); 646 | const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); 647 | 648 | rotateStart.set( x, y ); 649 | 650 | } 651 | 652 | } 653 | 654 | function handleTouchStartPan() { 655 | 656 | if ( pointers.length === 1 ) { 657 | 658 | panStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); 659 | 660 | } else { 661 | 662 | const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); 663 | const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); 664 | 665 | panStart.set( x, y ); 666 | 667 | } 668 | 669 | } 670 | 671 | function handleTouchStartDolly() { 672 | 673 | const dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX; 674 | const dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY; 675 | 676 | const distance = Math.sqrt( dx * dx + dy * dy ); 677 | 678 | dollyStart.set( 0, distance ); 679 | 680 | } 681 | 682 | function handleTouchStartDollyPan() { 683 | 684 | if ( scope.enableZoom ) handleTouchStartDolly(); 685 | 686 | if ( scope.enablePan ) handleTouchStartPan(); 687 | 688 | } 689 | 690 | function handleTouchStartDollyRotate() { 691 | 692 | if ( scope.enableZoom ) handleTouchStartDolly(); 693 | 694 | if ( scope.enableRotate ) handleTouchStartRotate(); 695 | 696 | } 697 | 698 | function handleTouchMoveRotate( event ) { 699 | 700 | if ( pointers.length == 1 ) { 701 | 702 | rotateEnd.set( event.pageX, event.pageY ); 703 | 704 | } else { 705 | 706 | const position = getSecondPointerPosition( event ); 707 | 708 | const x = 0.5 * ( event.pageX + position.x ); 709 | const y = 0.5 * ( event.pageY + position.y ); 710 | 711 | rotateEnd.set( x, y ); 712 | 713 | } 714 | 715 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 716 | 717 | const element = scope.domElement; 718 | 719 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 720 | 721 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 722 | 723 | rotateStart.copy( rotateEnd ); 724 | 725 | } 726 | 727 | function handleTouchMovePan( event ) { 728 | 729 | if ( pointers.length === 1 ) { 730 | 731 | panEnd.set( event.pageX, event.pageY ); 732 | 733 | } else { 734 | 735 | const position = getSecondPointerPosition( event ); 736 | 737 | const x = 0.5 * ( event.pageX + position.x ); 738 | const y = 0.5 * ( event.pageY + position.y ); 739 | 740 | panEnd.set( x, y ); 741 | 742 | } 743 | 744 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 745 | 746 | pan( panDelta.x, panDelta.y ); 747 | 748 | panStart.copy( panEnd ); 749 | 750 | } 751 | 752 | function handleTouchMoveDolly( event ) { 753 | 754 | const position = getSecondPointerPosition( event ); 755 | 756 | const dx = event.pageX - position.x; 757 | const dy = event.pageY - position.y; 758 | 759 | const distance = Math.sqrt( dx * dx + dy * dy ); 760 | 761 | dollyEnd.set( 0, distance ); 762 | 763 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 764 | 765 | dollyOut( dollyDelta.y ); 766 | 767 | dollyStart.copy( dollyEnd ); 768 | 769 | } 770 | 771 | function handleTouchMoveDollyPan( event ) { 772 | 773 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 774 | 775 | if ( scope.enablePan ) handleTouchMovePan( event ); 776 | 777 | } 778 | 779 | function handleTouchMoveDollyRotate( event ) { 780 | 781 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 782 | 783 | if ( scope.enableRotate ) handleTouchMoveRotate( event ); 784 | 785 | } 786 | 787 | // 788 | // event handlers - FSM: listen for events and reset state 789 | // 790 | 791 | function onPointerDown( event ) { 792 | 793 | if ( scope.enabled === false ) return; 794 | 795 | if ( pointers.length === 0 ) { 796 | 797 | scope.domElement.setPointerCapture( event.pointerId ); 798 | 799 | scope.domElement.addEventListener( 'pointermove', onPointerMove ); 800 | scope.domElement.addEventListener( 'pointerup', onPointerUp ); 801 | 802 | } 803 | 804 | // 805 | 806 | addPointer( event ); 807 | 808 | if ( event.pointerType === 'touch' ) { 809 | 810 | onTouchStart( event ); 811 | 812 | } else { 813 | 814 | onMouseDown( event ); 815 | 816 | } 817 | 818 | } 819 | 820 | function onPointerMove( event ) { 821 | 822 | if ( scope.enabled === false ) return; 823 | 824 | if ( event.pointerType === 'touch' ) { 825 | 826 | onTouchMove( event ); 827 | 828 | } else { 829 | 830 | onMouseMove( event ); 831 | 832 | } 833 | 834 | } 835 | 836 | function onPointerUp( event ) { 837 | 838 | removePointer( event ); 839 | 840 | if ( pointers.length === 0 ) { 841 | 842 | scope.domElement.releasePointerCapture( event.pointerId ); 843 | 844 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 845 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 846 | 847 | } 848 | 849 | scope.dispatchEvent( _endEvent ); 850 | 851 | state = STATE.NONE; 852 | 853 | } 854 | 855 | function onPointerCancel( event ) { 856 | 857 | removePointer( event ); 858 | 859 | } 860 | 861 | function onMouseDown( event ) { 862 | 863 | let mouseAction; 864 | 865 | switch ( event.button ) { 866 | 867 | case 0: 868 | 869 | mouseAction = scope.mouseButtons.LEFT; 870 | break; 871 | 872 | case 1: 873 | 874 | mouseAction = scope.mouseButtons.MIDDLE; 875 | break; 876 | 877 | case 2: 878 | 879 | mouseAction = scope.mouseButtons.RIGHT; 880 | break; 881 | 882 | default: 883 | 884 | mouseAction = - 1; 885 | 886 | } 887 | 888 | switch ( mouseAction ) { 889 | 890 | case MOUSE.DOLLY: 891 | 892 | if ( scope.enableZoom === false ) return; 893 | 894 | handleMouseDownDolly( event ); 895 | 896 | state = STATE.DOLLY; 897 | 898 | break; 899 | 900 | case MOUSE.ROTATE: 901 | 902 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 903 | 904 | if ( scope.enablePan === false ) return; 905 | 906 | handleMouseDownPan( event ); 907 | 908 | state = STATE.PAN; 909 | 910 | } else { 911 | 912 | if ( scope.enableRotate === false ) return; 913 | 914 | handleMouseDownRotate( event ); 915 | 916 | state = STATE.ROTATE; 917 | 918 | } 919 | 920 | break; 921 | 922 | case MOUSE.PAN: 923 | 924 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 925 | 926 | if ( scope.enableRotate === false ) return; 927 | 928 | handleMouseDownRotate( event ); 929 | 930 | state = STATE.ROTATE; 931 | 932 | } else { 933 | 934 | if ( scope.enablePan === false ) return; 935 | 936 | handleMouseDownPan( event ); 937 | 938 | state = STATE.PAN; 939 | 940 | } 941 | 942 | break; 943 | 944 | default: 945 | 946 | state = STATE.NONE; 947 | 948 | } 949 | 950 | if ( state !== STATE.NONE ) { 951 | 952 | scope.dispatchEvent( _startEvent ); 953 | 954 | } 955 | 956 | } 957 | 958 | function onMouseMove( event ) { 959 | 960 | switch ( state ) { 961 | 962 | case STATE.ROTATE: 963 | 964 | if ( scope.enableRotate === false ) return; 965 | 966 | handleMouseMoveRotate( event ); 967 | 968 | break; 969 | 970 | case STATE.DOLLY: 971 | 972 | if ( scope.enableZoom === false ) return; 973 | 974 | handleMouseMoveDolly( event ); 975 | 976 | break; 977 | 978 | case STATE.PAN: 979 | 980 | if ( scope.enablePan === false ) return; 981 | 982 | handleMouseMovePan( event ); 983 | 984 | break; 985 | 986 | } 987 | 988 | } 989 | 990 | function onMouseWheel( event ) { 991 | 992 | if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return; 993 | 994 | event.preventDefault(); 995 | 996 | scope.dispatchEvent( _startEvent ); 997 | 998 | handleMouseWheel( event ); 999 | 1000 | scope.dispatchEvent( _endEvent ); 1001 | 1002 | } 1003 | 1004 | function onKeyDown( event ) { 1005 | 1006 | if ( scope.enabled === false || scope.enablePan === false ) return; 1007 | 1008 | handleKeyDown( event ); 1009 | 1010 | } 1011 | 1012 | function onTouchStart( event ) { 1013 | 1014 | trackPointer( event ); 1015 | 1016 | switch ( pointers.length ) { 1017 | 1018 | case 1: 1019 | 1020 | switch ( scope.touches.ONE ) { 1021 | 1022 | case TOUCH.ROTATE: 1023 | 1024 | if ( scope.enableRotate === false ) return; 1025 | 1026 | handleTouchStartRotate(); 1027 | 1028 | state = STATE.TOUCH_ROTATE; 1029 | 1030 | break; 1031 | 1032 | case TOUCH.PAN: 1033 | 1034 | if ( scope.enablePan === false ) return; 1035 | 1036 | handleTouchStartPan(); 1037 | 1038 | state = STATE.TOUCH_PAN; 1039 | 1040 | break; 1041 | 1042 | default: 1043 | 1044 | state = STATE.NONE; 1045 | 1046 | } 1047 | 1048 | break; 1049 | 1050 | case 2: 1051 | 1052 | switch ( scope.touches.TWO ) { 1053 | 1054 | case TOUCH.DOLLY_PAN: 1055 | 1056 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1057 | 1058 | handleTouchStartDollyPan(); 1059 | 1060 | state = STATE.TOUCH_DOLLY_PAN; 1061 | 1062 | break; 1063 | 1064 | case TOUCH.DOLLY_ROTATE: 1065 | 1066 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1067 | 1068 | handleTouchStartDollyRotate(); 1069 | 1070 | state = STATE.TOUCH_DOLLY_ROTATE; 1071 | 1072 | break; 1073 | 1074 | default: 1075 | 1076 | state = STATE.NONE; 1077 | 1078 | } 1079 | 1080 | break; 1081 | 1082 | default: 1083 | 1084 | state = STATE.NONE; 1085 | 1086 | } 1087 | 1088 | if ( state !== STATE.NONE ) { 1089 | 1090 | scope.dispatchEvent( _startEvent ); 1091 | 1092 | } 1093 | 1094 | } 1095 | 1096 | function onTouchMove( event ) { 1097 | 1098 | trackPointer( event ); 1099 | 1100 | switch ( state ) { 1101 | 1102 | case STATE.TOUCH_ROTATE: 1103 | 1104 | if ( scope.enableRotate === false ) return; 1105 | 1106 | handleTouchMoveRotate( event ); 1107 | 1108 | scope.update(); 1109 | 1110 | break; 1111 | 1112 | case STATE.TOUCH_PAN: 1113 | 1114 | if ( scope.enablePan === false ) return; 1115 | 1116 | handleTouchMovePan( event ); 1117 | 1118 | scope.update(); 1119 | 1120 | break; 1121 | 1122 | case STATE.TOUCH_DOLLY_PAN: 1123 | 1124 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1125 | 1126 | handleTouchMoveDollyPan( event ); 1127 | 1128 | scope.update(); 1129 | 1130 | break; 1131 | 1132 | case STATE.TOUCH_DOLLY_ROTATE: 1133 | 1134 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1135 | 1136 | handleTouchMoveDollyRotate( event ); 1137 | 1138 | scope.update(); 1139 | 1140 | break; 1141 | 1142 | default: 1143 | 1144 | state = STATE.NONE; 1145 | 1146 | } 1147 | 1148 | } 1149 | 1150 | function onContextMenu( event ) { 1151 | 1152 | if ( scope.enabled === false ) return; 1153 | 1154 | event.preventDefault(); 1155 | 1156 | } 1157 | 1158 | function addPointer( event ) { 1159 | 1160 | pointers.push( event ); 1161 | 1162 | } 1163 | 1164 | function removePointer( event ) { 1165 | 1166 | delete pointerPositions[ event.pointerId ]; 1167 | 1168 | for ( let i = 0; i < pointers.length; i ++ ) { 1169 | 1170 | if ( pointers[ i ].pointerId == event.pointerId ) { 1171 | 1172 | pointers.splice( i, 1 ); 1173 | return; 1174 | 1175 | } 1176 | 1177 | } 1178 | 1179 | } 1180 | 1181 | function trackPointer( event ) { 1182 | 1183 | let position = pointerPositions[ event.pointerId ]; 1184 | 1185 | if ( position === undefined ) { 1186 | 1187 | position = new Vector2(); 1188 | pointerPositions[ event.pointerId ] = position; 1189 | 1190 | } 1191 | 1192 | position.set( event.pageX, event.pageY ); 1193 | 1194 | } 1195 | 1196 | function getSecondPointerPosition( event ) { 1197 | 1198 | const pointer = ( event.pointerId === pointers[ 0 ].pointerId ) ? pointers[ 1 ] : pointers[ 0 ]; 1199 | 1200 | return pointerPositions[ pointer.pointerId ]; 1201 | 1202 | } 1203 | 1204 | // 1205 | 1206 | scope.domElement.addEventListener( 'contextmenu', onContextMenu ); 1207 | 1208 | scope.domElement.addEventListener( 'pointerdown', onPointerDown ); 1209 | scope.domElement.addEventListener( 'pointercancel', onPointerCancel ); 1210 | scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } ); 1211 | 1212 | // force an update at start 1213 | 1214 | this.update(); 1215 | 1216 | } 1217 | 1218 | } 1219 | 1220 | 1221 | // This set of controls performs orbiting, dollying (zooming), and panning. 1222 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 1223 | // This is very similar to OrbitControls, another set of touch behavior 1224 | // 1225 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate 1226 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 1227 | // Pan - left mouse, or arrow keys / touch: one-finger move 1228 | 1229 | class MapControls extends OrbitControls { 1230 | 1231 | constructor( object, domElement ) { 1232 | 1233 | super( object, domElement ); 1234 | 1235 | this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up 1236 | 1237 | this.mouseButtons.LEFT = MOUSE.PAN; 1238 | this.mouseButtons.RIGHT = MOUSE.ROTATE; 1239 | 1240 | this.touches.ONE = TOUCH.PAN; 1241 | this.touches.TWO = TOUCH.DOLLY_ROTATE; 1242 | 1243 | } 1244 | 1245 | } 1246 | 1247 | export { OrbitControls, MapControls }; 1248 | --------------------------------------------------------------------------------