| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933 | ( function () {	// Note: "MATERIAL" tag (e.g. GLITTER, SPECKLE) is not implemented	const FINISH_TYPE_DEFAULT = 0;	const FINISH_TYPE_CHROME = 1;	const FINISH_TYPE_PEARLESCENT = 2;	const FINISH_TYPE_RUBBER = 3;	const FINISH_TYPE_MATTE_METALLIC = 4;	const FINISH_TYPE_METAL = 5; // State machine to search a subobject path.	// The LDraw standard establishes these various possible subfolders.	const FILE_LOCATION_AS_IS = 0;	const FILE_LOCATION_TRY_PARTS = 1;	const FILE_LOCATION_TRY_P = 2;	const FILE_LOCATION_TRY_MODELS = 3;	const FILE_LOCATION_TRY_RELATIVE = 4;	const FILE_LOCATION_TRY_ABSOLUTE = 5;	const FILE_LOCATION_NOT_FOUND = 6;	const _tempVec0 = new THREE.Vector3();	const _tempVec1 = new THREE.Vector3();	class LDrawConditionalLineMaterial extends THREE.ShaderMaterial {		constructor( parameters ) {			super( {				uniforms: THREE.UniformsUtils.merge( [ THREE.UniformsLib.fog, {					diffuse: {						value: new THREE.Color()					},					opacity: {						value: 1.0					}				} ] ),				vertexShader:      /* glsl */      `				attribute vec3 control0;				attribute vec3 control1;				attribute vec3 direction;				varying float discardFlag;				#include <common>				#include <color_pars_vertex>				#include <fog_pars_vertex>				#include <logdepthbuf_pars_vertex>				#include <clipping_planes_pars_vertex>				void main() {					#include <color_vertex>					vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );					gl_Position = projectionMatrix * mvPosition;					// Transform the line segment ends and control points into camera clip space					vec4 c0 = projectionMatrix * modelViewMatrix * vec4( control0, 1.0 );					vec4 c1 = projectionMatrix * modelViewMatrix * vec4( control1, 1.0 );					vec4 p0 = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );					vec4 p1 = projectionMatrix * modelViewMatrix * vec4( position + direction, 1.0 );					c0.xy /= c0.w;					c1.xy /= c1.w;					p0.xy /= p0.w;					p1.xy /= p1.w;					// Get the direction of the segment and an orthogonal vector					vec2 dir = p1.xy - p0.xy;					vec2 norm = vec2( -dir.y, dir.x );					// Get control point directions from the line					vec2 c0dir = c0.xy - p1.xy;					vec2 c1dir = c1.xy - p1.xy;					// If the vectors to the controls points are pointed in different directions away					// from the line segment then the line should not be drawn.					float d0 = dot( normalize( norm ), normalize( c0dir ) );					float d1 = dot( normalize( norm ), normalize( c1dir ) );					discardFlag = float( sign( d0 ) != sign( d1 ) );					#include <logdepthbuf_vertex>					#include <clipping_planes_vertex>					#include <fog_vertex>				}			`,				fragmentShader:      /* glsl */      `			uniform vec3 diffuse;			uniform float opacity;			varying float discardFlag;			#include <common>			#include <color_pars_fragment>			#include <fog_pars_fragment>			#include <logdepthbuf_pars_fragment>			#include <clipping_planes_pars_fragment>			void main() {				if ( discardFlag > 0.5 ) discard;				#include <clipping_planes_fragment>				vec3 outgoingLight = vec3( 0.0 );				vec4 diffuseColor = vec4( diffuse, opacity );				#include <logdepthbuf_fragment>				#include <color_fragment>				outgoingLight = diffuseColor.rgb; // simple shader				gl_FragColor = vec4( outgoingLight, diffuseColor.a );				#include <tonemapping_fragment>				#include <encodings_fragment>				#include <fog_fragment>				#include <premultiplied_alpha_fragment>			}			`			} );			Object.defineProperties( this, {				opacity: {					get: function () {						return this.uniforms.opacity.value;					},					set: function ( value ) {						this.uniforms.opacity.value = value;					}				},				color: {					get: function () {						return this.uniforms.diffuse.value;					}				}			} );			this.setValues( parameters );			this.isLDrawConditionalLineMaterial = true;		}	}	function smoothNormals( faces, lineSegments ) {		function hashVertex( v ) {			// NOTE: 1e2 is pretty coarse but was chosen because it allows edges			// to be smoothed as expected (see minifig arms). The errors between edges			// could be due to matrix multiplication.			const x = ~ ~ ( v.x * 1e2 );			const y = ~ ~ ( v.y * 1e2 );			const z = ~ ~ ( v.z * 1e2 );			return `${x},${y},${z}`;		}		function hashEdge( v0, v1 ) {			return `${hashVertex( v0 )}_${hashVertex( v1 )}`;		}		const hardEdges = new Set();		const halfEdgeList = {};		const normals = []; // Save the list of hard edges by hash		for ( let i = 0, l = lineSegments.length; i < l; i ++ ) {			const ls = lineSegments[ i ];			const vertices = ls.vertices;			const v0 = vertices[ 0 ];			const v1 = vertices[ 1 ];			hardEdges.add( hashEdge( v0, v1 ) );			hardEdges.add( hashEdge( v1, v0 ) );		} // track the half edges associated with each triangle		for ( let i = 0, l = faces.length; i < l; i ++ ) {			const tri = faces[ i ];			const vertices = tri.vertices;			const vertCount = vertices.length;			for ( let i2 = 0; i2 < vertCount; i2 ++ ) {				const index = i2;				const next = ( i2 + 1 ) % vertCount;				const v0 = vertices[ index ];				const v1 = vertices[ next ];				const hash = hashEdge( v0, v1 ); // don't add the triangle if the edge is supposed to be hard				if ( hardEdges.has( hash ) ) continue;				const info = {					index: index,					tri: tri				};				halfEdgeList[ hash ] = info;			}		} // Iterate until we've tried to connect all faces to share normals		while ( true ) {			// Stop if there are no more faces left			let halfEdge = null;			for ( const key in halfEdgeList ) {				halfEdge = halfEdgeList[ key ];				break;			}			if ( halfEdge === null ) {				break;			} // Exhaustively find all connected faces			const queue = [ halfEdge ];			while ( queue.length > 0 ) {				// initialize all vertex normals in this triangle				const tri = queue.pop().tri;				const vertices = tri.vertices;				const vertNormals = tri.normals;				const faceNormal = tri.faceNormal; // Check if any edge is connected to another triangle edge				const vertCount = vertices.length;				for ( let i2 = 0; i2 < vertCount; i2 ++ ) {					const index = i2;					const next = ( i2 + 1 ) % vertCount;					const v0 = vertices[ index ];					const v1 = vertices[ next ]; // delete this triangle from the list so it won't be found again					const hash = hashEdge( v0, v1 );					delete halfEdgeList[ hash ];					const reverseHash = hashEdge( v1, v0 );					const otherInfo = halfEdgeList[ reverseHash ];					if ( otherInfo ) {						const otherTri = otherInfo.tri;						const otherIndex = otherInfo.index;						const otherNormals = otherTri.normals;						const otherVertCount = otherNormals.length;						const otherFaceNormal = otherTri.faceNormal; // NOTE: If the angle between faces is > 67.5 degrees then assume it's						// hard edge. There are some cases where the line segments do not line up exactly						// with or span multiple triangle edges (see Lunar Vehicle wheels).						if ( Math.abs( otherTri.faceNormal.dot( tri.faceNormal ) ) < 0.25 ) {							continue;						} // if this triangle has already been traversed then it won't be in						// the halfEdgeList. If it has not then add it to the queue and delete						// it so it won't be found again.						if ( reverseHash in halfEdgeList ) {							queue.push( otherInfo );							delete halfEdgeList[ reverseHash ];						} // share the first normal						const otherNext = ( otherIndex + 1 ) % otherVertCount;						if ( vertNormals[ index ] && otherNormals[ otherNext ] && vertNormals[ index ] !== otherNormals[ otherNext ] ) {							otherNormals[ otherNext ].norm.add( vertNormals[ index ].norm );							vertNormals[ index ].norm = otherNormals[ otherNext ].norm;						}						let sharedNormal1 = vertNormals[ index ] || otherNormals[ otherNext ];						if ( sharedNormal1 === null ) {							// it's possible to encounter an edge of a triangle that has already been traversed meaning							// both edges already have different normals defined and shared. To work around this we create							// a wrapper object so when those edges are merged the normals can be updated everywhere.							sharedNormal1 = {								norm: new THREE.Vector3()							};							normals.push( sharedNormal1.norm );						}						if ( vertNormals[ index ] === null ) {							vertNormals[ index ] = sharedNormal1;							sharedNormal1.norm.add( faceNormal );						}						if ( otherNormals[ otherNext ] === null ) {							otherNormals[ otherNext ] = sharedNormal1;							sharedNormal1.norm.add( otherFaceNormal );						} // share the second normal						if ( vertNormals[ next ] && otherNormals[ otherIndex ] && vertNormals[ next ] !== otherNormals[ otherIndex ] ) {							otherNormals[ otherIndex ].norm.add( vertNormals[ next ].norm );							vertNormals[ next ].norm = otherNormals[ otherIndex ].norm;						}						let sharedNormal2 = vertNormals[ next ] || otherNormals[ otherIndex ];						if ( sharedNormal2 === null ) {							sharedNormal2 = {								norm: new THREE.Vector3()							};							normals.push( sharedNormal2.norm );						}						if ( vertNormals[ next ] === null ) {							vertNormals[ next ] = sharedNormal2;							sharedNormal2.norm.add( faceNormal );						}						if ( otherNormals[ otherIndex ] === null ) {							otherNormals[ otherIndex ] = sharedNormal2;							sharedNormal2.norm.add( otherFaceNormal );						}					}				}			}		} // The normals of each face have been added up so now we average them by normalizing the vector.		for ( let i = 0, l = normals.length; i < l; i ++ ) {			normals[ i ].normalize();		}	}	function isPartType( type ) {		return type === 'Part';	}	function isModelType( type ) {		return type === 'Model' || type === 'Unofficial_Model';	}	function isPrimitiveType( type ) {		return /primitive/i.test( type ) || type === 'Subpart';	}	class LineParser {		constructor( line, lineNumber ) {			this.line = line;			this.lineLength = line.length;			this.currentCharIndex = 0;			this.currentChar = ' ';			this.lineNumber = lineNumber;		}		seekNonSpace() {			while ( this.currentCharIndex < this.lineLength ) {				this.currentChar = this.line.charAt( this.currentCharIndex );				if ( this.currentChar !== ' ' && this.currentChar !== '\t' ) {					return;				}				this.currentCharIndex ++;			}		}		getToken() {			const pos0 = this.currentCharIndex ++; // Seek space			while ( this.currentCharIndex < this.lineLength ) {				this.currentChar = this.line.charAt( this.currentCharIndex );				if ( this.currentChar === ' ' || this.currentChar === '\t' ) {					break;				}				this.currentCharIndex ++;			}			const pos1 = this.currentCharIndex;			this.seekNonSpace();			return this.line.substring( pos0, pos1 );		}		getRemainingString() {			return this.line.substring( this.currentCharIndex, this.lineLength );		}		isAtTheEnd() {			return this.currentCharIndex >= this.lineLength;		}		setToEnd() {			this.currentCharIndex = this.lineLength;		}		getLineNumberString() {			return this.lineNumber >= 0 ? ' at line ' + this.lineNumber : '';		}	}	class LDrawFileCache {		constructor( loader ) {			this.cache = {};			this.loader = loader;		}		setData( key, contents ) {			this.cache[ key.toLowerCase() ] = contents;		}		async loadData( fileName ) {			const key = fileName.toLowerCase();			if ( key in this.cache ) {				return this.cache[ key ];			}			this.cache[ fileName ] = new Promise( async ( resolve, reject ) => {				let triedLowerCase = false;				let locationState = FILE_LOCATION_AS_IS;				while ( locationState !== FILE_LOCATION_NOT_FOUND ) {					let subobjectURL = fileName;					switch ( locationState ) {						case FILE_LOCATION_AS_IS:							locationState = locationState + 1;							break;						case FILE_LOCATION_TRY_PARTS:							subobjectURL = 'parts/' + subobjectURL;							locationState = locationState + 1;							break;						case FILE_LOCATION_TRY_P:							subobjectURL = 'p/' + subobjectURL;							locationState = locationState + 1;							break;						case FILE_LOCATION_TRY_MODELS:							subobjectURL = 'models/' + subobjectURL;							locationState = locationState + 1;							break;						case FILE_LOCATION_TRY_RELATIVE:							subobjectURL = fileName.substring( 0, fileName.lastIndexOf( '/' ) + 1 ) + subobjectURL;							locationState = locationState + 1;							break;						case FILE_LOCATION_TRY_ABSOLUTE:							if ( triedLowerCase ) {								// Try absolute path								locationState = FILE_LOCATION_NOT_FOUND;							} else {								// Next attempt is lower case								fileName = fileName.toLowerCase();								subobjectURL = fileName;								triedLowerCase = true;								locationState = FILE_LOCATION_AS_IS;							}							break;					}					const loader = this.loader;					const fileLoader = new THREE.FileLoader( loader.manager );					fileLoader.setPath( loader.partsLibraryPath );					fileLoader.setRequestHeader( loader.requestHeader );					fileLoader.setWithCredentials( loader.withCredentials );					try {						const text = await fileLoader.loadAsync( subobjectURL );						this.setData( fileName, text );						resolve( text );						return;					} catch {						continue;					}				}				reject();			} );			return this.cache[ fileName ];		}	}	function sortByMaterial( a, b ) {		if ( a.colourCode === b.colourCode ) {			return 0;		}		if ( a.colourCode < b.colourCode ) {			return - 1;		}		return 1;	}	function createObject( elements, elementSize, isConditionalSegments = false, totalElements = null ) {		// Creates a THREE.LineSegments (elementSize = 2) or a THREE.Mesh (elementSize = 3 )		// With per face / segment material, implemented with mesh groups and materials array		// Sort the faces or line segments by colour code to make later the mesh groups		elements.sort( sortByMaterial );		if ( totalElements === null ) {			totalElements = elements.length;		}		const positions = new Float32Array( elementSize * totalElements * 3 );		const normals = elementSize === 3 ? new Float32Array( elementSize * totalElements * 3 ) : null;		const materials = [];		const quadArray = new Array( 6 );		const bufferGeometry = new THREE.BufferGeometry();		let prevMaterial = null;		let index0 = 0;		let numGroupVerts = 0;		let offset = 0;		for ( let iElem = 0, nElem = elements.length; iElem < nElem; iElem ++ ) {			const elem = elements[ iElem ];			let vertices = elem.vertices;			if ( vertices.length === 4 ) {				quadArray[ 0 ] = vertices[ 0 ];				quadArray[ 1 ] = vertices[ 1 ];				quadArray[ 2 ] = vertices[ 2 ];				quadArray[ 3 ] = vertices[ 0 ];				quadArray[ 4 ] = vertices[ 2 ];				quadArray[ 5 ] = vertices[ 3 ];				vertices = quadArray;			}			for ( let j = 0, l = vertices.length; j < l; j ++ ) {				const v = vertices[ j ];				const index = offset + j * 3;				positions[ index + 0 ] = v.x;				positions[ index + 1 ] = v.y;				positions[ index + 2 ] = v.z;			}			if ( elementSize === 3 ) {				let elemNormals = elem.normals;				if ( elemNormals.length === 4 ) {					quadArray[ 0 ] = elemNormals[ 0 ];					quadArray[ 1 ] = elemNormals[ 1 ];					quadArray[ 2 ] = elemNormals[ 2 ];					quadArray[ 3 ] = elemNormals[ 0 ];					quadArray[ 4 ] = elemNormals[ 2 ];					quadArray[ 5 ] = elemNormals[ 3 ];					elemNormals = quadArray;				}				for ( let j = 0, l = elemNormals.length; j < l; j ++ ) {					let n = elem.faceNormal;					if ( elemNormals[ j ] ) {						n = elemNormals[ j ].norm;					}					const index = offset + j * 3;					normals[ index + 0 ] = n.x;					normals[ index + 1 ] = n.y;					normals[ index + 2 ] = n.z;				}			}			if ( prevMaterial !== elem.material ) {				if ( prevMaterial !== null ) {					bufferGeometry.addGroup( index0, numGroupVerts, materials.length - 1 );				}				materials.push( elem.material );				prevMaterial = elem.material;				index0 = offset / 3;				numGroupVerts = vertices.length;			} else {				numGroupVerts += vertices.length;			}			offset += 3 * vertices.length;		}		if ( numGroupVerts > 0 ) {			bufferGeometry.addGroup( index0, Infinity, materials.length - 1 );		}		bufferGeometry.setAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );		if ( normals !== null ) {			bufferGeometry.setAttribute( 'normal', new THREE.BufferAttribute( normals, 3 ) );		}		let object3d = null;		if ( elementSize === 2 ) {			object3d = new THREE.LineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );		} else if ( elementSize === 3 ) {			object3d = new THREE.Mesh( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );		}		if ( isConditionalSegments ) {			object3d.isConditionalLine = true;			const controlArray0 = new Float32Array( elements.length * 3 * 2 );			const controlArray1 = new Float32Array( elements.length * 3 * 2 );			const directionArray = new Float32Array( elements.length * 3 * 2 );			for ( let i = 0, l = elements.length; i < l; i ++ ) {				const os = elements[ i ];				const vertices = os.vertices;				const controlPoints = os.controlPoints;				const c0 = controlPoints[ 0 ];				const c1 = controlPoints[ 1 ];				const v0 = vertices[ 0 ];				const v1 = vertices[ 1 ];				const index = i * 3 * 2;				controlArray0[ index + 0 ] = c0.x;				controlArray0[ index + 1 ] = c0.y;				controlArray0[ index + 2 ] = c0.z;				controlArray0[ index + 3 ] = c0.x;				controlArray0[ index + 4 ] = c0.y;				controlArray0[ index + 5 ] = c0.z;				controlArray1[ index + 0 ] = c1.x;				controlArray1[ index + 1 ] = c1.y;				controlArray1[ index + 2 ] = c1.z;				controlArray1[ index + 3 ] = c1.x;				controlArray1[ index + 4 ] = c1.y;				controlArray1[ index + 5 ] = c1.z;				directionArray[ index + 0 ] = v1.x - v0.x;				directionArray[ index + 1 ] = v1.y - v0.y;				directionArray[ index + 2 ] = v1.z - v0.z;				directionArray[ index + 3 ] = v1.x - v0.x;				directionArray[ index + 4 ] = v1.y - v0.y;				directionArray[ index + 5 ] = v1.z - v0.z;			}			bufferGeometry.setAttribute( 'control0', new THREE.BufferAttribute( controlArray0, 3, false ) );			bufferGeometry.setAttribute( 'control1', new THREE.BufferAttribute( controlArray1, 3, false ) );			bufferGeometry.setAttribute( 'direction', new THREE.BufferAttribute( directionArray, 3, false ) );		}		return object3d;	} //	class LDrawLoader extends THREE.Loader {		constructor( manager ) {			super( manager ); // Array of THREE.Material			this.materials = []; // Not using THREE.Cache here because it returns the previous HTML error response instead of calling onError()			// This also allows to handle the embedded text files ("0 FILE" lines)			this.cache = new LDrawFileCache( this ); // This object is a map from file names to paths. It agilizes the paths search. If it is not set then files will be searched by trial and error.			this.fileMap = null;			this.rootParseScope = this.newParseScopeLevel(); // Add default main triangle and line edge materials (used in pieces that can be coloured with a main color)			this.setMaterials( [ this.parseColourMetaDirective( new LineParser( 'Main_Colour CODE 16 VALUE #FF8080 EDGE #333333' ) ), this.parseColourMetaDirective( new LineParser( 'Edge_Colour CODE 24 VALUE #A0A0A0 EDGE #333333' ) ) ] ); // If this flag is set to true, each subobject will be a Object.			// If not (the default), only one object which contains all the merged primitives will be created.			this.separateObjects = false; // If this flag is set to true the vertex normals will be smoothed.			this.smoothNormals = true; // The path to load parts from the LDraw parts library from.			this.partsLibraryPath = '';		}		setPartsLibraryPath( path ) {			this.partsLibraryPath = path;			return this;		}		async preloadMaterials( url ) {			const fileLoader = new THREE.FileLoader( this.manager );			fileLoader.setPath( this.path );			fileLoader.setRequestHeader( this.requestHeader );			fileLoader.setWithCredentials( this.withCredentials );			const text = await fileLoader.loadAsync( url );			const colorLineRegex = /^0 !COLOUR/;			const lines = text.split( /[\n\r]/g );			const materials = [];			for ( let i = 0, l = lines.length; i < l; i ++ ) {				const line = lines[ i ];				if ( colorLineRegex.test( line ) ) {					const directive = line.replace( colorLineRegex, '' );					const material = this.parseColourMetaDirective( new LineParser( directive ) );					materials.push( material );				}			}			this.setMaterials( materials );		}		load( url, onLoad, onProgress, onError ) {			if ( ! this.fileMap ) {				this.fileMap = {};			}			const fileLoader = new THREE.FileLoader( this.manager );			fileLoader.setPath( this.path );			fileLoader.setRequestHeader( this.requestHeader );			fileLoader.setWithCredentials( this.withCredentials );			fileLoader.load( url, text => {				this.processObject( text, null, url, this.rootParseScope ).then( function ( result ) {					onLoad( result.groupObject );				} );			}, onProgress, onError );		}		parse( text, path, onLoad ) {			// Async parse.  This function calls onParse with the parsed THREE.Object3D as parameter			this.processObject( text, null, path, this.rootParseScope ).then( function ( result ) {				onLoad( result.groupObject );			} );		}		setMaterials( materials ) {			// Clears parse scopes stack, adds new scope with material library			this.rootParseScope = this.newParseScopeLevel( materials );			this.rootParseScope.isFromParse = false;			this.materials = materials;			return this;		}		setFileMap( fileMap ) {			this.fileMap = fileMap;			return this;		}		newParseScopeLevel( materials = null, parentScope = null ) {			// Adds a new scope level, assign materials to it and returns it			const matLib = {};			if ( materials ) {				for ( let i = 0, n = materials.length; i < n; i ++ ) {					const material = materials[ i ];					matLib[ material.userData.code ] = material;				}			}			const newParseScope = {				parentScope: parentScope,				lib: matLib,				url: null,				// Subobjects				subobjects: null,				numSubobjects: 0,				subobjectIndex: 0,				inverted: false,				category: null,				keywords: null,				// Current subobject				currentFileName: null,				mainColourCode: parentScope ? parentScope.mainColourCode : '16',				mainEdgeColourCode: parentScope ? parentScope.mainEdgeColourCode : '24',				currentMatrix: new THREE.Matrix4(),				matrix: new THREE.Matrix4(),				// If false, it is a root material scope previous to parse				isFromParse: true,				faces: null,				lineSegments: null,				conditionalSegments: null,				totalFaces: 0,				// If true, this object is the start of a construction step				startingConstructionStep: false			};			return newParseScope;		}		addMaterial( material, parseScope ) {			// Adds a material to the material library which is on top of the parse scopes stack. And also to the materials array			const matLib = parseScope.lib;			if ( ! matLib[ material.userData.code ] ) {				this.materials.push( material );			}			matLib[ material.userData.code ] = material;			return this;		}		getMaterial( colourCode, parseScope = this.rootParseScope ) {			// Given a colour code search its material in the parse scopes stack			if ( colourCode.startsWith( '0x2' ) ) {				// Special 'direct' material value (RGB colour)				const colour = colourCode.substring( 3 );				return this.parseColourMetaDirective( new LineParser( 'Direct_Color_' + colour + ' CODE -1 VALUE #' + colour + ' EDGE #' + colour + '' ) );			}			while ( parseScope ) {				const material = parseScope.lib[ colourCode ];				if ( material ) {					return material;				} else {					parseScope = parseScope.parentScope;				}			} // Material was not found			return null;		}		parseColourMetaDirective( lineParser ) {			// Parses a colour definition and returns a THREE.Material			let code = null; // Triangle and line colours			let colour = 0xFF00FF;			let edgeColour = 0xFF00FF; // Transparency			let alpha = 1;			let isTransparent = false; // Self-illumination:			let luminance = 0;			let finishType = FINISH_TYPE_DEFAULT;			let edgeMaterial = null;			const name = lineParser.getToken();			if ( ! name ) {				throw 'LDrawLoader: Material name was expected after "!COLOUR tag' + lineParser.getLineNumberString() + '.';			} // Parse tag tokens and their parameters			let token = null;			while ( true ) {				token = lineParser.getToken();				if ( ! token ) {					break;				}				switch ( token.toUpperCase() ) {					case 'CODE':						code = lineParser.getToken();						break;					case 'VALUE':						colour = lineParser.getToken();						if ( colour.startsWith( '0x' ) ) {							colour = '#' + colour.substring( 2 );						} else if ( ! colour.startsWith( '#' ) ) {							throw 'LDrawLoader: Invalid colour while parsing material' + lineParser.getLineNumberString() + '.';						}						break;					case 'EDGE':						edgeColour = lineParser.getToken();						if ( edgeColour.startsWith( '0x' ) ) {							edgeColour = '#' + edgeColour.substring( 2 );						} else if ( ! edgeColour.startsWith( '#' ) ) {							// Try to see if edge colour is a colour code							edgeMaterial = this.getMaterial( edgeColour );							if ( ! edgeMaterial ) {								throw 'LDrawLoader: Invalid edge colour while parsing material' + lineParser.getLineNumberString() + '.';							} // Get the edge material for this triangle material							edgeMaterial = edgeMaterial.userData.edgeMaterial;						}						break;					case 'ALPHA':						alpha = parseInt( lineParser.getToken() );						if ( isNaN( alpha ) ) {							throw 'LDrawLoader: Invalid alpha value in material definition' + lineParser.getLineNumberString() + '.';						}						alpha = Math.max( 0, Math.min( 1, alpha / 255 ) );						if ( alpha < 1 ) {							isTransparent = true;						}						break;					case 'LUMINANCE':						luminance = parseInt( lineParser.getToken() );						if ( isNaN( luminance ) ) {							throw 'LDrawLoader: Invalid luminance value in material definition' + LineParser.getLineNumberString() + '.';						}						luminance = Math.max( 0, Math.min( 1, luminance / 255 ) );						break;					case 'CHROME':						finishType = FINISH_TYPE_CHROME;						break;					case 'PEARLESCENT':						finishType = FINISH_TYPE_PEARLESCENT;						break;					case 'RUBBER':						finishType = FINISH_TYPE_RUBBER;						break;					case 'MATTE_METALLIC':						finishType = FINISH_TYPE_MATTE_METALLIC;						break;					case 'METAL':						finishType = FINISH_TYPE_METAL;						break;					case 'MATERIAL':						// Not implemented						lineParser.setToEnd();						break;					default:						throw 'LDrawLoader: Unknown token "' + token + '" while parsing material' + lineParser.getLineNumberString() + '.';						break;				}			}			let material = null;			switch ( finishType ) {				case FINISH_TYPE_DEFAULT:					material = new THREE.MeshStandardMaterial( {						color: colour,						roughness: 0.3,						metalness: 0					} );					break;				case FINISH_TYPE_PEARLESCENT:					// Try to imitate pearlescency by setting the specular to the complementary of the color, and low shininess					const specular = new THREE.Color( colour );					const hsl = specular.getHSL( {						h: 0,						s: 0,						l: 0					} );					hsl.h = ( hsl.h + 0.5 ) % 1;					hsl.l = Math.min( 1, hsl.l + ( 1 - hsl.l ) * 0.7 );					specular.setHSL( hsl.h, hsl.s, hsl.l );					material = new THREE.MeshPhongMaterial( {						color: colour,						specular: specular,						shininess: 10,						reflectivity: 0.3					} );					break;				case FINISH_TYPE_CHROME:					// Mirror finish surface					material = new THREE.MeshStandardMaterial( {						color: colour,						roughness: 0,						metalness: 1					} );					break;				case FINISH_TYPE_RUBBER:					// Rubber finish					material = new THREE.MeshStandardMaterial( {						color: colour,						roughness: 0.9,						metalness: 0					} );					break;				case FINISH_TYPE_MATTE_METALLIC:					// Brushed metal finish					material = new THREE.MeshStandardMaterial( {						color: colour,						roughness: 0.8,						metalness: 0.4					} );					break;				case FINISH_TYPE_METAL:					// Average metal finish					material = new THREE.MeshStandardMaterial( {						color: colour,						roughness: 0.2,						metalness: 0.85					} );					break;				default:					// Should not happen					break;			}			material.transparent = isTransparent;			material.premultipliedAlpha = true;			material.opacity = alpha;			material.depthWrite = ! isTransparent;			material.polygonOffset = true;			material.polygonOffsetFactor = 1;			if ( luminance !== 0 ) {				material.emissive.set( material.color ).multiplyScalar( luminance );			}			if ( ! edgeMaterial ) {				// This is the material used for edges				edgeMaterial = new THREE.LineBasicMaterial( {					color: edgeColour,					transparent: isTransparent,					opacity: alpha,					depthWrite: ! isTransparent				} );				edgeMaterial.userData.code = code;				edgeMaterial.name = name + ' - Edge'; // This is the material used for conditional edges				edgeMaterial.userData.conditionalEdgeMaterial = new LDrawConditionalLineMaterial( {					fog: true,					transparent: isTransparent,					depthWrite: ! isTransparent,					color: edgeColour,					opacity: alpha				} );			}			material.userData.code = code;			material.name = name;			material.userData.edgeMaterial = edgeMaterial;			return material;		} //		objectParse( text, parseScope ) {			// Retrieve data from the parent parse scope			const currentParseScope = parseScope;			const parentParseScope = currentParseScope.parentScope; // Main colour codes passed to this subobject (or default codes 16 and 24 if it is the root object)			const mainColourCode = currentParseScope.mainColourCode;			const mainEdgeColourCode = currentParseScope.mainEdgeColourCode; // Parse result variables			let faces;			let lineSegments;			let conditionalSegments;			const subobjects = [];			let category = null;			let keywords = null;			if ( text.indexOf( '\r\n' ) !== - 1 ) {				// This is faster than String.split with regex that splits on both				text = text.replace( /\r\n/g, '\n' );			}			const lines = text.split( '\n' );			const numLines = lines.length;			let parsingEmbeddedFiles = false;			let currentEmbeddedFileName = null;			let currentEmbeddedText = null;			let bfcCertified = false;			let bfcCCW = true;			let bfcInverted = false;			let bfcCull = true;			let type = '';			let startingConstructionStep = false;			const scope = this;			function parseColourCode( lineParser, forEdge ) {				// Parses next colour code and returns a THREE.Material				let colourCode = lineParser.getToken();				if ( ! forEdge && colourCode === '16' ) {					colourCode = mainColourCode;				}				if ( forEdge && colourCode === '24' ) {					colourCode = mainEdgeColourCode;				}				const material = scope.getMaterial( colourCode, currentParseScope );				if ( ! material ) {					throw 'LDrawLoader: Unknown colour code "' + colourCode + '" is used' + lineParser.getLineNumberString() + ' but it was not defined previously.';				}				return material;			}			function parseVector( lp ) {				const v = new THREE.Vector3( parseFloat( lp.getToken() ), parseFloat( lp.getToken() ), parseFloat( lp.getToken() ) );				if ( ! scope.separateObjects ) {					v.applyMatrix4( currentParseScope.currentMatrix );				}				return v;			} // Parse all line commands			for ( let lineIndex = 0; lineIndex < numLines; lineIndex ++ ) {				const line = lines[ lineIndex ];				if ( line.length === 0 ) continue;				if ( parsingEmbeddedFiles ) {					if ( line.startsWith( '0 FILE ' ) ) {						// Save previous embedded file in the cache						this.cache.setData( currentEmbeddedFileName.toLowerCase(), currentEmbeddedText ); // New embedded text file						currentEmbeddedFileName = line.substring( 7 );						currentEmbeddedText = '';					} else {						currentEmbeddedText += line + '\n';					}					continue;				}				const lp = new LineParser( line, lineIndex + 1 );				lp.seekNonSpace();				if ( lp.isAtTheEnd() ) {					// Empty line					continue;				} // Parse the line type				const lineType = lp.getToken();				let material;				let segment;				let inverted;				let ccw;				let doubleSided;				let v0, v1, v2, v3, c0, c1, faceNormal;				switch ( lineType ) {					// Line type 0: Comment or META					case '0':						// Parse meta directive						const meta = lp.getToken();						if ( meta ) {							switch ( meta ) {								case '!LDRAW_ORG':									type = lp.getToken();									currentParseScope.faces = [];									currentParseScope.lineSegments = [];									currentParseScope.conditionalSegments = [];									currentParseScope.type = type;									const isRoot = ! parentParseScope.isFromParse;									if ( isRoot || scope.separateObjects && ! isPrimitiveType( type ) ) {										currentParseScope.groupObject = new THREE.Group();										currentParseScope.groupObject.userData.startingConstructionStep = currentParseScope.startingConstructionStep;									} // If the scale of the object is negated then the triangle winding order									// needs to be flipped.									if ( currentParseScope.matrix.determinant() < 0 && ( scope.separateObjects && isPrimitiveType( type ) || ! scope.separateObjects ) ) {										currentParseScope.inverted = ! currentParseScope.inverted;									}									faces = currentParseScope.faces;									lineSegments = currentParseScope.lineSegments;									conditionalSegments = currentParseScope.conditionalSegments;									break;								case '!COLOUR':									material = this.parseColourMetaDirective( lp );									if ( material ) {										this.addMaterial( material, parseScope );									} else {										console.warn( 'LDrawLoader: Error parsing material' + lp.getLineNumberString() );									}									break;								case '!CATEGORY':									category = lp.getToken();									break;								case '!KEYWORDS':									const newKeywords = lp.getRemainingString().split( ',' );									if ( newKeywords.length > 0 ) {										if ( ! keywords ) {											keywords = [];										}										newKeywords.forEach( function ( keyword ) {											keywords.push( keyword.trim() );										} );									}									break;								case 'FILE':									if ( lineIndex > 0 ) {										// Start embedded text files parsing										parsingEmbeddedFiles = true;										currentEmbeddedFileName = lp.getRemainingString();										currentEmbeddedText = '';										bfcCertified = false;										bfcCCW = true;									}									break;								case 'BFC':									// Changes to the backface culling state									while ( ! lp.isAtTheEnd() ) {										const token = lp.getToken();										switch ( token ) {											case 'CERTIFY':											case 'NOCERTIFY':												bfcCertified = token === 'CERTIFY';												bfcCCW = true;												break;											case 'CW':											case 'CCW':												bfcCCW = token === 'CCW';												break;											case 'INVERTNEXT':												bfcInverted = true;												break;											case 'CLIP':											case 'NOCLIP':												bfcCull = token === 'CLIP';												break;											default:												console.warn( 'THREE.LDrawLoader: BFC directive "' + token + '" is unknown.' );												break;										}									}									break;								case 'STEP':									startingConstructionStep = true;									break;								default:									// Other meta directives are not implemented									break;							}						}						break;						// Line type 1: Sub-object file					case '1':						material = parseColourCode( lp );						const posX = parseFloat( lp.getToken() );						const posY = parseFloat( lp.getToken() );						const posZ = parseFloat( lp.getToken() );						const m0 = parseFloat( lp.getToken() );						const m1 = parseFloat( lp.getToken() );						const m2 = parseFloat( lp.getToken() );						const m3 = parseFloat( lp.getToken() );						const m4 = parseFloat( lp.getToken() );						const m5 = parseFloat( lp.getToken() );						const m6 = parseFloat( lp.getToken() );						const m7 = parseFloat( lp.getToken() );						const m8 = parseFloat( lp.getToken() );						const matrix = new THREE.Matrix4().set( m0, m1, m2, posX, m3, m4, m5, posY, m6, m7, m8, posZ, 0, 0, 0, 1 );						let fileName = lp.getRemainingString().trim().replace( /\\/g, '/' );						if ( scope.fileMap[ fileName ] ) {							// Found the subobject path in the preloaded file path map							fileName = scope.fileMap[ fileName ];						} else {							// Standardized subfolders							if ( fileName.startsWith( 's/' ) ) {								fileName = 'parts/' + fileName;							} else if ( fileName.startsWith( '48/' ) ) {								fileName = 'p/' + fileName;							}						}						subobjects.push( {							material: material,							matrix: matrix,							fileName: fileName,							inverted: bfcInverted !== currentParseScope.inverted,							startingConstructionStep: startingConstructionStep						} );						bfcInverted = false;						break;						// Line type 2: Line segment					case '2':						material = parseColourCode( lp, true );						v0 = parseVector( lp );						v1 = parseVector( lp );						segment = {							material: material.userData.edgeMaterial,							colourCode: material.userData.code,							v0: v0,							v1: v1,							vertices: [ v0, v1 ]						};						lineSegments.push( segment );						break;						// Line type 5: Conditional Line segment					case '5':						material = parseColourCode( lp, true );						v0 = parseVector( lp );						v1 = parseVector( lp );						c0 = parseVector( lp );						c1 = parseVector( lp );						segment = {							material: material.userData.edgeMaterial.userData.conditionalEdgeMaterial,							colourCode: material.userData.code,							vertices: [ v0, v1 ],							controlPoints: [ c0, c1 ]						};						conditionalSegments.push( segment );						break;						// Line type 3: Triangle					case '3':						material = parseColourCode( lp );						inverted = currentParseScope.inverted;						ccw = bfcCCW !== inverted;						doubleSided = ! bfcCertified || ! bfcCull;						if ( ccw === true ) {							v0 = parseVector( lp );							v1 = parseVector( lp );							v2 = parseVector( lp );						} else {							v2 = parseVector( lp );							v1 = parseVector( lp );							v0 = parseVector( lp );						}						_tempVec0.subVectors( v1, v0 );						_tempVec1.subVectors( v2, v1 );						faceNormal = new THREE.Vector3().crossVectors( _tempVec0, _tempVec1 ).normalize();						faces.push( {							material: material,							colourCode: material.userData.code,							faceNormal: faceNormal,							vertices: [ v0, v1, v2 ],							normals: [ null, null, null ]						} );						currentParseScope.totalFaces ++;						if ( doubleSided === true ) {							faces.push( {								material: material,								colourCode: material.userData.code,								faceNormal: faceNormal,								vertices: [ v2, v1, v0 ],								normals: [ null, null, null ]							} );							currentParseScope.totalFaces ++;						}						break;						// Line type 4: Quadrilateral					case '4':						material = parseColourCode( lp );						inverted = currentParseScope.inverted;						ccw = bfcCCW !== inverted;						doubleSided = ! bfcCertified || ! bfcCull;						if ( ccw === true ) {							v0 = parseVector( lp );							v1 = parseVector( lp );							v2 = parseVector( lp );							v3 = parseVector( lp );						} else {							v3 = parseVector( lp );							v2 = parseVector( lp );							v1 = parseVector( lp );							v0 = parseVector( lp );						}						_tempVec0.subVectors( v1, v0 );						_tempVec1.subVectors( v2, v1 );						faceNormal = new THREE.Vector3().crossVectors( _tempVec0, _tempVec1 ).normalize(); // specifically place the triangle diagonal in the v0 and v1 slots so we can						// account for the doubling of vertices later when smoothing normals.						faces.push( {							material: material,							colourCode: material.userData.code,							faceNormal: faceNormal,							vertices: [ v0, v1, v2, v3 ],							normals: [ null, null, null, null ]						} );						currentParseScope.totalFaces += 2;						if ( doubleSided === true ) {							faces.push( {								material: material,								colourCode: material.userData.code,								faceNormal: faceNormal,								vertices: [ v3, v2, v1, v0 ],								normals: [ null, null, null, null ]							} );							currentParseScope.totalFaces += 2;						}						break;					default:						throw 'LDrawLoader: Unknown line type "' + lineType + '"' + lp.getLineNumberString() + '.';						break;				}			}			if ( parsingEmbeddedFiles ) {				this.cache.setData( currentEmbeddedFileName.toLowerCase(), currentEmbeddedText );			}			currentParseScope.category = category;			currentParseScope.keywords = keywords;			currentParseScope.subobjects = subobjects;			currentParseScope.numSubobjects = subobjects.length;			currentParseScope.subobjectIndex = 0;		}		computeConstructionSteps( model ) {			// Sets userdata.constructionStep number in THREE.Group objects and userData.numConstructionSteps number in the root THREE.Group object.			let stepNumber = 0;			model.traverse( c => {				if ( c.isGroup ) {					if ( c.userData.startingConstructionStep ) {						stepNumber ++;					}					c.userData.constructionStep = stepNumber;				}			} );			model.userData.numConstructionSteps = stepNumber + 1;		}		finalizeObject( subobjectParseScope ) {			const parentParseScope = subobjectParseScope.parentScope; // Smooth the normals if this is a part or if this is a case where the subpart			// is added directly into the parent model (meaning it will never get smoothed by			// being added to a part)			const doSmooth = isPartType( subobjectParseScope.type ) || ! isPartType( subobjectParseScope.type ) && ! isModelType( subobjectParseScope.type ) && isModelType( subobjectParseScope.parentScope.type );			if ( this.smoothNormals && doSmooth ) {				smoothNormals( subobjectParseScope.faces, subobjectParseScope.lineSegments );			}			const isRoot = ! parentParseScope.isFromParse;			if ( this.separateObjects && ! isPrimitiveType( subobjectParseScope.type ) || isRoot ) {				const objGroup = subobjectParseScope.groupObject;				if ( subobjectParseScope.faces.length > 0 ) {					objGroup.add( createObject( subobjectParseScope.faces, 3, false, subobjectParseScope.totalFaces ) );				}				if ( subobjectParseScope.lineSegments.length > 0 ) {					objGroup.add( createObject( subobjectParseScope.lineSegments, 2 ) );				}				if ( subobjectParseScope.conditionalSegments.length > 0 ) {					objGroup.add( createObject( subobjectParseScope.conditionalSegments, 2, true ) );				}				if ( parentParseScope.groupObject ) {					objGroup.name = subobjectParseScope.fileName;					objGroup.userData.category = subobjectParseScope.category;					objGroup.userData.keywords = subobjectParseScope.keywords;					subobjectParseScope.matrix.decompose( objGroup.position, objGroup.quaternion, objGroup.scale );					parentParseScope.groupObject.add( objGroup );				}			} else {				const separateObjects = this.separateObjects;				const parentLineSegments = parentParseScope.lineSegments;				const parentConditionalSegments = parentParseScope.conditionalSegments;				const parentFaces = parentParseScope.faces;				const lineSegments = subobjectParseScope.lineSegments;				const conditionalSegments = subobjectParseScope.conditionalSegments;				const faces = subobjectParseScope.faces;				for ( let i = 0, l = lineSegments.length; i < l; i ++ ) {					const ls = lineSegments[ i ];					if ( separateObjects ) {						const vertices = ls.vertices;						vertices[ 0 ].applyMatrix4( subobjectParseScope.matrix );						vertices[ 1 ].applyMatrix4( subobjectParseScope.matrix );					}					parentLineSegments.push( ls );				}				for ( let i = 0, l = conditionalSegments.length; i < l; i ++ ) {					const os = conditionalSegments[ i ];					if ( separateObjects ) {						const vertices = os.vertices;						const controlPoints = os.controlPoints;						vertices[ 0 ].applyMatrix4( subobjectParseScope.matrix );						vertices[ 1 ].applyMatrix4( subobjectParseScope.matrix );						controlPoints[ 0 ].applyMatrix4( subobjectParseScope.matrix );						controlPoints[ 1 ].applyMatrix4( subobjectParseScope.matrix );					}					parentConditionalSegments.push( os );				}				for ( let i = 0, l = faces.length; i < l; i ++ ) {					const tri = faces[ i ];					if ( separateObjects ) {						const vertices = tri.vertices;						for ( let i = 0, l = vertices.length; i < l; i ++ ) {							vertices[ i ] = vertices[ i ].clone().applyMatrix4( subobjectParseScope.matrix );						}						_tempVec0.subVectors( vertices[ 1 ], vertices[ 0 ] );						_tempVec1.subVectors( vertices[ 2 ], vertices[ 1 ] );						tri.faceNormal.crossVectors( _tempVec0, _tempVec1 ).normalize();					}					parentFaces.push( tri );				}				parentParseScope.totalFaces += subobjectParseScope.totalFaces;			}		}		async processObject( text, subobject, url, parentScope ) {			const scope = this;			const parseScope = this.newParseScopeLevel( null, parentScope );			parseScope.url = url;			const parentParseScope = parseScope.parentScope; // Set current matrix			if ( subobject ) {				parseScope.currentMatrix.multiplyMatrices( parentParseScope.currentMatrix, subobject.matrix );				parseScope.matrix.copy( subobject.matrix );				parseScope.inverted = subobject.inverted;				parseScope.startingConstructionStep = subobject.startingConstructionStep;				parseScope.mainColourCode = subobject.material.userData.code;				parseScope.mainEdgeColourCode = subobject.material.userData.edgeMaterial.userData.code;				parseScope.fileName = subobject.fileName;			} // Parse the object			this.objectParse( text, parseScope );			const subobjects = parseScope.subobjects;			const promises = [];			for ( let i = 0, l = subobjects.length; i < l; i ++ ) {				promises.push( loadSubobject( parseScope.subobjects[ i ] ) );			} // Kick off of the downloads in parallel but process all the subobjects			// in order so all the assembly instructions are correct			const subobjectScopes = await Promise.all( promises );			for ( let i = 0, l = subobjectScopes.length; i < l; i ++ ) {				this.finalizeObject( subobjectScopes[ i ] );			} // If it is root object then finalize this object and compute construction steps			if ( ! parentParseScope.isFromParse ) {				this.finalizeObject( parseScope );				this.computeConstructionSteps( parseScope.groupObject );			}			return parseScope;			function loadSubobject( subobject ) {				return scope.cache.loadData( subobject.fileName ).then( function ( text ) {					return scope.processObject( text, subobject, url, parseScope );				} ).catch( function () {					console.warn( 'LDrawLoader: Subobject "' + subobject.fileName + '" could not be found.' );				} );			}		}	}	THREE.LDrawLoader = LDrawLoader;} )();
 |