Lasso tool | Selection tool on gltf model

Hi there, I am using three-mesh-bvh library and this selection tool example.

My purpose is to use a gltf model instead of the torusknot and apply the selection method onit is it possible? if so please describe me a bit in detail cause I am new, an example using JSFiddle will be good.

This is my gltf model

This is my code on which I am trying to implement the selection tool on:

import * as THREE from 'three';
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import {
	MeshBVHHelper,
	MeshBVH,
	CONTAINED,
	INTERSECTED,
	NOT_INTERSECTED,
} from 'three-mesh-bvh';

const params = {

	toolMode: 'lasso',
	selectionMode: 'intersection',
	liveUpdate: false,
	selectModel: false,
	wireframe: false,
	useBoundsTree: true,
	displayHelper: false,
	helperDepth: 10,
	rotate: true,

};

let renderer, camera, scene, gui, controls, selectionShape, mesh, helper;
let highlightMesh, highlightWireframeMesh, outputContainer, group;
const selectionPoints = [];
let dragging = false;
let selectionShapeNeedsUpdate = false;
let selectionNeedsUpdate = false;

init();
render();

function init() {

	outputContainer = document.getElementById( 'output' );

	const bgColor = new THREE.Color( 0x263238 );

	// renderer setup
	renderer = new THREE.WebGLRenderer( { antialias: true } );
	renderer.setPixelRatio( window.devicePixelRatio );
	renderer.setSize( window.innerWidth, window.innerHeight );
	renderer.setClearColor( bgColor, 1 );
	renderer.shadowMap.enabled = true;
	document.body.appendChild( renderer.domElement );

	// scene setup
	scene = new THREE.Scene();

	const light = new THREE.DirectionalLight( 0xffffff, 1 );
	light.castShadow = true;
	light.shadow.mapSize.set( 2048, 2048 );
	light.position.set( 10, 10, 10 );
	scene.add( light );
	scene.add( new THREE.AmbientLight( 0xb0bec5, 0.8 ) );

	// camera setup
	camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 50 );
	camera.position.set( 2, 4, 6 );
	camera.far = 100;
	camera.updateProjectionMatrix();
	scene.add( camera );

	// selection shape
	selectionShape = new THREE.Line();
	selectionShape.material.color.set( 0xff9800 ).convertSRGBToLinear();
	selectionShape.renderOrder = 1;
	selectionShape.position.z = - .2;
	selectionShape.depthTest = false;
	selectionShape.scale.setScalar( 1 );
	camera.add( selectionShape );

	// group for rotation
	group = new THREE.Group();
	scene.add( group );

	// base mesh
	const loader = new GLTFLoader();

	loader.load(
	'den.gltf',
	function (gltf) {
		const mesh = new THREE.Mesh(gltf.scene.children[0].geometry, gltf.scene.children[0].material);

		mesh.name = 'mesh';

		group.add(mesh)
		mesh.geometry.boundsTree = new MeshBVH( mesh.geometry );
		mesh.geometry.setAttribute( 'color', new THREE.Uint8BufferAttribute(
			new Array( mesh.geometry.index.count * 1 ).fill( 255 ), 3, true
		) );
		mesh.castShadow = true;
		mesh.receiveShadow = true;
		mesh.scale(0.5, 0.1, 0.5)
		group.add( mesh );
	
		helper = new MeshBVHHelper( mesh, 10 );
		group.add( helper );

	},
	function (xhr) {
		console.log((xhr.loaded / xhr.total) * 100 + '% loaded');
	},
	function (error) {
		console.log('An error happened');
	}
	);

	mesh = new THREE.Mesh(
		new THREE.TorusKnotGeometry( 1.5, 0.5, 500, 60 ).toNonIndexed(),
		new THREE.MeshStandardMaterial( {
			polygonOffset: true,
			polygonOffsetFactor: 1,
		} )
	);
	mesh.geometry.boundsTree = new MeshBVH( mesh.geometry );
	mesh.geometry.setAttribute( 'color', new THREE.Uint8BufferAttribute(
		new Array( mesh.geometry.index.count * 3 ).fill( 255 ), 3, true
	) );
	mesh.castShadow = true;
	mesh.receiveShadow = true;
	group.add( mesh );

	helper = new MeshBVHHelper( mesh, 10 );
	group.add( helper );

	// meshes for selection highlights
	highlightMesh = new THREE.Mesh();
	highlightMesh.geometry = mesh.geometry.clone();
	highlightMesh.geometry.drawRange.count = 0;
	highlightMesh.material = new THREE.MeshBasicMaterial( {
		opacity: 0.05,
		transparent: true,
		depthWrite: false,
	} );
	highlightMesh.material.color.set( 0xff9800 ).convertSRGBToLinear();
	highlightMesh.renderOrder = 1;
	group.add( highlightMesh );

	highlightWireframeMesh = new THREE.Mesh();
	highlightWireframeMesh.geometry = highlightMesh.geometry;
	highlightWireframeMesh.material = new THREE.MeshBasicMaterial( {
		opacity: 0.25,
		transparent: true,
		wireframe: true,
		depthWrite: false,
	} );
	highlightWireframeMesh.material.color.copy( highlightMesh.material.color );
	highlightWireframeMesh.renderOrder = 2;
	group.add( highlightWireframeMesh );

	// add floor
	const gridHelper = new THREE.GridHelper( 10, 10, 0xffffff, 0xffffff );
	gridHelper.material.opacity = 0.2;
	gridHelper.material.transparent = true;
	gridHelper.position.y = - 2.75;
	scene.add( gridHelper );

	const shadowPlane = new THREE.Mesh(
		new THREE.PlaneGeometry(),
		new THREE.ShadowMaterial( { color: 0, opacity: 0.2, depthWrite: false } )
	);
	shadowPlane.position.y = - 2.74;
	shadowPlane.rotation.x = - Math.PI / 2;
	shadowPlane.scale.setScalar( 20 );
	shadowPlane.renderOrder = 2;
	shadowPlane.receiveShadow = true;
	scene.add( shadowPlane );

	// controls
	controls = new OrbitControls( camera, renderer.domElement );
	controls.minDistance = 3;
	controls.touches.ONE = THREE.TOUCH.PAN;
	controls.mouseButtons.LEFT = THREE.MOUSE.PAN;
	controls.touches.TWO = THREE.TOUCH.ROTATE;
	controls.mouseButtons.RIGHT = THREE.MOUSE.ROTATE;
	controls.enablePan = false;

	// gui
	gui = new GUI();
	const selectionFolder = gui.addFolder( 'selection' );
	selectionFolder.add( params, 'toolMode', [ 'lasso', 'box' ] );
	selectionFolder.add( params, 'selectionMode', [ 'centroid', 'centroid-visible', 'intersection' ] );
	selectionFolder.add( params, 'selectModel' );
	selectionFolder.add( params, 'liveUpdate' );
	selectionFolder.add( params, 'useBoundsTree' );
	selectionFolder.open();

	const displayFolder = gui.addFolder( 'display' );
	displayFolder.add( params, 'wireframe' );
	displayFolder.add( params, 'rotate' );
	displayFolder.add( params, 'displayHelper' );
	displayFolder.add( params, 'helperDepth', 1, 30, 1 ).onChange( v => {

		helper.depth = v;
		helper.update();

	} );
	displayFolder.open();
	gui.open();

	// handle building lasso shape
	let startX = - Infinity;
	let startY = - Infinity;

	let prevX = - Infinity;
	let prevY = - Infinity;

	const tempVec0 = new THREE.Vector2();
	const tempVec1 = new THREE.Vector2();
	const tempVec2 = new THREE.Vector2();
	renderer.domElement.addEventListener( 'pointerdown', e => {

		prevX = e.clientX;
		prevY = e.clientY;
		startX = ( e.clientX / window.innerWidth ) * 2 - 1;
		startY = - ( ( e.clientY / window.innerHeight ) * 2 - 1 );
		selectionPoints.length = 0;
		dragging = true;

	} );

	renderer.domElement.addEventListener( 'pointerup', () => {

		selectionShape.visible = false;
		dragging = false;
		if ( selectionPoints.length ) {

			selectionNeedsUpdate = true;

		}

	} );

	renderer.domElement.addEventListener( 'pointermove', e => {

		// If the left mouse button is not pressed
		if ( ( 1 & e.buttons ) === 0 ) {

			return;

		}

		const ex = e.clientX;
		const ey = e.clientY;

		const nx = ( e.clientX / window.innerWidth ) * 2 - 1;
		const ny = - ( ( e.clientY / window.innerHeight ) * 2 - 1 );

		if ( params.toolMode === 'box' ) {

			// set points for the corner of the box
			selectionPoints.length = 3 * 5;

			selectionPoints[ 0 ] = startX;
			selectionPoints[ 1 ] = startY;
			selectionPoints[ 2 ] = 0;

			selectionPoints[ 3 ] = nx;
			selectionPoints[ 4 ] = startY;
			selectionPoints[ 5 ] = 0;

			selectionPoints[ 6 ] = nx;
			selectionPoints[ 7 ] = ny;
			selectionPoints[ 8 ] = 0;

			selectionPoints[ 9 ] = startX;
			selectionPoints[ 10 ] = ny;
			selectionPoints[ 11 ] = 0;

			selectionPoints[ 12 ] = startX;
			selectionPoints[ 13 ] = startY;
			selectionPoints[ 14 ] = 0;

			if ( ex !== prevX || ey !== prevY ) {

				selectionShapeNeedsUpdate = true;

			}

			prevX = ex;
			prevY = ey;
			selectionShape.visible = true;
			if ( params.liveUpdate ) {

				selectionNeedsUpdate = true;

			}

		} else {

			// If the mouse hasn't moved a lot since the last point
			if (
				Math.abs( ex - prevX ) >= 3 ||
				Math.abs( ey - prevY ) >= 3
			) {

				// Check if the mouse moved in roughly the same direction as the previous point
				// and replace it if so.
				const i = ( selectionPoints.length / 3 ) - 1;
				const i3 = i * 3;
				let doReplace = false;
				if ( selectionPoints.length > 3 ) {

					// prev segment direction
					tempVec0.set( selectionPoints[ i3 - 3 ], selectionPoints[ i3 - 3 + 1 ] );
					tempVec1.set( selectionPoints[ i3 ], selectionPoints[ i3 + 1 ] );
					tempVec1.sub( tempVec0 ).normalize();

					// this segment direction
					tempVec0.set( selectionPoints[ i3 ], selectionPoints[ i3 + 1 ] );
					tempVec2.set( nx, ny );
					tempVec2.sub( tempVec0 ).normalize();

					const dot = tempVec1.dot( tempVec2 );
					doReplace = dot > 0.99;

				}

				if ( doReplace ) {

					selectionPoints[ i3 ] = nx;
					selectionPoints[ i3 + 1 ] = ny;

				} else {

					selectionPoints.push( nx, ny, 0 );

				}

				selectionShapeNeedsUpdate = true;
				selectionShape.visible = true;

				prevX = ex;
				prevY = ey;

				if ( params.liveUpdate ) {

					selectionNeedsUpdate = true;

				}

			}

		}

	} );

	window.addEventListener( 'resize', function () {

		camera.aspect = window.innerWidth / window.innerHeight;
		camera.updateProjectionMatrix();

		renderer.setSize( window.innerWidth, window.innerHeight );

	}, false );

}

