2
0
Эх сурвалжийг харах

LDrawLoader: Cache geometry versions of parts, reduce geometry redundancy, improve parse time (#23232)

* Add functions to LDrawParsedCache

* Get a monolithic model working with the new parts builder cache

* cleanup

* More progress

* Switch to "ensureDataLoaded" function

* improve steps

* apply materials at every group level

* add comment

* Fix steps

* remove material context for part parse, bookkeep unscoped part materials as ids in final part geometry

* material improvements

* getdata function cleanup

* Remove unused code

* more unused code

* improve when to use groups

* comments

* fix hard edge condition

* handle failure case

* fix color condition

* remove unneeded "local materials"

* cleanup

* remove unused import

* comment

* cleanup

* oops

* LDrawPartsBuilderCache -> LDrawPartsGeometryCache

* move group update

* always add built in materials

* add lost changes from other PR, fix getMaterial

* Fix LDrawUtils merge object

* Add missing clone

* Fix always true condition, throw error if no material is available

* Fix support for direct color materials

* Move "applyMaterialsToMesh" function to the loader

* more simplification

* Log error in ldraw example

* Initialize prevmaterial to null instead of undefined
Garrett Johnson 3 жил өмнө
parent
commit
ca021a215b

+ 530 - 442
examples/jsm/loaders/LDrawLoader.js

@@ -7,7 +7,6 @@ import {
 	LineBasicMaterial,
 	LineBasicMaterial,
 	LineSegments,
 	LineSegments,
 	Loader,
 	Loader,
-	Matrix3,
 	Matrix4,
 	Matrix4,
 	Mesh,
 	Mesh,
 	MeshStandardMaterial,
 	MeshStandardMaterial,
@@ -37,6 +36,9 @@ const FILE_LOCATION_TRY_RELATIVE = 4;
 const FILE_LOCATION_TRY_ABSOLUTE = 5;
 const FILE_LOCATION_TRY_ABSOLUTE = 5;
 const FILE_LOCATION_NOT_FOUND = 6;
 const FILE_LOCATION_NOT_FOUND = 6;
 
 
+const MAIN_COLOUR_CODE = '16';
+const MAIN_EDGE_COLOUR_CODE = '24';
+
 const _tempVec0 = new Vector3();
 const _tempVec0 = new Vector3();
 const _tempVec1 = new Vector3();
 const _tempVec1 = new Vector3();
 
 
@@ -169,6 +171,17 @@ class LDrawConditionalLineMaterial extends ShaderMaterial {
 
 
 }
 }
 
 
+class ConditionalLineSegments extends LineSegments {
+
+	constructor( geometry, material ) {
+
+		super( geometry, material );
+		this.isConditionalLine = true;
+
+	}
+
+}
+
 function generateFaceNormals( faces ) {
 function generateFaceNormals( faces ) {
 
 
 	for ( let i = 0, l = faces.length; i < l; i ++ ) {
 	for ( let i = 0, l = faces.length; i < l; i ++ ) {
@@ -523,13 +536,7 @@ function smoothNormals( faces, lineSegments, checkSubSegments = false ) {
 
 
 function isPartType( type ) {
 function isPartType( type ) {
 
 
-	return type === 'Part';
-
-}
-
-function isModelType( type ) {
-
-	return type === 'Model' || type === 'Unofficial_Model';
+	return type === 'Part' || type === 'Unofficial_Part';
 
 
 }
 }
 
 
@@ -628,12 +635,13 @@ class LineParser {
 
 
 }
 }
 
 
+// Fetches and parses an intermediate representation of LDraw parts files.
 class LDrawParsedCache {
 class LDrawParsedCache {
 
 
 	constructor( loader ) {
 	constructor( loader ) {
 
 
 		this.loader = loader;
 		this.loader = loader;
-		this.cache = {};
+		this._cache = {};
 
 
 	}
 	}
 
 
@@ -684,6 +692,7 @@ class LDrawParsedCache {
 		result.totalFaces = original.totalFaces;
 		result.totalFaces = original.totalFaces;
 		result.startingConstructionStep = original.startingConstructionStep;
 		result.startingConstructionStep = original.startingConstructionStep;
 		result.materials = original.materials;
 		result.materials = original.materials;
+		result.group = null;
 		return result;
 		return result;
 
 
 	}
 	}
@@ -765,7 +774,7 @@ class LDrawParsedCache {
 
 
 	}
 	}
 
 
-	parse( text ) {
+	parse( text, fileName = null ) {
 
 
 		const loader = this.loader;
 		const loader = this.loader;
 
 
@@ -778,7 +787,7 @@ class LDrawParsedCache {
 
 
 		const getLocalMaterial = colorCode => {
 		const getLocalMaterial = colorCode => {
 
 
-			return colorCode in materials ? materials[ colorCode ] : null;
+			return materials[ colorCode ] || null;
 
 
 		};
 		};
 
 
@@ -1215,36 +1224,380 @@ class LDrawParsedCache {
 			subobjects,
 			subobjects,
 			totalFaces,
 			totalFaces,
 			startingConstructionStep,
 			startingConstructionStep,
-			materials
+			materials,
+			fileName,
+			group: null
 		};
 		};
 
 
 	}
 	}
 
 
-	loadData( fileName ) {
+	// returns an (optionally cloned) instance of the data
+	getData( fileName, clone = true ) {
 
 
 		const key = fileName.toLowerCase();
 		const key = fileName.toLowerCase();
-		if ( ! ( key in this.cache ) ) {
+		const result = this._cache[ key ];
+		if ( result === null || result instanceof Promise ) {
 
 
-			this.cache[ key ] = this.fetchData( fileName ).then( text => {
+			return null;
 
 
-				return this.parse( text );
+		}
 
 
-			} );
+		if ( clone ) {
+
+			return this.cloneResult( result );
+
+		} else {
+
+			return result;
 
 
 		}
 		}
 
 
-		return this.cache[ key ].then( result => {
+	}
 
 
-			return this.cloneResult( result );
+	// kicks off a fetch and parse of the requested data if it hasn't already been loaded. Returns when
+	// the data is ready to use and can be retrieved synchronously with "getData".
+	async ensureDataLoaded( fileName ) {
 
 
-		} );
+		const key = fileName.toLowerCase();
+		if ( ! ( key in this._cache ) ) {
+
+			// replace the promise with a copy of the parsed data for immediate processing
+			this._cache[ key ] = this.fetchData( fileName ).then( text => {
+
+				const info = this.parse( text, fileName );
+				this._cache[ key ] = info;
+				return info;
+
+			} );
+
+		}
+
+		await this._cache[ key ];
 
 
 	}
 	}
 
 
+	// sets the data in the cache from parsed data
 	setData( fileName, text ) {
 	setData( fileName, text ) {
 
 
 		const key = fileName.toLowerCase();
 		const key = fileName.toLowerCase();
-		this.cache[ key ] = Promise.resolve( this.parse( text ) );
+		this._cache[ key ] = this.parse( text, fileName );
+
+	}
+
+}
+
+// returns the material for an associated color code. If the color code is 16 for a face or 24 for
+// an edge then the passthroughColorCode is used.
+function getMaterialFromCode( colorCode, parentColorCode, materialHierarchy, forEdge ) {
+
+	const isPassthrough = ! forEdge && colorCode === MAIN_COLOUR_CODE || forEdge && colorCode === MAIN_EDGE_COLOUR_CODE;
+	if ( isPassthrough ) {
+
+		colorCode = parentColorCode;
+
+	}
+
+	return materialHierarchy[ colorCode ] || null;
+
+}
+
+// Class used to parse and build LDraw parts as three.js objects and cache them if they're a "Part" type.
+class LDrawPartsGeometryCache {
+
+	constructor( loader ) {
+
+		this.loader = loader;
+		this.parseCache = new LDrawParsedCache( loader );
+		this._cache = {};
+
+	}
+
+	// Convert the given file information into a mesh by processing subobjects.
+	async processIntoMesh( info ) {
+
+		const loader = this.loader;
+		const parseCache = this.parseCache;
+		const faceMaterials = new Set();
+
+		// Processes the part subobject information to load child parts and merge geometry onto part
+		// piece object.
+		const processInfoSubobjects = async ( info, subobject = null ) => {
+
+			const subobjects = info.subobjects;
+			const promises = [];
+
+			// Trigger load of all subobjects. If a subobject isn't a primitive then load it as a separate
+			// group which lets instruction steps apply correctly.
+			for ( let i = 0, l = subobjects.length; i < l; i ++ ) {
+
+				const subobject = subobjects[ i ];
+				const promise = parseCache.ensureDataLoaded( subobject.fileName ).then( () => {
+
+					const subobjectInfo = parseCache.getData( subobject.fileName, false );
+					if ( ! isPrimitiveType( subobjectInfo.type ) ) {
+
+						return this.loadModel( subobject.fileName ).catch( error => {
+
+							console.warn( error );
+							return null;
+
+						} );
+
+					}
+
+					return processInfoSubobjects( parseCache.getData( subobject.fileName ), subobject );
+
+				} );
+
+				promises.push( promise );
+
+			}
+
+			const group = new Group();
+			group.userData.category = info.category;
+			group.userData.keywords = info.keywords;
+			info.group = group;
+
+			const subobjectInfos = await Promise.all( promises );
+			for ( let i = 0, l = subobjectInfos.length; i < l; i ++ ) {
+
+				const subobject = info.subobjects[ i ];
+				const subobjectInfo = subobjectInfos[ i ];
+
+				if ( subobjectInfo === null ) {
+
+					// the subobject failed to load
+					continue;
+
+				}
+
+				// if the subobject was loaded as a separate group then apply the parent scopes materials
+				if ( subobjectInfo.isGroup ) {
+
+					const subobjectGroup = subobjectInfo;
+					subobject.matrix.decompose( subobjectGroup.position, subobjectGroup.quaternion, subobjectGroup.scale );
+					subobjectGroup.userData.startingConstructionStep = subobject.startingConstructionStep;
+					subobjectGroup.name = subobject.fileName;
+
+					loader.applyMaterialsToMesh( subobjectGroup, subobject.colorCode, info.materials );
+
+					group.add( subobjectGroup );
+					continue;
+
+				}
+
+				// add the subobject group if it has children in case it has both children and primitives
+				if ( subobjectInfo.group.children.length ) {
+
+					group.add( subobjectInfo.group );
+
+				}
+
+				// transform the primitives into the local space of the parent piece and append them to
+				// to the parent primitives list.
+				const parentLineSegments = info.lineSegments;
+				const parentConditionalSegments = info.conditionalSegments;
+				const parentFaces = info.faces;
+
+				const lineSegments = subobjectInfo.lineSegments;
+				const conditionalSegments = subobjectInfo.conditionalSegments;
+
+				const faces = subobjectInfo.faces;
+				const matrix = subobject.matrix;
+				const inverted = subobject.inverted;
+				const matrixScaleInverted = matrix.determinant() < 0;
+				const colorCode = subobject.colorCode;
+
+				const lineColorCode = colorCode === MAIN_COLOUR_CODE ? MAIN_EDGE_COLOUR_CODE : colorCode;
+				for ( let i = 0, l = lineSegments.length; i < l; i ++ ) {
+
+					const ls = lineSegments[ i ];
+					const vertices = ls.vertices;
+					vertices[ 0 ].applyMatrix4( matrix );
+					vertices[ 1 ].applyMatrix4( matrix );
+					ls.colorCode = ls.colorCode === MAIN_EDGE_COLOUR_CODE ? lineColorCode : ls.colorCode;
+					ls.material = ls.material || getMaterialFromCode( ls.colorCode, ls.colorCode, info.materials, true );
+
+					parentLineSegments.push( ls );
+
+				}
+
+				for ( let i = 0, l = conditionalSegments.length; i < l; i ++ ) {
+
+					const os = conditionalSegments[ i ];
+					const vertices = os.vertices;
+					const controlPoints = os.controlPoints;
+					vertices[ 0 ].applyMatrix4( matrix );
+					vertices[ 1 ].applyMatrix4( matrix );
+					controlPoints[ 0 ].applyMatrix4( matrix );
+					controlPoints[ 1 ].applyMatrix4( matrix );
+					os.colorCode = os.colorCode === MAIN_EDGE_COLOUR_CODE ? lineColorCode : os.colorCode;
+					os.material = os.material || getMaterialFromCode( os.colorCode, os.colorCode, info.materials, true );
+
+					parentConditionalSegments.push( os );
+
+				}
+
+				for ( let i = 0, l = faces.length; i < l; i ++ ) {
+
+					const tri = faces[ i ];
+					const vertices = tri.vertices;
+					for ( let i = 0, l = vertices.length; i < l; i ++ ) {
+
+						vertices[ i ].applyMatrix4( matrix );
+
+					}
+
+					tri.colorCode = tri.colorCode === MAIN_COLOUR_CODE ? colorCode : tri.colorCode;
+					tri.material = tri.material || getMaterialFromCode( tri.colorCode, colorCode, info.materials, false );
+					faceMaterials.add( tri.colorCode );
+
+					// If the scale of the object is negated then the triangle winding order
+					// needs to be flipped.
+					if ( matrixScaleInverted !== inverted ) {
+
+						vertices.reverse();
+
+					}
+
+					parentFaces.push( tri );
+
+				}
+
+				info.totalFaces += subobjectInfo.totalFaces;
+
+			}
+
+			// Apply the parent subobjects pass through material code to this object. This is done several times due
+			// to material scoping.
+			if ( subobject ) {
+
+				loader.applyMaterialsToMesh( group, subobject.colorCode, info.materials );
+
+			}
+
+			return info;
+
+		};
+
+		// Track material use to see if we need to use the normal smooth slow path for hard edges.
+		for ( let i = 0, l = info.faces; i < l; i ++ ) {
+
+			faceMaterials.add( info.faces[ i ].colorCode );
+
+		}
+
+		await processInfoSubobjects( info );
+
+		if ( loader.smoothNormals ) {
+
+			const checkSubSegments = faceMaterials.size > 1;
+			generateFaceNormals( info.faces );
+			smoothNormals( info.faces, info.lineSegments, checkSubSegments );
+
+		}
+
+		// Add the primitive objects and metadata.
+		const group = info.group;
+		if ( info.faces.length > 0 ) {
+
+			group.add( createObject( info.faces, 3, false, info.totalFaces ) );
+
+		}
+
+		if ( info.lineSegments.length > 0 ) {
+
+			group.add( createObject( info.lineSegments, 2 ) );
+
+		}
+
+		if ( info.conditionalSegments.length > 0 ) {
+
+			group.add( createObject( info.conditionalSegments, 2, true ) );
+
+		}
+
+		return group;
+
+	}
+
+	hasCachedModel( fileName ) {
+
+		return fileName !== null && fileName.toLowerCase() in this._cache;
+
+	}
+
+	async getCachedModel( fileName ) {
+
+		if ( fileName !== null && this.hasCachedModel( fileName ) ) {
+
+			const key = fileName.toLowerCase();
+			const group = await this._cache[ key ];
+			return group.clone();
+
+		} else {
+
+			return null;
+
+		}
+
+	}
+
+	// Loads and parses the model with the given file name. Returns a cached copy if available.
+	async loadModel( fileName ) {
+
+		const parseCache = this.parseCache;
+		const key = fileName.toLowerCase();
+		if ( this.hasCachedModel( fileName ) ) {
+
+			// Return cached model if available.
+			return this.getCachedModel( fileName );
+
+		} else {
+
+			// Otherwise parse a new model.
+			// Ensure the file data is loaded and pre parsed.
+			await parseCache.ensureDataLoaded( fileName );
+
+			const info = parseCache.getData( fileName );
+			const promise = this.processIntoMesh( info );
+
+			// Now that the file has loaded it's possible that another part parse has been waiting in parallel
+			// so check the cache again to see if it's been added since the last async operation so we don't
+			// do unnecessary work.
+			if ( this.hasCachedModel( fileName ) ) {
+
+				return this.getCachedModel( fileName );
+
+			}
+
+			// Cache object if it's a part so it can be reused later.
+			if ( isPartType( info.type ) ) {
+
+				this._cache[ key ] = promise;
+
+			}
+
+			// return a copy
+			const group = await promise;
+			return group.clone();
+
+		}
+
+	}
+
+	// parses the given model text into a renderable object. Returns cached copy if available.
+	async parseModel( text ) {
+
+		const parseCache = this.parseCache;
+		const info = parseCache.parse( text );
+		if ( isPartType( info.type ) && this.hasCachedModel( info.fileName ) ) {
+
+			return this.getCachedModel( info.fileName );
+
+		}
+
+		return this.processIntoMesh( info );
 
 
 	}
 	}
 
 
@@ -1367,7 +1720,7 @@ function createObject( elements, elementSize, isConditionalSegments = false, tot
 
 
 		}
 		}
 
 
-		if ( prevMaterial !== elem.material ) {
+		if ( prevMaterial !== elem.colorCode ) {
 
 
 			if ( prevMaterial !== null ) {
 			if ( prevMaterial !== null ) {
 
 
@@ -1376,25 +1729,43 @@ function createObject( elements, elementSize, isConditionalSegments = false, tot
 			}
 			}
 
 
 			const material = elem.material;
 			const material = elem.material;
-			if ( elementSize === 3 ) {
+			if ( material !== null ) {
 
 
-				materials.push( material );
+				if ( elementSize === 3 ) {
 
 
-			} else if ( elementSize === 2 ) {
+					materials.push( material );
 
 
-				if ( isConditionalSegments ) {
+				} else if ( elementSize === 2 ) {
 
 
-					materials.push( material.userData.edgeMaterial.userData.conditionalEdgeMaterial );
+					if ( material !== null ) {
 
 
-				} else {
+						if ( isConditionalSegments ) {
+
+							materials.push( material.userData.edgeMaterial.userData.conditionalEdgeMaterial );
 
 
-					materials.push( material.userData.edgeMaterial );
+						} else {
+
+							materials.push( material.userData.edgeMaterial );
+
+						}
+
+					} else {
+
+						materials.push( null );
+
+					}
 
 
 				}
 				}
 
 
+			} else {
+
+				// If a material has not been made available yet then keep the color code string in the material array
+				// to save the spot for the material once a parent scopes materials are being applied to the object.
+				materials.push( elem.colorCode );
+
 			}
 			}
 
 
-			prevMaterial = elem.material;
+			prevMaterial = elem.colorCode;
 			index0 = offset / 3;
 			index0 = offset / 3;
 			numGroupVerts = vertices.length;
 			numGroupVerts = vertices.length;
 
 
@@ -1426,7 +1797,15 @@ function createObject( elements, elementSize, isConditionalSegments = false, tot
 
 
 	if ( elementSize === 2 ) {
 	if ( elementSize === 2 ) {
 
 
-		object3d = new LineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );
+		if ( isConditionalSegments ) {
+
+			object3d = new ConditionalLineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );
+
+		} else {
+
+			object3d = new LineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );
+
+		}
 
 
 	} else if ( elementSize === 3 ) {
 	} else if ( elementSize === 3 ) {
 
 
@@ -1486,9 +1865,6 @@ function createObject( elements, elementSize, isConditionalSegments = false, tot
 
 
 //
 //
 
 
-const MAIN_COLOUR_CODE = '16';
-const MAIN_EDGE_COLOUR_CODE = '24';
-
 class LDrawLoader extends Loader {
 class LDrawLoader extends Loader {
 
 
 	constructor( manager ) {
 	constructor( manager ) {
@@ -1497,21 +1873,16 @@ class LDrawLoader extends Loader {
 
 
 		// Array of THREE.Material
 		// Array of THREE.Material
 		this.materials = [];
 		this.materials = [];
+		this.materialLibrary = {};
 
 
-		// 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 also allows to handle the embedded text files ("0 FILE" lines)
-		this.parseCache = new LDrawParsedCache( this );
+		this.partsCache = new LDrawPartsGeometryCache( 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 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 = {};
 		this.fileMap = {};
 
 
-		this.rootParseScope = this.newParseScopeLevel();
-
-		// Add default main triangle and line edge materials (used in pieces that can be colored with a main color)
-		this.setMaterials( [
-			this.parseColorMetaDirective( new LineParser( 'Main_Colour CODE 16 VALUE #FF8080 EDGE #333333' ) ),
-			this.parseColorMetaDirective( new LineParser( 'Edge_Colour CODE 24 VALUE #A0A0A0 EDGE #333333' ) )
-		] );
+		// Initializes the materials library with default materials
+		this.setMaterials( [] );
 
 
 		// If this flag is set to true the vertex normals will be smoothed.
 		// If this flag is set to true the vertex normals will be smoothed.
 		this.smoothNormals = true;
 		this.smoothNormals = true;
@@ -1564,26 +1935,29 @@ class LDrawLoader extends Loader {
 		fileLoader.setWithCredentials( this.withCredentials );
 		fileLoader.setWithCredentials( this.withCredentials );
 		fileLoader.load( url, text => {
 		fileLoader.load( url, text => {
 
 
-			const parsedInfo = this.parseCache.parse( text );
-			this.processObject( parsedInfo, null, url, this.rootParseScope )
-				.then( function ( result ) {
+			this.partsCache
+				.parseModel( text, this.materialLibrary )
+				.then( group => {
 
 
-					onLoad( result.groupObject );
+					this.applyMaterialsToMesh( group, MAIN_COLOUR_CODE, this.materialLibrary, true );
+					this.computeConstructionSteps( group );
+					onLoad( group );
 
 
-				} );
+				} )
+				.catch( onError );
 
 
 		}, onProgress, onError );
 		}, onProgress, onError );
 
 
 	}
 	}
 
 
-	parse( text, path, onLoad ) {
+	parse( text, onLoad ) {
 
 
-		// Async parse. This function calls onParse with the parsed THREE.Object3D as parameter
-		const parsedInfo = this.parseCache.parse( text );
-		this.processObject( parsedInfo, null, path, this.rootParseScope )
-			.then( function ( result ) {
+		this.partsCache
+			.parseModel( text, this.materialLibrary )
+			.then( group => {
 
 
-				onLoad( result.groupObject );
+				this.computeConstructionSteps( group );
+				onLoad( group );
 
 
 			} );
 			} );
 
 
@@ -1591,11 +1965,17 @@ class LDrawLoader extends Loader {
 
 
 	setMaterials( materials ) {
 	setMaterials( materials ) {
 
 
-		// Clears parse scopes stack, adds new scope with material library
-		this.rootParseScope = this.newParseScopeLevel( materials );
-		this.rootParseScope.isFromParse = false;
+		this.materialLibrary = {};
+		this.materials = [];
+		for ( let i = 0, l = materials.length; i < l; i ++ ) {
+
+			this.addMaterial( materials[ i ] );
 
 
-		this.materials = materials;
+		}
+
+		// Add default main triangle and line edge materials (used in pieces that can be colored with a main color)
+		this.addMaterial( this.parseColorMetaDirective( new LineParser( 'Main_Colour CODE 16 VALUE #FF8080 EDGE #333333' ) ) );
+		this.addMaterial( this.parseColorMetaDirective( new LineParser( 'Edge_Colour CODE 24 VALUE #A0A0A0 EDGE #333333' ) ) );
 
 
 		return this;
 		return this;
 
 
@@ -1609,121 +1989,144 @@ class LDrawLoader extends Loader {
 
 
 	}
 	}
 
 
-	newParseScopeLevel( materials = null, parentScope = null ) {
+	addMaterial( material ) {
 
 
-		// Adds a new scope level, assign materials to it and returns it
+		// Adds a material to the material library which is on top of the parse scopes stack. And also to the materials array
 
 
-		const matLib = {};
+		const matLib = this.materialLibrary;
+		if ( ! matLib[ material.userData.code ] ) {
 
 
-		if ( materials ) {
+			this.materials.push( material );
+			matLib[ material.userData.code ] = material;
 
 
-			for ( let i = 0, n = materials.length; i < n; i ++ ) {
+		}
 
 
-				const material = materials[ i ];
-				matLib[ material.userData.code ] = material;
+		return this;
 
 
-			}
+	}
 
 
-		}
+	getMaterial( colorCode ) {
 
 
-		const newParseScope = {
-
-			parentScope: parentScope,
-			lib: matLib,
-			url: null,
-
-			// Subobjects
-			subobjects: null,
-			inverted: false,
-			category: null,
-			keywords: null,
-
-			// Current subobject
-			currentFileName: null,
-			mainColorCode: parentScope ? parentScope.mainColorCode : MAIN_COLOUR_CODE,
-			mainEdgeColorCode: parentScope ? parentScope.mainEdgeColorCode : MAIN_EDGE_COLOUR_CODE,
-			matrix: new Matrix4(),
-			type: 'Model',
-			groupObject: null,
-
-			// If false, it is a root material scope previous to parse
-			isFromParse: true,
-
-			faces: [],
-			lineSegments: [],
-			conditionalSegments: [],
-			totalFaces: 0,
-			faceMaterials: new Set(),
-
-			// If true, this object is the start of a construction step
-			startingConstructionStep: false
-		};
+		if ( colorCode.startsWith( '0x2' ) ) {
+
+			// Special 'direct' material value (RGB color)
+			const color = colorCode.substring( 3 );
 
 
-		return newParseScope;
+			return this.parseColorMetaDirective( new LineParser( 'Direct_Color_' + color + ' CODE -1 VALUE #' + color + ' EDGE #' + color + '' ) );
+
+		}
+
+		return this.materialLibrary[ colorCode ] || null;
 
 
 	}
 	}
 
 
-	addMaterial( material, parseScope ) {
+	// Applies the appropriate materials to a prebuilt hierarchy of geometry. Assumes that color codes are present
+	// in the material array if they need to be filled in.
+	applyMaterialsToMesh( group, parentColorCode, materialHierarchy, finalMaterialPass = false ) {
 
 
-		// Adds a material to the material library which is on top of the parse scopes stack. And also to the materials array
+		// find any missing materials as indicated by a color code string and replace it with a material from the current material lib
+		const loader = this;
+		const parentIsPassthrough = parentColorCode === MAIN_COLOUR_CODE;
+		group.traverse( c => {
 
 
-		const matLib = parseScope.lib;
+			if ( c.isMesh || c.isLineSegments ) {
 
 
-		if ( ! matLib[ material.userData.code ] ) {
+				if ( Array.isArray( c.material ) ) {
 
 
-			this.materials.push( material );
+					for ( let i = 0, l = c.material.length; i < l; i ++ ) {
 
 
-		}
+						if ( ! c.material[ i ].isMaterial ) {
 
 
-		matLib[ material.userData.code ] = material;
+							c.material[ i ] = getMaterial( c, c.material[ i ] );
 
 
-		return this;
+						}
 
 
-	}
+					}
 
 
-	getMaterial( colorCode, parseScope = this.rootParseScope ) {
+				} else if ( ! c.material.isMaterial ) {
 
 
-		// Given a color code search its material in the parse scopes stack
+					c.material = getMaterial( c, c.material );
 
 
-		if ( colorCode.startsWith( '0x2' ) ) {
+				}
 
 
-			// Special 'direct' material value (RGB color)
+			}
 
 
-			const color = colorCode.substring( 3 );
+		} );
 
 
-			return this.parseColorMetaDirective( new LineParser( 'Direct_Color_' + color + ' CODE -1 VALUE #' + color + ' EDGE #' + color + '' ) );
 
 
-		}
+		// Returns the appropriate material for the object (line or face) given color code. If the code is "pass through"
+		// (24 for lines, 16 for edges) then the pass through color code is used. If that is also pass through then it's
+		// simply returned for the subsequent material application.
+		function getMaterial( c, colorCode ) {
+
+			// if our parent is a passthrough color code and we don't have the current material color available then
+			// return early.
+			if ( parentIsPassthrough && ! ( colorCode in materialHierarchy ) && ! finalMaterialPass ) {
+
+				return colorCode;
+
+			}
+
+			const forEdge = c.isLineSegments || c.isConditionalLine;
+			const isPassthrough = ! forEdge && colorCode === MAIN_COLOUR_CODE || forEdge && colorCode === MAIN_EDGE_COLOUR_CODE;
+			if ( isPassthrough ) {
+
+				colorCode = parentColorCode;
+
+			}
 
 
-		while ( parseScope ) {
+			let material = null;
+			if ( colorCode in materialHierarchy ) {
 
 
-			const material = parseScope.lib[ colorCode ];
+				material = materialHierarchy[ colorCode ];
 
 
-			if ( material ) {
+			} else if ( finalMaterialPass ) {
+
+				// see if we can get the final material from from the "getMaterial" function which will attempt to
+				// parse the "direct" colors
+				material = loader.getMaterial( colorCode );
+				if ( material === null ) {
+
+					// otherwise throw an error if this is final opportunity to set the material
+					throw new Error( `LDrawLoader: Material properties for code ${ colorCode } not available.` );
+
+				}
 
 
-				return material;
 
 
 			} else {
 			} else {
 
 
-				parseScope = parseScope.parentScope;
+				return colorCode;
 
 
 			}
 			}
 
 
-		}
+			if ( c.isLineSegments ) {
+
+				material = material.userData.edgeMaterial;
+
+				if ( c.isConditionalLine ) {
 
 
-		// Material was not found
-		return null;
+					material = material.userData.conditionalEdgeMaterial;
+
+				}
+
+			}
+
+			return material;
+
+		}
 
 
 	}
 	}
 
 
 	getMainMaterial() {
 	getMainMaterial() {
 
 
 		return this.getMaterial( MAIN_COLOUR_CODE );
 		return this.getMaterial( MAIN_COLOUR_CODE );
+
 	}
 	}
 
 
 	getMainEdgeMaterial() {
 	getMainEdgeMaterial() {
 
 
-		return this.getMaterial( MAIN_EDGE_COLOUR_CODE );;
+		return this.getMaterial( MAIN_EDGE_COLOUR_CODE );
+
 	}
 	}
 
 
 	parseColorMetaDirective( lineParser ) {
 	parseColorMetaDirective( lineParser ) {
@@ -1966,115 +2369,9 @@ class LDrawLoader extends Loader {
 
 
 		material.userData.edgeMaterial = edgeMaterial;
 		material.userData.edgeMaterial = edgeMaterial;
 
 
-		return material;
-
-	}
-
-	//
-
-	objectParse( info, parseScope ) {
-
-		// Retrieve data from the parent parse scope
-		const currentParseScope = parseScope;
-		const parentParseScope = currentParseScope.parentScope;
-
-		// Main color codes passed to this subobject (or default codes 16 and 24 if it is the root object)
-		const mainColorCode = currentParseScope.mainColorCode;
-		const mainEdgeColorCode = currentParseScope.mainEdgeColorCode;
-
-		const parseColorCode = ( colorCode, forEdge ) => {
-
-			// Parses next color code and returns a THREE.Material
-
-			if ( ! forEdge && colorCode === MAIN_COLOUR_CODE ) {
-
-				colorCode = mainColorCode;
-
-			}
-
-			if ( forEdge && colorCode === MAIN_EDGE_COLOUR_CODE ) {
-
-				colorCode = mainEdgeColorCode;
-
-			}
-
-			const material = this.getMaterial( colorCode, currentParseScope );
-
-			if ( ! material ) {
-
-				throw new Error( 'LDrawLoader: Unknown color code "' + colorCode + '" is used but it was not defined previously.' );
-
-			}
-
-			return material;
-
-		};
-
-		const faces = info.faces;
-		const lineSegments = info.lineSegments;
-		const conditionalSegments = info.conditionalSegments;
-		const materials = info.materials;
-		if ( currentParseScope.inverted ) {
-
-			faces.reverse();
-
-		}
-
-		for ( const colorCode in materials ) {
-
-			this.addMaterial( materials[ colorCode ], currentParseScope );
-
-		}
-
-		for ( let i = 0, l = faces.length; i < l; i ++ ) {
-
-			const face = faces[ i ];
-			if ( face.material === null ) {
-
-				face.material = parseColorCode( face.colorCode, false );
-
-			}
-
-		}
-
-		for ( let i = 0, l = lineSegments.length; i < l; i ++ ) {
-
-			const ls = lineSegments[ i ];
-			if ( ls.material === null ) {
-
-				ls.material = parseColorCode( ls.colorCode, true );
-
-			}
-
-		}
-
-		for ( let i = 0, l = conditionalSegments.length; i < l; i ++ ) {
-
-			const cs = conditionalSegments[ i ];
-			if ( cs.material === null ) {
+		this.addMaterial( material );
 
 
-				cs.material = parseColorCode( cs.colorCode, true );
-
-			}
-
-		}
-
-		currentParseScope.faces = info.faces;
-		currentParseScope.conditionalSegments = info.conditionalSegments;
-		currentParseScope.lineSegments = info.lineSegments;
-		currentParseScope.category = info.category;
-		currentParseScope.keywords = info.keywords;
-		currentParseScope.subobjects = info.subobjects;
-		currentParseScope.type = info.type;
-		currentParseScope.totalFaces = info.totalFaces;
-
-		const isRoot = ! parentParseScope.isFromParse;
-		if ( isRoot || ! isPrimitiveType( info.type ) ) {
-
-			currentParseScope.groupObject = new Group();
-			currentParseScope.groupObject.userData.startingConstructionStep = currentParseScope.startingConstructionStep;
-
-		}
+		return material;
 
 
 	}
 	}
 
 
@@ -2104,215 +2401,6 @@ class LDrawLoader extends Loader {
 
 
 	}
 	}
 
 
-	finalizeObject( subobjectParseScope ) {
-
-		// fail gracefully if an object could not be loaded
-		if ( subobjectParseScope === null ) {
-
-			return;
-
-		}
-
-		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 ) ||
-			(
-				isPrimitiveType( subobjectParseScope.type ) &&
-				isModelType( subobjectParseScope.parentScope.type )
-			);
-
-		if ( this.smoothNormals && doSmooth ) {
-
-			generateFaceNormals( subobjectParseScope.faces );
-
-			// only check subsetgments if we have multiple materials in a single part because this seems to be the case where it's needed most --
-			// there may be cases where a single edge line crosses over polygon edges that are broken up by multiple materials.
-			const checkSubSegments = subobjectParseScope.faceMaterials.size > 1;
-			smoothNormals( subobjectParseScope.faces, subobjectParseScope.lineSegments, checkSubSegments );
-
-		}
-
-		const isRoot = ! parentParseScope.isFromParse;
-		if ( ! 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 parentLineSegments = parentParseScope.lineSegments;
-			const parentConditionalSegments = parentParseScope.conditionalSegments;
-			const parentFaces = parentParseScope.faces;
-			const parentFaceMaterials = parentParseScope.faceMaterials;
-
-			const lineSegments = subobjectParseScope.lineSegments;
-			const conditionalSegments = subobjectParseScope.conditionalSegments;
-			const faces = subobjectParseScope.faces;
-			const faceMaterials = subobjectParseScope.faceMaterials;
-			const matrix = subobjectParseScope.matrix;
-			const matrixScaleInverted = matrix.determinant() < 0;
-
-			for ( let i = 0, l = lineSegments.length; i < l; i ++ ) {
-
-				const ls = lineSegments[ i ];
-				const vertices = ls.vertices;
-				vertices[ 0 ].applyMatrix4( matrix );
-				vertices[ 1 ].applyMatrix4( matrix );
-
-				parentLineSegments.push( ls );
-
-			}
-
-			for ( let i = 0, l = conditionalSegments.length; i < l; i ++ ) {
-
-				const os = conditionalSegments[ i ];
-				const vertices = os.vertices;
-				const controlPoints = os.controlPoints;
-				vertices[ 0 ].applyMatrix4( matrix );
-				vertices[ 1 ].applyMatrix4( matrix );
-				controlPoints[ 0 ].applyMatrix4( matrix );
-				controlPoints[ 1 ].applyMatrix4( matrix );
-
-				parentConditionalSegments.push( os );
-
-			}
-
-			for ( let i = 0, l = faces.length; i < l; i ++ ) {
-
-				const tri = faces[ i ];
-				const vertices = tri.vertices;
-				for ( let i = 0, l = vertices.length; i < l; i ++ ) {
-
-					vertices[ i ].applyMatrix4( matrix );
-
-				}
-
-				// If the scale of the object is negated then the triangle winding order
-				// needs to be flipped.
-				if ( matrixScaleInverted !== subobjectParseScope.inverted ) {
-
-					vertices.reverse();
-
-				}
-
-				parentFaces.push( tri );
-
-			}
-
-			parentParseScope.totalFaces += subobjectParseScope.totalFaces;
-			faceMaterials.forEach( material => parentFaceMaterials.add( material ) );
-
-		}
-
-	}
-
-	async processObject( parsedInfo, subobject, url, parentScope ) {
-
-		const scope = this;
-
-		const parseScope = this.newParseScopeLevel( null, parentScope );
-		parseScope.url = url;
-
-		// Set current matrix
-		if ( subobject ) {
-
-			parseScope.matrix.copy( subobject.matrix );
-			parseScope.inverted = subobject.inverted;
-			parseScope.startingConstructionStep = subobject.startingConstructionStep;
-			parseScope.fileName = subobject.fileName;
-			if ( subobject.colorCode === MAIN_COLOUR_CODE && parentScope ) {
-
-				parseScope.mainColorCode = parentScope.mainColorCode;
-				parseScope.mainEdgeColorCode = parentScope.mainEdgeColorCode;
-
-			} else if ( subobject.colorCode !== MAIN_COLOUR_CODE ) {
-
-				parseScope.mainColorCode = subobject.colorCode;
-				parseScope.mainEdgeColorCode = subobject.colorCode;
-
-			}
-
-		}
-
-		// Parse the object
-		this.objectParse( parsedInfo, parseScope );
-
-		const subobjects = parseScope.subobjects;
-		const promises = [];
-		for ( let i = 0, l = subobjects.length; i < l; i ++ ) {
-
-			promises.push( loadSubobject( 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 ( ! parentScope.isFromParse ) {
-
-			this.finalizeObject( parseScope );
-			this.computeConstructionSteps( parseScope.groupObject );
-
-		}
-
-		return parseScope;
-
-		function loadSubobject( subobject ) {
-
-			return scope.parseCache.loadData( subobject.fileName ).then( function ( parsedInfo ) {
-
-				return scope.processObject( parsedInfo, subobject, url, parseScope );
-
-			} ).catch( function ( err ) {
-
-				console.warn( err );
-				return null;
-
-			} );
-
-		}
-
-	}
-
 }
 }
 
 
 export { LDrawLoader };
 export { LDrawLoader };

+ 18 - 11
examples/jsm/utils/LDrawUtils.js

@@ -62,8 +62,11 @@ class LDrawUtils {
 					arr: [ geometry ]
 					arr: [ geometry ]
 				};
 				};
 
 
+			} else {
+
+				geoms.arr.push( geometry );
+
 			}
 			}
-			else geoms.arr.push( geometry );
 
 
 		}
 		}
 
 
@@ -111,22 +114,23 @@ class LDrawUtils {
 
 
 				const elemSize = c.isMesh ? 3 : 2;
 				const elemSize = c.isMesh ? 3 : 2;
 
 
+				const geometry = c.geometry.clone();
 				const matrixIsInverted = c.matrixWorld.determinant() < 0;
 				const matrixIsInverted = c.matrixWorld.determinant() < 0;
 				if ( matrixIsInverted ) {
 				if ( matrixIsInverted ) {
 
 
-					permuteAttribute( c.geometry.attributes.position, elemSize );
-					permuteAttribute( c.geometry.attributes.normal, elemSize );
+					permuteAttribute( geometry.attributes.position, elemSize );
+					permuteAttribute( geometry.attributes.normal, elemSize );
 
 
 				}
 				}
 
 
-				c.geometry.applyMatrix4( c.matrixWorld );
+				geometry.applyMatrix4( c.matrixWorld );
 
 
 				if ( c.isConditionalLine ) {
 				if ( c.isConditionalLine ) {
 
 
-					c.geometry.attributes.control0.applyMatrix4( c.matrixWorld );
-					c.geometry.attributes.control1.applyMatrix4( c.matrixWorld );
+					geometry.attributes.control0.applyMatrix4( c.matrixWorld );
+					geometry.attributes.control1.applyMatrix4( c.matrixWorld );
 					normalMatrix.getNormalMatrix( c.matrixWorld );
 					normalMatrix.getNormalMatrix( c.matrixWorld );
-					c.geometry.attributes.direction.applyNormalMatrix( normalMatrix );
+					geometry.attributes.direction.applyNormalMatrix( normalMatrix );
 
 
 				}
 				}
 
 
@@ -134,17 +138,20 @@ class LDrawUtils {
 
 
 				if ( Array.isArray( c.material ) ) {
 				if ( Array.isArray( c.material ) ) {
 
 
-					for ( const groupIndex in c.geometry.groups ) {
+					for ( const groupIndex in geometry.groups ) {
 
 
-						const group = c.geometry.groups[ groupIndex ];
+						const group = geometry.groups[ groupIndex ];
 						const mat = c.material[ group.materialIndex ];
 						const mat = c.material[ group.materialIndex ];
-						const newGeometry = extractGroup( c.geometry, group, elemSize, c.isConditionalLine );
+						const newGeometry = extractGroup( geometry, group, elemSize, c.isConditionalLine );
 						addGeometry( mat, newGeometry, geometries );
 						addGeometry( mat, newGeometry, geometries );
 
 
 					}
 					}
 
 
+				} else {
+
+					addGeometry( c.material, geometry, geometries );
+
 				}
 				}
-				else addGeometry( c.material, c.geometry, geometries );
 
 
 			}
 			}
 
 

+ 2 - 1
examples/webgl_loader_ldraw.html

@@ -334,11 +334,12 @@
 
 
 			}
 			}
 
 
-			function onError() {
+			function onError( error ) {
 
 
 				const message = 'Error loading model';
 				const message = 'Error loading model';
 				progressBarDiv.innerText = message;
 				progressBarDiv.innerText = message;
 				console.log( message );
 				console.log( message );
+				console.error( error );
 
 
 			}
 			}