├── .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 |
40 |
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 |
--------------------------------------------------------------------------------