function render() {

	requestAnimationFrame( render );

	mesh.material.wireframe = params.wireframe;
	helper.visible = params.displayHelper;

	// Update the selection lasso lines
	if ( selectionShapeNeedsUpdate ) {

		if ( params.toolMode === 'lasso' ) {

			const ogLength = selectionPoints.length;
			selectionPoints.push(
				selectionPoints[ 0 ],
				selectionPoints[ 1 ],
				selectionPoints[ 2 ]
			);

			selectionShape.geometry.setAttribute(
				'position',
				new THREE.Float32BufferAttribute( selectionPoints, 3, false )
			);

			selectionPoints.length = ogLength;

		} else {

			selectionShape.geometry.setAttribute(
				'position',
				new THREE.Float32BufferAttribute( selectionPoints, 3, false )
			);

		}

		selectionShape.frustumCulled = false;
		selectionShapeNeedsUpdate = false;

	}

	if ( selectionNeedsUpdate ) {

		selectionNeedsUpdate = false;

		if ( selectionPoints.length > 0 ) {

			updateSelection();

		}

	}

	const yScale = Math.tan( THREE.MathUtils.DEG2RAD * camera.fov / 2 ) * selectionShape.position.z;
	selectionShape.scale.set( - yScale * camera.aspect, - yScale, 1 );

	renderer.render( scene, camera );

	if ( params.rotate ) {

		group.rotation.y += 0.01;
		if ( params.liveUpdate && dragging ) {

			selectionNeedsUpdate = true;

		}

	}

}

