|
@@ -17,6 +17,8 @@
|
|
|
const FILE_LOCATION_TRY_RELATIVE = 4;
|
|
|
const FILE_LOCATION_TRY_ABSOLUTE = 5;
|
|
|
const FILE_LOCATION_NOT_FOUND = 6;
|
|
|
+ const MAIN_COLOUR_CODE = '16';
|
|
|
+ const MAIN_EDGE_COLOUR_CODE = '24';
|
|
|
|
|
|
const _tempVec0 = new THREE.Vector3();
|
|
|
|
|
@@ -142,6 +144,17 @@
|
|
|
|
|
|
}
|
|
|
|
|
|
+ class ConditionalLineSegments extends THREE.LineSegments {
|
|
|
+
|
|
|
+ constructor( geometry, material ) {
|
|
|
+
|
|
|
+ super( geometry, material );
|
|
|
+ this.isConditionalLine = true;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
function generateFaceNormals( faces ) {
|
|
|
|
|
|
for ( let i = 0, l = faces.length; i < l; i ++ ) {
|
|
@@ -497,13 +510,7 @@
|
|
|
|
|
|
function isPartType( type ) {
|
|
|
|
|
|
- return type === 'Part';
|
|
|
-
|
|
|
- }
|
|
|
-
|
|
|
- function isModelType( type ) {
|
|
|
-
|
|
|
- return type === 'Model' || type === 'Unofficial_Model';
|
|
|
+ return type === 'Part' || type === 'Unofficial_Part';
|
|
|
|
|
|
}
|
|
|
|
|
@@ -597,14 +604,15 @@
|
|
|
|
|
|
}
|
|
|
|
|
|
- }
|
|
|
+ } // Fetches and parses an intermediate representation of LDraw parts files.
|
|
|
+
|
|
|
|
|
|
class LDrawParsedCache {
|
|
|
|
|
|
constructor( loader ) {
|
|
|
|
|
|
this.loader = loader;
|
|
|
- this.cache = {};
|
|
|
+ this._cache = {};
|
|
|
|
|
|
}
|
|
|
|
|
@@ -651,6 +659,7 @@
|
|
|
result.totalFaces = original.totalFaces;
|
|
|
result.startingConstructionStep = original.startingConstructionStep;
|
|
|
result.materials = original.materials;
|
|
|
+ result.group = null;
|
|
|
return result;
|
|
|
|
|
|
}
|
|
@@ -733,7 +742,7 @@
|
|
|
|
|
|
}
|
|
|
|
|
|
- parse( text ) {
|
|
|
+ parse( text, fileName = null ) {
|
|
|
|
|
|
const loader = this.loader; // final results
|
|
|
|
|
@@ -745,7 +754,7 @@
|
|
|
|
|
|
const getLocalMaterial = colorCode => {
|
|
|
|
|
|
- return colorCode in materials ? materials[ colorCode ] : null;
|
|
|
+ return materials[ colorCode ] || null;
|
|
|
|
|
|
};
|
|
|
|
|
@@ -1135,37 +1144,376 @@
|
|
|
subobjects,
|
|
|
totalFaces,
|
|
|
startingConstructionStep,
|
|
|
- materials
|
|
|
+ materials,
|
|
|
+ fileName,
|
|
|
+ group: null
|
|
|
};
|
|
|
|
|
|
- }
|
|
|
+ } // returns an (optionally cloned) instance of the data
|
|
|
|
|
|
- loadData( fileName ) {
|
|
|
+
|
|
|
+ getData( fileName, clone = true ) {
|
|
|
|
|
|
const key = fileName.toLowerCase();
|
|
|
+ const result = this._cache[ key ];
|
|
|
+
|
|
|
+ if ( result === null || result instanceof Promise ) {
|
|
|
+
|
|
|
+ return null;
|
|
|
+
|
|
|
+ }
|
|
|
|
|
|
- if ( ! ( key in this.cache ) ) {
|
|
|
+ if ( clone ) {
|
|
|
+
|
|
|
+ return this.cloneResult( result );
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ return result;
|
|
|
+
|
|
|
+ }
|
|
|
|
|
|
- this.cache[ key ] = this.fetchData( fileName ).then( text => {
|
|
|
+ } // 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".
|
|
|
|
|
|
- return this.parse( text );
|
|
|
+
|
|
|
+ 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;
|
|
|
|
|
|
} );
|
|
|
|
|
|
}
|
|
|
|
|
|
- return this.cache[ key ].then( result => {
|
|
|
+ await this._cache[ key ];
|
|
|
|
|
|
- return this.cloneResult( result );
|
|
|
+ } // sets the data in the cache from parsed data
|
|
|
|
|
|
- } );
|
|
|
+
|
|
|
+ setData( fileName, text ) {
|
|
|
+
|
|
|
+ const key = fileName.toLowerCase();
|
|
|
+ this._cache[ key ] = this.parse( text, fileName );
|
|
|
|
|
|
}
|
|
|
|
|
|
- setData( fileName, text ) {
|
|
|
+ } // 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 THREE.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();
|
|
|
- this.cache[ key ] = Promise.resolve( this.parse( text ) );
|
|
|
+
|
|
|
+ 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 );
|
|
|
|
|
|
}
|
|
|
|
|
@@ -1290,7 +1638,7 @@
|
|
|
|
|
|
}
|
|
|
|
|
|
- if ( prevMaterial !== elem.material ) {
|
|
|
+ if ( prevMaterial !== elem.colorCode ) {
|
|
|
|
|
|
if ( prevMaterial !== null ) {
|
|
|
|
|
@@ -1300,25 +1648,43 @@
|
|
|
|
|
|
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 );
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ materials.push( material.userData.edgeMaterial );
|
|
|
+
|
|
|
+ }
|
|
|
|
|
|
- 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;
|
|
|
numGroupVerts = vertices.length;
|
|
|
|
|
@@ -1350,7 +1716,15 @@
|
|
|
|
|
|
if ( elementSize === 2 ) {
|
|
|
|
|
|
- object3d = new THREE.LineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );
|
|
|
+ if ( isConditionalSegments ) {
|
|
|
+
|
|
|
+ object3d = new ConditionalLineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ object3d = new THREE.LineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );
|
|
|
+
|
|
|
+ }
|
|
|
|
|
|
} else if ( elementSize === 3 ) {
|
|
|
|
|
@@ -1407,24 +1781,20 @@
|
|
|
} //
|
|
|
|
|
|
|
|
|
- const MAIN_COLOUR_CODE = '16';
|
|
|
- const MAIN_EDGE_COLOUR_CODE = '24';
|
|
|
-
|
|
|
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.materials = [];
|
|
|
+ this.materialLibrary = {}; // This also allows to handle the embedded text files ("0 FILE" lines)
|
|
|
|
|
|
- this.parseCache = new LDrawParsedCache( 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.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.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.fileMap = {}; // Initializes the materials library with default materials
|
|
|
|
|
|
- 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' ) ) ] ); // If this flag is set to true the vertex normals will be smoothed.
|
|
|
+ this.setMaterials( [] ); // 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.
|
|
|
|
|
@@ -1476,24 +1846,24 @@
|
|
|
fileLoader.setWithCredentials( this.withCredentials );
|
|
|
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 );
|
|
|
|
|
|
}
|
|
|
|
|
|
- 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 );
|
|
|
|
|
|
} );
|
|
|
|
|
@@ -1501,86 +1871,47 @@
|
|
|
|
|
|
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;
|
|
|
-
|
|
|
- }
|
|
|
+ this.materialLibrary = {};
|
|
|
+ this.materials = [];
|
|
|
|
|
|
- newParseScopeLevel( materials = null, parentScope = null ) {
|
|
|
+ for ( let i = 0, l = materials.length; i < l; i ++ ) {
|
|
|
|
|
|
- // Adds a new scope level, assign materials to it and returns it
|
|
|
- const matLib = {};
|
|
|
+ this.addMaterial( materials[ i ] );
|
|
|
|
|
|
- if ( materials ) {
|
|
|
+ } // Add default main triangle and line edge materials (used in pieces that can be colored with a main color)
|
|
|
|
|
|
- for ( let i = 0, n = materials.length; i < n; i ++ ) {
|
|
|
|
|
|
- const material = materials[ i ];
|
|
|
- matLib[ material.userData.code ] = material;
|
|
|
+ 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;
|
|
|
|
|
|
- }
|
|
|
+ }
|
|
|
|
|
|
- }
|
|
|
+ setFileMap( fileMap ) {
|
|
|
|
|
|
- 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 THREE.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
|
|
|
- };
|
|
|
- return newParseScope;
|
|
|
+ this.fileMap = fileMap;
|
|
|
+ return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
- addMaterial( material, parseScope ) {
|
|
|
+ addMaterial( material ) {
|
|
|
|
|
|
// 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;
|
|
|
+ const matLib = this.materialLibrary;
|
|
|
|
|
|
if ( ! matLib[ material.userData.code ] ) {
|
|
|
|
|
|
this.materials.push( material );
|
|
|
+ matLib[ material.userData.code ] = material;
|
|
|
|
|
|
}
|
|
|
|
|
|
- matLib[ material.userData.code ] = material;
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
- getMaterial( colorCode, parseScope = this.rootParseScope ) {
|
|
|
+ getMaterial( colorCode ) {
|
|
|
|
|
|
- // Given a color code search its material in the parse scopes stack
|
|
|
if ( colorCode.startsWith( '0x2' ) ) {
|
|
|
|
|
|
// Special 'direct' material value (RGB color)
|
|
@@ -1589,24 +1920,104 @@
|
|
|
|
|
|
}
|
|
|
|
|
|
- while ( parseScope ) {
|
|
|
+ return this.materialLibrary[ colorCode ] || null;
|
|
|
+
|
|
|
+ } // 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 ) {
|
|
|
+
|
|
|
+ // 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 => {
|
|
|
+
|
|
|
+ if ( c.isMesh || c.isLineSegments ) {
|
|
|
+
|
|
|
+ if ( Array.isArray( c.material ) ) {
|
|
|
|
|
|
- const material = parseScope.lib[ colorCode ];
|
|
|
+ for ( let i = 0, l = c.material.length; i < l; i ++ ) {
|
|
|
|
|
|
- if ( material ) {
|
|
|
+ if ( ! c.material[ i ].isMaterial ) {
|
|
|
|
|
|
- return material;
|
|
|
+ c.material[ i ] = getMaterial( c, c.material[ i ] );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ } else if ( ! c.material.isMaterial ) {
|
|
|
+
|
|
|
+ c.material = getMaterial( c, c.material );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ } ); // 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;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ let material = null;
|
|
|
+
|
|
|
+ if ( colorCode in materialHierarchy ) {
|
|
|
+
|
|
|
+ material = materialHierarchy[ colorCode ];
|
|
|
+
|
|
|
+ } 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.` );
|
|
|
+
|
|
|
+ }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
- parseScope = parseScope.parentScope;
|
|
|
+ return colorCode;
|
|
|
|
|
|
}
|
|
|
|
|
|
- } // Material was not found
|
|
|
+ if ( c.isLineSegments ) {
|
|
|
+
|
|
|
+ material = material.userData.edgeMaterial;
|
|
|
|
|
|
+ if ( c.isConditionalLine ) {
|
|
|
|
|
|
- return null;
|
|
|
+ material = material.userData.conditionalEdgeMaterial;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ return material;
|
|
|
+
|
|
|
+ }
|
|
|
|
|
|
}
|
|
|
|
|
@@ -1867,117 +2278,9 @@
|
|
|
material.userData.code = code;
|
|
|
material.name = name;
|
|
|
material.userData.edgeMaterial = edgeMaterial;
|
|
|
+ this.addMaterial( material );
|
|
|
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 ) {
|
|
|
-
|
|
|
- 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 THREE.Group();
|
|
|
- currentParseScope.groupObject.userData.startingConstructionStep = currentParseScope.startingConstructionStep;
|
|
|
-
|
|
|
- }
|
|
|
-
|
|
|
}
|
|
|
|
|
|
computeConstructionSteps( model ) {
|
|
@@ -2003,206 +2306,6 @@
|
|
|
|
|
|
}
|
|
|
|
|
|
- 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;
|
|
|
-
|
|
|
- } );
|
|
|
-
|
|
|
- }
|
|
|
-
|
|
|
- }
|
|
|
-
|
|
|
}
|
|
|
|
|
|
THREE.LDrawLoader = LDrawLoader;
|