const invWorldMatrix = new THREE.Matrix4();
const camLocalPosition = new THREE.Vector3();
const tempRay = new THREE.Ray();
const centroid = new THREE.Vector3();
const screenCentroid = new THREE.Vector3();
const faceNormal = new THREE.Vector3();
const toScreenSpaceMatrix = new THREE.Matrix4();
const boxPoints = new Array( 8 ).fill().map( () => new THREE.Vector3() );
const boxLines = new Array( 12 ).fill().map( () => new THREE.Line3() );
const lassoSegments = [];
const perBoundsSegments = [];
function updateSelection() {

	// TODO: Possible improvements
	// - Correctly handle the camera near clip
	// - Improve line line intersect performance?

	toScreenSpaceMatrix
		.copy( mesh.matrixWorld )
		.premultiply( camera.matrixWorldInverse )
		.premultiply( camera.projectionMatrix );

	// create scratch points and lines to use for selection
	while ( lassoSegments.length < selectionPoints.length ) {

		lassoSegments.push( new THREE.Line3() );

	}

	lassoSegments.length = selectionPoints.length;

	for ( let s = 0, l = selectionPoints.length; s < l; s += 3 ) {

		const line = lassoSegments[ s ];
		const sNext = ( s + 3 ) % l;
		line.start.x = selectionPoints[ s ];
		line.start.y = selectionPoints[ s + 1 ];

		line.end.x = selectionPoints[ sNext ];
		line.end.y = selectionPoints[ sNext + 1 ];

	}

	invWorldMatrix.copy( mesh.matrixWorld ).invert();
	camLocalPosition.set( 0, 0, 0 ).applyMatrix4( camera.matrixWorld ).applyMatrix4( invWorldMatrix );

	const startTime = window.performance.now();
	const indices = [];
	mesh.geometry.boundsTree.shapecast( {
		intersectsBounds: ( box, isLeaf, score, depth ) => {

			// check if bounds intersect or contain the lasso region
			if ( ! params.useBoundsTree ) {

				return INTERSECTED;

			}

			// Get the bounding box points
			const { min, max } = box;
			let index = 0;

			let minY = Infinity;
			let maxY = - Infinity;
			let minX = Infinity;
			for ( let x = 0; x <= 1; x ++ ) {

				for ( let y = 0; y <= 1; y ++ ) {

					for ( let z = 0; z <= 1; z ++ ) {

						const v = boxPoints[ index ];
						v.x = x === 0 ? min.x : max.x;
						v.y = y === 0 ? min.y : max.y;
						v.z = z === 0 ? min.z : max.z;
						v.w = 1;
						v.applyMatrix4( toScreenSpaceMatrix );
						index ++;

						if ( v.y < minY ) minY = v.y;
						if ( v.y > maxY ) maxY = v.y;
						if ( v.x < minX ) minX = v.x;

					}

				}

			}

			// Find all the relevant segments here and cache them in the above array for
			// subsequent child checks to use.
			const parentSegments = perBoundsSegments[ depth - 1 ] || lassoSegments;
			const segmentsToCheck = perBoundsSegments[ depth ] || [];
			segmentsToCheck.length = 0;
			perBoundsSegments[ depth ] = segmentsToCheck;
			for ( let i = 0, l = parentSegments.length; i < l; i ++ ) {

				const line = parentSegments[ i ];
				const sx = line.start.x;
				const sy = line.start.y;
				const ex = line.end.x;
				const ey = line.end.y;
				if ( sx < minX && ex < minX ) continue;

				const startAbove = sy > maxY;
				const endAbove = ey > maxY;
				if ( startAbove && endAbove ) continue;

				const startBelow = sy < minY;
				const endBelow = ey < minY;
				if ( startBelow && endBelow ) continue;

				segmentsToCheck.push( line );

			}

			if ( segmentsToCheck.length === 0 ) {

				return NOT_INTERSECTED;

			}

			// Get the screen space hull lines
			const hull = getConvexHull( boxPoints );
			const lines = hull.map( ( p, i ) => {

				const nextP = hull[ ( i + 1 ) % hull.length ];
				const line = boxLines[ i ];
				line.start.copy( p );
				line.end.copy( nextP );
				return line;

			} );

			// If a lasso point is inside the hull then it's intersected and cannot be contained
			if ( pointRayCrossesSegments( segmentsToCheck[ 0 ].start, lines ) % 2 === 1 ) {

				return INTERSECTED;

			}

			// check if the screen space hull is in the lasso
			let crossings = 0;
			for ( let i = 0, l = hull.length; i < l; i ++ ) {

				const v = hull[ i ];
				const pCrossings = pointRayCrossesSegments( v, segmentsToCheck );

				if ( i === 0 ) {

					crossings = pCrossings;

				}

				// if two points on the hull have different amounts of crossings then
				// it can only be intersected
				if ( crossings !== pCrossings ) {

					return INTERSECTED;

				}

			}

			// check if there are any intersections
			for ( let i = 0, l = lines.length; i < l; i ++ ) {

				const boxLine = lines[ i ];
				for ( let s = 0, ls = segmentsToCheck.length; s < ls; s ++ ) {

					if ( lineCrossesLine( boxLine, segmentsToCheck[ s ] ) ) {

						return INTERSECTED;

					}

				}

			}

			return crossings % 2 === 0 ? NOT_INTERSECTED : CONTAINED;

		},

		intersectsTriangle: ( tri, index, contained, depth ) => {

			const i3 = index * 3;
			const a = i3 + 0;
			const b = i3 + 1;
			const c = i3 + 2;

			// check all the segments if using no bounds tree
			const segmentsToCheck = params.useBoundsTree ? perBoundsSegments[ depth ] : lassoSegments;
			if ( params.selectionMode === 'centroid' || params.selectionMode === 'centroid-visible' ) {

				// get the center of the triangle
				centroid.copy( tri.a ).add( tri.b ).add( tri.c ).multiplyScalar( 1 / 3 );
				screenCentroid.copy( centroid ).applyMatrix4( toScreenSpaceMatrix );

				// counting the crossings
				if (
					contained ||
					pointRayCrossesSegments( screenCentroid, segmentsToCheck ) % 2 === 1
				) {

					// if we're only selecting visible faces then perform a ray check to ensure the centroid
					// is visible.
					if ( params.selectionMode === 'centroid-visible' ) {

						tri.getNormal( faceNormal );
						tempRay.origin.copy( centroid ).addScaledVector( faceNormal, 1e-6 );
						tempRay.direction.subVectors( camLocalPosition, centroid );

						const res = mesh.geometry.boundsTree.raycastFirst( tempRay, THREE.DoubleSide );
						if ( res ) {

							return false;

						}

					}

					indices.push( a, b, c );
					return params.selectModel;

				}

			} else if ( params.selectionMode === 'intersection' ) {

				// if the parent bounds were marked as contained then we contain all the triangles within
				if ( contained ) {

					indices.push( a, b, c );
					return params.selectModel;

				}

				// get the projected vertices
				const vertices = [
					tri.a,
					tri.b,
					tri.c,
				];

				// check if any of the vertices are inside the selection and if so then the triangle is selected
				for ( let j = 0; j < 3; j ++ ) {

					const v = vertices[ j ];
					v.applyMatrix4( toScreenSpaceMatrix );

					const crossings = pointRayCrossesSegments( v, segmentsToCheck );
					if ( crossings % 2 === 1 ) {

						indices.push( a, b, c );
						return params.selectModel;

					}

				}

				// get the lines for the triangle
				const lines = [
					boxLines[ 0 ],
					boxLines[ 1 ],
					boxLines[ 2 ],
				];

				lines[ 0 ].start.copy( tri.a );
				lines[ 0 ].end.copy( tri.b );

				lines[ 1 ].start.copy( tri.b );
				lines[ 1 ].end.copy( tri.c );

				lines[ 2 ].start.copy( tri.c );
				lines[ 2 ].end.copy( tri.a );

				// check for the case where a selection intersects a triangle but does not contain any
				// of the vertices
				for ( let i = 0; i < 3; i ++ ) {

					const l = lines[ i ];
					for ( let s = 0, sl = segmentsToCheck.length; s < sl; s ++ ) {

						if ( lineCrossesLine( l, segmentsToCheck[ s ] ) ) {

							indices.push( a, b, c );
							return params.selectModel;

						}

					}

				}

			}

			return false;

		}

	} );

	const traverseTime = window.performance.now() - startTime;
	outputContainer.innerText = `${ traverseTime.toFixed( 3 ) }ms`;

	const indexAttr = mesh.geometry.index;
	const newIndexAttr = highlightMesh.geometry.index;
	if ( indices.length && params.selectModel ) {

		// if we found indices and we want to select the whole model
		for ( let i = 0, l = indexAttr.count; i < l; i ++ ) {

			const i2 = indexAttr.getX( i );
			newIndexAttr.setX( i, i2 );

		}

		highlightMesh.geometry.drawRange.count = Infinity;
		newIndexAttr.needsUpdate = true;

	} else {

		// update the highlight mesh
		for ( let i = 0, l = indices.length; i < l; i ++ ) {

			const i2 = indexAttr.getX( indices[ i ] );
			newIndexAttr.setX( i, i2 );

		}

		highlightMesh.geometry.drawRange.count = indices.length;
		newIndexAttr.needsUpdate = true;

	}

}

// Math Functions
// https://www.geeksforgeeks.org/convex-hull-set-2-graham-scan/
function getConvexHull( points ) {

	function orientation( p, q, r ) {

		const val =
			( q.y - p.y ) * ( r.x - q.x ) -
			( q.x - p.x ) * ( r.y - q.y );

		if ( val == 0 ) {

			return 0; // colinear

		}

		// clockwise or counterclockwise
		return ( val > 0 ) ? 1 : 2;

	}

	function distSq( p1, p2 ) {

		return ( p1.x - p2.x ) * ( p1.x - p2.x ) +
			( p1.y - p2.y ) * ( p1.y - p2.y );

	}

	function compare( p1, p2 ) {

	   // Find orientation
	   const o = orientation( p0, p1, p2 );
	   if ( o == 0 )
		 return ( distSq( p0, p2 ) >= distSq( p0, p1 ) ) ? - 1 : 1;

	   return ( o == 2 ) ? - 1 : 1;

	}

	// find the lowest point in 2d
	let lowestY = Infinity;
	let lowestIndex = - 1;
	for ( let i = 0, l = points.length; i < l; i ++ ) {

		const p = points[ i ];
		if ( p.y < lowestY ) {

			lowestIndex = i;
			lowestY = p.y;

		}

	}

	// sort the points
	const p0 = points[ lowestIndex ];
	points[ lowestIndex ] = points[ 0 ];
	points[ 0 ] = p0;

	points = points.sort( compare );

	// filter the points
	let m = 1;
	const n = points.length;
	for ( let i = 1; i < n; i ++ ) {

		while ( i < n - 1 && orientation( p0, points[ i ], points[ i + 1 ] ) == 0 ) {

			i ++;

		}

		points[ m ] = points[ i ];
		m ++;

	}

	// early out if we don't have enough points for a hull
	if ( m < 3 ) return null;

	// generate the hull
	const hull = [ points[ 0 ], points[ 1 ], points[ 2 ] ];
	for ( let i = 3; i < m; i ++ ) {

		while ( orientation( hull[ hull.length - 2 ], hull[ hull.length - 1 ], points[ i ] ) !== 2 ) {

			hull.pop();

		}

		hull.push( points[ i ] );

	}

	return hull;

}

function pointRayCrossesLine( point, line, prevDirection, thisDirection ) {

	const { start, end } = line;
	const px = point.x;
	const py = point.y;

	const sy = start.y;
	const ey = end.y;

	if ( sy === ey ) return false;

	if ( py > sy && py > ey ) return false; // above
	if ( py < sy && py < ey ) return false; // below

	const sx = start.x;
	const ex = end.x;
	if ( px > sx && px > ex ) return false; // right
	if ( px < sx && px < ex ) { // left

		if ( py === sy && prevDirection !== thisDirection ) {

			return false;

		}

		return true;

	}

	// check the side
	const dx = ex - sx;
	const dy = ey - sy;
	const perpx = dy;
	const perpy = - dx;

	const pdx = px - sx;
	const pdy = py - sy;

	const dot = perpx * pdx + perpy * pdy;

	if ( Math.sign( dot ) !== Math.sign( perpx ) ) {

		return true;

	}

	return false;

}

function pointRayCrossesSegments( point, segments ) {

	let crossings = 0;
	const firstSeg = segments[ segments.length - 1 ];
	let prevDirection = firstSeg.start.y > firstSeg.end.y;
	for ( let s = 0, l = segments.length; s < l; s ++ ) {

		const line = segments[ s ];
		const thisDirection = line.start.y > line.end.y;
		if ( pointRayCrossesLine( point, line, prevDirection, thisDirection ) ) {

			crossings ++;

		}

		prevDirection = thisDirection;

	}

	return crossings;

}

// https://stackoverflow.com/questions/3838329/how-can-i-check-if-two-segments-intersect
function lineCrossesLine( l1, l2 ) {

	function ccw( A, B, C ) {

		return ( C.y - A.y ) * ( B.x - A.x ) > ( B.y - A.y ) * ( C.x - A.x );

	}

	const A = l1.start;
	const B = l1.end;

	const C = l2.start;
	const D = l2.end;

	return ccw( A, C, D ) !== ccw( B, C, D ) && ccw( A, B, C ) !== ccw( A, B, D );

}