You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1331 lines
29 KiB
1331 lines
29 KiB
/**
|
|
* @author mrdoob / http://mrdoob.com/
|
|
* @author yomboprime / https://github.com/yomboprime/
|
|
*
|
|
*
|
|
*/
|
|
|
|
THREE.LDrawLoader = ( function () {
|
|
|
|
function LineParser( line, lineNumber ) {
|
|
|
|
this.line = line;
|
|
this.lineLength = line.length;
|
|
this.currentCharIndex = 0;
|
|
this.currentChar = ' ';
|
|
this.lineNumber = lineNumber;
|
|
|
|
}
|
|
|
|
LineParser.prototype = {
|
|
|
|
constructor: LineParser,
|
|
|
|
seekNonSpace: function () {
|
|
|
|
while ( this.currentCharIndex < this.lineLength ) {
|
|
|
|
this.currentChar = this.line.charAt( this.currentCharIndex );
|
|
|
|
if ( this.currentChar !== ' ' && this.currentChar !== '\t' ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.currentCharIndex ++;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
getToken: function () {
|
|
|
|
var 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 ++;
|
|
|
|
}
|
|
|
|
var pos1 = this.currentCharIndex;
|
|
|
|
this.seekNonSpace();
|
|
|
|
return this.line.substring( pos0, pos1 );
|
|
|
|
},
|
|
|
|
getRemainingString: function () {
|
|
|
|
return this.line.substring( this.currentCharIndex, this.lineLength );
|
|
|
|
},
|
|
|
|
isAtTheEnd: function () {
|
|
|
|
return this.currentCharIndex >= this.lineLength;
|
|
|
|
},
|
|
|
|
setToEnd: function () {
|
|
|
|
this.currentCharIndex = this.lineLength;
|
|
|
|
},
|
|
|
|
getLineNumberString: function () {
|
|
|
|
return this.lineNumber >= 0 ? " at line " + this.lineNumber : "";
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
function sortByMaterial( a, b ) {
|
|
|
|
if ( a.colourCode === b.colourCode ) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
if ( a.colourCode < b.colourCode ) {
|
|
|
|
return - 1;
|
|
|
|
}
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
function createObject( elements, elementSize ) {
|
|
|
|
// 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 triangles or line segments by colour code to make later the mesh groups
|
|
elements.sort( sortByMaterial );
|
|
|
|
var vertices = [];
|
|
var materials = [];
|
|
|
|
var bufferGeometry = new THREE.BufferGeometry();
|
|
bufferGeometry.clearGroups();
|
|
var prevMaterial = null;
|
|
var index0 = 0;
|
|
var numGroupVerts = 0;
|
|
|
|
for ( var iElem = 0, nElem = elements.length; iElem < nElem; iElem ++ ) {
|
|
|
|
var elem = elements[ iElem ];
|
|
var v0 = elem.v0;
|
|
var v1 = elem.v1;
|
|
// Note that LDraw coordinate system is rotated 180 deg. in the X axis w.r.t. Three.js's one
|
|
vertices.push( v0.x, v0.y, v0.z, v1.x, v1.y, v1.z );
|
|
if ( elementSize === 3 ) {
|
|
|
|
vertices.push( elem.v2.x, elem.v2.y, elem.v2.z );
|
|
|
|
}
|
|
|
|
if ( prevMaterial !== elem.material ) {
|
|
|
|
if ( prevMaterial !== null ) {
|
|
|
|
bufferGeometry.addGroup( index0, numGroupVerts, materials.length - 1 );
|
|
|
|
}
|
|
|
|
materials.push( elem.material );
|
|
|
|
prevMaterial = elem.material;
|
|
index0 = iElem * elementSize;
|
|
numGroupVerts = elementSize;
|
|
|
|
} else {
|
|
|
|
numGroupVerts += elementSize;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ( numGroupVerts > 0 ) {
|
|
|
|
bufferGeometry.addGroup( index0, Infinity, materials.length - 1 );
|
|
|
|
}
|
|
|
|
bufferGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
|
|
|
|
var object3d = null;
|
|
|
|
if ( elementSize === 2 ) {
|
|
|
|
object3d = new THREE.LineSegments( bufferGeometry, materials );
|
|
|
|
} else if ( elementSize === 3 ) {
|
|
|
|
bufferGeometry.computeVertexNormals();
|
|
|
|
object3d = new THREE.Mesh( bufferGeometry, materials );
|
|
|
|
}
|
|
|
|
return object3d;
|
|
|
|
}
|
|
|
|
//
|
|
|
|
function LDrawLoader( manager ) {
|
|
|
|
this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;
|
|
|
|
// This is a stack of 'parse scopes' with one level per subobject loaded file.
|
|
// Each level contains a material lib and also other runtime variables passed between parent and child subobjects
|
|
// When searching for a material code, the stack is read from top of the stack to bottom
|
|
// Each material library is an object map keyed by colour codes.
|
|
this.parseScopesStack = null;
|
|
|
|
this.path = '';
|
|
|
|
// 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.subobjectCache = {};
|
|
|
|
// 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;
|
|
|
|
// Add default main triangle and line edge materials (used in piecess 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 THREE.Object.
|
|
// If not (the default), only one object which contains all the merged primitives will be created.
|
|
this.separateObjects = false;
|
|
|
|
// Current merged object and primitives
|
|
this.currentGroupObject = null;
|
|
this.currentTriangles = null;
|
|
this.currentLineSegments = null;
|
|
|
|
}
|
|
|
|
// Special surface finish tag types.
|
|
// Note: "MATERIAL" tag (e.g. GLITTER, SPECKLE) is not implemented
|
|
LDrawLoader.FINISH_TYPE_DEFAULT = 0;
|
|
LDrawLoader.FINISH_TYPE_CHROME = 1;
|
|
LDrawLoader.FINISH_TYPE_PEARLESCENT = 2;
|
|
LDrawLoader.FINISH_TYPE_RUBBER = 3;
|
|
LDrawLoader.FINISH_TYPE_MATTE_METALLIC = 4;
|
|
LDrawLoader.FINISH_TYPE_METAL = 5;
|
|
|
|
// State machine to search a subobject path.
|
|
// The LDraw standard establishes these various possible subfolders.
|
|
LDrawLoader.FILE_LOCATION_AS_IS = 0;
|
|
LDrawLoader.FILE_LOCATION_TRY_PARTS = 1;
|
|
LDrawLoader.FILE_LOCATION_TRY_P = 2;
|
|
LDrawLoader.FILE_LOCATION_TRY_MODELS = 3;
|
|
LDrawLoader.FILE_LOCATION_TRY_RELATIVE = 4;
|
|
LDrawLoader.FILE_LOCATION_TRY_ABSOLUTE = 5;
|
|
LDrawLoader.FILE_LOCATION_NOT_FOUND = 6;
|
|
|
|
LDrawLoader.prototype = {
|
|
|
|
constructor: LDrawLoader,
|
|
|
|
load: function ( url, onLoad, onProgress, onError ) {
|
|
|
|
if ( ! this.fileMap ) {
|
|
|
|
this.fileMap = {};
|
|
|
|
}
|
|
|
|
var scope = this;
|
|
|
|
var fileLoader = new THREE.FileLoader( this.manager );
|
|
fileLoader.setPath( this.path );
|
|
fileLoader.load( url, function ( text ) {
|
|
|
|
processObject( text, onLoad );
|
|
|
|
}, onProgress, onError );
|
|
|
|
function processObject( text, onProcessed ) {
|
|
|
|
var parseScope = scope.newParseScopeLevel();
|
|
parseScope.url = url;
|
|
|
|
var parentParseScope = scope.getParentParseScope();
|
|
|
|
// Add to cache
|
|
var currentFileName = parentParseScope.currentFileName;
|
|
if ( scope.subobjectCache[ currentFileName ] === undefined ) {
|
|
|
|
scope.subobjectCache[ currentFileName ] = text;
|
|
|
|
|
|
}
|
|
|
|
// Parse the object (returns a THREE.Group)
|
|
var objGroup = scope.parse( text );
|
|
|
|
// Load subobjects
|
|
parseScope.subobjects = objGroup.userData.subobjects;
|
|
parseScope.numSubobjects = parseScope.subobjects.length;
|
|
parseScope.subobjectIndex = 0;
|
|
|
|
if ( parseScope.numSubobjects > 0 ) {
|
|
|
|
// Load the first subobject
|
|
var subobjectGroup = loadSubobject( parseScope.subobjects[ 0 ], true );
|
|
|
|
// Optimization for loading pack: If subobjects are obtained from cache, keep loading them iteratively rather than recursively
|
|
if ( subobjectGroup ) {
|
|
|
|
while ( subobjectGroup && parseScope.subobjectIndex < parseScope.numSubobjects - 1 ) {
|
|
|
|
subobjectGroup = loadSubobject( parseScope.subobjects[ ++ parseScope.subobjectIndex ], true );
|
|
|
|
}
|
|
|
|
if ( subobjectGroup ) {
|
|
|
|
finalizeObject();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// No subobjects, finish object
|
|
finalizeObject();
|
|
|
|
}
|
|
|
|
return objGroup;
|
|
|
|
function finalizeObject() {
|
|
|
|
if ( ! scope.separateObjects && ! parentParseScope.isFromParse ) {
|
|
|
|
// We are finalizing the root object and merging primitives is activated, so create the entire Mesh and LineSegments objects now
|
|
if ( scope.currentLineSegments.length > 0 ) {
|
|
|
|
objGroup.add( createObject( scope.currentLineSegments, 2 ) );
|
|
|
|
}
|
|
|
|
if ( scope.currentTriangles.length > 0 ) {
|
|
|
|
objGroup.add( createObject( scope.currentTriangles, 3 ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
scope.removeScopeLevel();
|
|
|
|
if ( onProcessed ) {
|
|
|
|
onProcessed( objGroup );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function loadSubobject( subobject, sync ) {
|
|
|
|
parseScope.mainColourCode = subobject.material.userData.code;
|
|
parseScope.mainEdgeColourCode = subobject.material.userData.edgeMaterial.userData.code;
|
|
parseScope.currentFileName = subobject.originalFileName;
|
|
|
|
if ( ! scope.separateObjects ) {
|
|
|
|
// Set current matrix
|
|
parseScope.currentMatrix.multiplyMatrices( parentParseScope.currentMatrix, subobject.matrix );
|
|
|
|
}
|
|
|
|
// If subobject was cached previously, use the cached one
|
|
var cached = scope.subobjectCache[ subobject.originalFileName ];
|
|
if ( cached ) {
|
|
|
|
var subobjectGroup = processObject( cached, sync ? undefined : onSubobjectLoaded );
|
|
if ( sync ) {
|
|
|
|
addSubobject( subobject, subobjectGroup );
|
|
return subobjectGroup;
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Adjust file name to locate the subobject file path in standard locations (always under directory scope.path)
|
|
// Update also subobject.locationState for the next try if this load fails.
|
|
var subobjectURL = subobject.fileName;
|
|
var newLocationState = LDrawLoader.FILE_LOCATION_NOT_FOUND;
|
|
|
|
switch ( subobject.locationState ) {
|
|
|
|
case LDrawLoader.FILE_LOCATION_AS_IS:
|
|
newLocationState = subobject.locationState + 1;
|
|
break;
|
|
|
|
case LDrawLoader.FILE_LOCATION_TRY_PARTS:
|
|
subobjectURL = 'parts/' + subobjectURL;
|
|
newLocationState = subobject.locationState + 1;
|
|
break;
|
|
|
|
case LDrawLoader.FILE_LOCATION_TRY_P:
|
|
subobjectURL = 'p/' + subobjectURL;
|
|
newLocationState = subobject.locationState + 1;
|
|
break;
|
|
|
|
case LDrawLoader.FILE_LOCATION_TRY_MODELS:
|
|
subobjectURL = 'models/' + subobjectURL;
|
|
newLocationState = subobject.locationState + 1;
|
|
break;
|
|
|
|
case LDrawLoader.FILE_LOCATION_TRY_RELATIVE:
|
|
subobjectURL = url.substring( 0, url.lastIndexOf( "/" ) + 1 ) + subobjectURL;
|
|
newLocationState = subobject.locationState + 1;
|
|
break;
|
|
|
|
case LDrawLoader.FILE_LOCATION_TRY_ABSOLUTE:
|
|
|
|
if ( subobject.triedLowerCase ) {
|
|
|
|
// Try absolute path
|
|
newLocationState = LDrawLoader.FILE_LOCATION_NOT_FOUND;
|
|
|
|
} else {
|
|
|
|
// Next attempt is lower case
|
|
subobject.fileName = subobject.fileName.toLowerCase();
|
|
subobjectURL = subobject.fileName;
|
|
subobject.triedLowerCase = true;
|
|
newLocationState = LDrawLoader.FILE_LOCATION_AS_IS;
|
|
|
|
}
|
|
break;
|
|
|
|
case LDrawLoader.FILE_LOCATION_NOT_FOUND:
|
|
|
|
// All location possibilities have been tried, give up loading this object
|
|
console.warn( 'LDrawLoader: Subobject "' + subobject.originalFileName + '" could not be found.' );
|
|
|
|
// Try to read the next subobject
|
|
parseScope.subobjectIndex ++;
|
|
|
|
if ( parseScope.subobjectIndex >= parseScope.numSubobjects ) {
|
|
|
|
// All subojects have been loaded. Finish parent object
|
|
scope.removeScopeLevel();
|
|
onProcessed( objGroup );
|
|
|
|
} else {
|
|
|
|
// Load next subobject
|
|
loadSubobject( parseScope.subobjects[ parseScope.subobjectIndex ] );
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
subobject.locationState = newLocationState;
|
|
subobject.url = subobjectURL;
|
|
|
|
// Load the subobject
|
|
scope.load( subobjectURL, onSubobjectLoaded, undefined, onSubobjectError );
|
|
|
|
}
|
|
|
|
function onSubobjectLoaded( subobjectGroup ) {
|
|
|
|
var subobject = parseScope.subobjects[ parseScope.subobjectIndex ];
|
|
|
|
if ( subobjectGroup === null ) {
|
|
|
|
// Try to reload
|
|
loadSubobject( subobject );
|
|
return;
|
|
|
|
}
|
|
|
|
// Add the subobject just loaded
|
|
addSubobject( subobject, subobjectGroup );
|
|
|
|
// Proceed to load the next subobject, or finish the parent object
|
|
|
|
parseScope.subobjectIndex ++;
|
|
|
|
if ( parseScope.subobjectIndex < parseScope.numSubobjects ) {
|
|
|
|
loadSubobject( parseScope.subobjects[ parseScope.subobjectIndex ] );
|
|
|
|
} else {
|
|
|
|
finalizeObject();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function addSubobject( subobject, subobjectGroup ) {
|
|
|
|
if ( scope.separateObjects ) {
|
|
|
|
subobjectGroup.name = subobject.fileName;
|
|
objGroup.add( subobjectGroup );
|
|
subobjectGroup.matrix.copy( subobject.matrix );
|
|
subobjectGroup.matrixAutoUpdate = false;
|
|
|
|
}
|
|
|
|
scope.fileMap[ subobject.originalFileName ] = subobject.url;
|
|
|
|
}
|
|
|
|
function onSubobjectError( err ) {
|
|
|
|
// Retry download from a different default possible location
|
|
loadSubobject( parseScope.subobjects[ parseScope.subobjectIndex ] );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
setPath: function ( value ) {
|
|
|
|
this.path = value;
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
setMaterials: function ( materials ) {
|
|
|
|
// Clears parse scopes stack, adds new scope with material library
|
|
|
|
this.parseScopesStack = [];
|
|
|
|
this.newParseScopeLevel( materials );
|
|
|
|
this.getCurrentParseScope().isFromParse = false;
|
|
|
|
this.materials = materials;
|
|
|
|
this.currentGroupObject = null;
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
setFileMap: function ( fileMap ) {
|
|
|
|
this.fileMap = fileMap;
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
newParseScopeLevel: function ( materials ) {
|
|
|
|
// Adds a new scope level, assign materials to it and returns it
|
|
|
|
var matLib = {};
|
|
|
|
if ( materials ) {
|
|
|
|
for ( var i = 0, n = materials.length; i < n; i ++ ) {
|
|
|
|
var material = materials[ i ];
|
|
matLib[ material.userData.code ] = material;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var topParseScope = this.getCurrentParseScope();
|
|
|
|
var parentParseScope = this.getParentParseScope();
|
|
|
|
var newParseScope = {
|
|
|
|
lib: matLib,
|
|
url: null,
|
|
|
|
// Subobjects
|
|
subobjects: null,
|
|
numSubobjects: 0,
|
|
subobjectIndex: 0,
|
|
|
|
// Current subobject
|
|
currentFileName: null,
|
|
mainColourCode: topParseScope ? topParseScope.mainColourCode : '16',
|
|
mainEdgeColourCode: topParseScope ? topParseScope.mainEdgeColourCode : '24',
|
|
currentMatrix: new THREE.Matrix4(),
|
|
|
|
// If false, it is a root material scope previous to parse
|
|
isFromParse: true
|
|
};
|
|
|
|
this.parseScopesStack.push( newParseScope );
|
|
|
|
return newParseScope;
|
|
|
|
},
|
|
|
|
removeScopeLevel: function () {
|
|
|
|
this.parseScopesStack.pop();
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
addMaterial: function ( material ) {
|
|
|
|
// Adds a material to the material library which is on top of the parse scopes stack. And also to the materials array
|
|
|
|
var matLib = this.getCurrentParseScope().lib;
|
|
|
|
if ( ! matLib[ material.userData.code ] ) {
|
|
|
|
this.materials.push( material );
|
|
|
|
}
|
|
|
|
matLib[ material.userData.code ] = material;
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
getMaterial: function ( colourCode ) {
|
|
|
|
// Given a colour code search its material in the parse scopes stack
|
|
|
|
if ( colourCode.startsWith( "0x2" ) ) {
|
|
|
|
// Special 'direct' material value (RGB colour)
|
|
|
|
var colour = colourCode.substring( 3 );
|
|
|
|
return this.parseColourMetaDirective( new LineParser( "Direct_Color_" + colour + " CODE -1 VALUE #" + colour + " EDGE #" + colour + "" ) );
|
|
|
|
}
|
|
|
|
for ( var i = this.parseScopesStack.length - 1; i >= 0; i -- ) {
|
|
|
|
var material = this.parseScopesStack[ i ].lib[ colourCode ];
|
|
|
|
if ( material ) {
|
|
|
|
return material;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Material was not found
|
|
return null;
|
|
|
|
},
|
|
|
|
getParentParseScope: function () {
|
|
|
|
if ( this.parseScopesStack.length > 1 ) {
|
|
|
|
return this.parseScopesStack[ this.parseScopesStack.length - 2 ];
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
getCurrentParseScope: function () {
|
|
|
|
if ( this.parseScopesStack.length > 0 ) {
|
|
|
|
return this.parseScopesStack[ this.parseScopesStack.length - 1 ];
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
parseColourMetaDirective: function ( lineParser ) {
|
|
|
|
// Parses a colour definition and returns a THREE.Material or null if error
|
|
|
|
var code = null;
|
|
|
|
// Triangle and line colours
|
|
var colour = 0xFF00FF;
|
|
var edgeColour = 0xFF00FF;
|
|
|
|
// Transparency
|
|
var alpha = 1;
|
|
var isTransparent = false;
|
|
// Self-illumination:
|
|
var luminance = 0;
|
|
|
|
var finishType = LDrawLoader.FINISH_TYPE_DEFAULT;
|
|
var canHaveEnvMap = true;
|
|
|
|
var edgeMaterial = null;
|
|
|
|
var name = lineParser.getToken();
|
|
if ( ! name ) {
|
|
|
|
throw 'LDrawLoader: Material name was expected after "!COLOUR tag' + lineParser.getLineNumberString() + ".";
|
|
|
|
}
|
|
|
|
// Parse tag tokens and their parameters
|
|
var 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 = LDrawLoader.FINISH_TYPE_CHROME;
|
|
break;
|
|
|
|
case 'PEARLESCENT':
|
|
finishType = LDrawLoader.FINISH_TYPE_PEARLESCENT;
|
|
break;
|
|
|
|
case 'RUBBER':
|
|
finishType = LDrawLoader.FINISH_TYPE_RUBBER;
|
|
break;
|
|
|
|
case 'MATTE_METALLIC':
|
|
finishType = LDrawLoader.FINISH_TYPE_MATTE_METALLIC;
|
|
break;
|
|
|
|
case 'METAL':
|
|
finishType = LDrawLoader.FINISH_TYPE_METAL;
|
|
break;
|
|
|
|
case 'MATERIAL':
|
|
// Not implemented
|
|
lineParser.setToEnd();
|
|
break;
|
|
|
|
default:
|
|
throw 'LDrawLoader: Unknown token "' + token + '" while parsing material' + lineParser.getLineNumberString() + ".";
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var material = null;
|
|
|
|
switch ( finishType ) {
|
|
|
|
case LDrawLoader.FINISH_TYPE_DEFAULT:
|
|
case LDrawLoader.FINISH_TYPE_PEARLESCENT:
|
|
|
|
var specular = new THREE.Color( colour );
|
|
var shininess = 35;
|
|
var hsl = specular.getHSL( { h: 0, s: 0, l: 0 } );
|
|
|
|
if ( finishType === LDrawLoader.FINISH_TYPE_DEFAULT ) {
|
|
|
|
// Default plastic material with shiny specular
|
|
hsl.l = Math.min( 1, hsl.l + ( 1 - hsl.l ) * 0.12 );
|
|
|
|
} else {
|
|
|
|
// Try to imitate pearlescency by setting the specular to the complementary of the color, and low shininess
|
|
hsl.h = ( hsl.h + 0.5 ) % 1;
|
|
hsl.l = Math.min( 1, hsl.l + ( 1 - hsl.l ) * 0.7 );
|
|
shininess = 10;
|
|
|
|
}
|
|
|
|
specular.setHSL( hsl.h, hsl.s, hsl.l );
|
|
|
|
material = new THREE.MeshPhongMaterial( { color: colour, specular: specular, shininess: shininess, reflectivity: 0.3 } );
|
|
break;
|
|
|
|
case LDrawLoader.FINISH_TYPE_CHROME:
|
|
|
|
// Mirror finish surface
|
|
material = new THREE.MeshStandardMaterial( { color: colour, roughness: 0, metalness: 1 } );
|
|
break;
|
|
|
|
case LDrawLoader.FINISH_TYPE_RUBBER:
|
|
|
|
// Rubber is best simulated with Lambert
|
|
material = new THREE.MeshLambertMaterial( { color: colour } );
|
|
canHaveEnvMap = false;
|
|
break;
|
|
|
|
case LDrawLoader.FINISH_TYPE_MATTE_METALLIC:
|
|
|
|
// Brushed metal finish
|
|
material = new THREE.MeshStandardMaterial( { color: colour, roughness: 0.8, metalness: 0.4 } );
|
|
break;
|
|
|
|
case LDrawLoader.FINISH_TYPE_METAL:
|
|
|
|
// Average metal finish
|
|
material = new THREE.MeshStandardMaterial( { color: colour, roughness: 0.2, metalness: 0.85 } );
|
|
break;
|
|
|
|
default:
|
|
// Should not happen
|
|
break;
|
|
|
|
}
|
|
|
|
// BFC (Back Face Culling) LDraw language meta extension is not implemented, so set all materials double-sided:
|
|
material.side = THREE.DoubleSide;
|
|
|
|
material.transparent = isTransparent;
|
|
material.opacity = alpha;
|
|
|
|
material.userData.canHaveEnvMap = canHaveEnvMap;
|
|
|
|
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 } );
|
|
edgeMaterial.userData.code = code;
|
|
edgeMaterial.name = name + " - Edge";
|
|
edgeMaterial.userData.canHaveEnvMap = false;
|
|
|
|
}
|
|
|
|
material.userData.code = code;
|
|
material.name = name;
|
|
|
|
material.userData.edgeMaterial = edgeMaterial;
|
|
|
|
return material;
|
|
|
|
},
|
|
|
|
//
|
|
|
|
parse: function ( text ) {
|
|
|
|
//console.time( 'LDrawLoader' );
|
|
|
|
// Retrieve data from the parent parse scope
|
|
var parentParseScope = this.getParentParseScope();
|
|
|
|
// Main colour codes passed to this subobject (or default codes 16 and 24 if it is the root object)
|
|
var mainColourCode = parentParseScope.mainColourCode;
|
|
var mainEdgeColourCode = parentParseScope.mainEdgeColourCode;
|
|
|
|
var url = parentParseScope.url;
|
|
|
|
var currentParseScope = this.getCurrentParseScope();
|
|
|
|
// Parse result variables
|
|
var triangles;
|
|
var lineSegments;
|
|
|
|
if ( this.separateObjects ) {
|
|
|
|
triangles = [];
|
|
lineSegments = [];
|
|
|
|
} else {
|
|
|
|
if ( this.currentGroupObject === null ) {
|
|
|
|
this.currentGroupObject = new THREE.Group();
|
|
this.currentTriangles = [];
|
|
this.currentLineSegments = [];
|
|
|
|
}
|
|
|
|
triangles = this.currentTriangles;
|
|
lineSegments = this.currentLineSegments;
|
|
|
|
}
|
|
|
|
var subobjects = [];
|
|
|
|
var category = null;
|
|
var 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' );
|
|
|
|
}
|
|
|
|
var lines = text.split( '\n' );
|
|
var numLines = lines.length;
|
|
var lineIndex = 0;
|
|
|
|
var parsingEmbeddedFiles = false;
|
|
var currentEmbeddedFileName = null;
|
|
var currentEmbeddedText = null;
|
|
|
|
var scope = this;
|
|
function parseColourCode( lineParser, forEdge ) {
|
|
|
|
// Parses next colour code and returns a THREE.Material
|
|
|
|
var colourCode = lineParser.getToken();
|
|
|
|
if ( ! forEdge && colourCode === '16' ) {
|
|
|
|
colourCode = mainColourCode;
|
|
|
|
}
|
|
if ( forEdge && colourCode === '24' ) {
|
|
|
|
colourCode = mainEdgeColourCode;
|
|
|
|
}
|
|
|
|
var material = scope.getMaterial( colourCode );
|
|
|
|
if ( ! material ) {
|
|
|
|
throw 'LDrawLoader: Unknown colour code "' + colourCode + '" is used' + lineParser.getLineNumberString() + ' but it was not defined previously.';
|
|
|
|
}
|
|
|
|
return material;
|
|
|
|
}
|
|
|
|
function parseVector( lp ) {
|
|
|
|
var v = new THREE.Vector3( parseFloat( lp.getToken() ), parseFloat( lp.getToken() ), parseFloat( lp.getToken() ) );
|
|
|
|
if ( ! scope.separateObjects ) {
|
|
|
|
v.applyMatrix4( parentParseScope.currentMatrix );
|
|
|
|
}
|
|
|
|
return v;
|
|
|
|
}
|
|
|
|
// Parse all line commands
|
|
for ( lineIndex = 0; lineIndex < numLines; lineIndex ++ ) {
|
|
|
|
var line = lines[ lineIndex ];
|
|
|
|
if ( line.length === 0 ) continue;
|
|
|
|
if ( parsingEmbeddedFiles ) {
|
|
|
|
if ( line.startsWith( '0 FILE ' ) ) {
|
|
|
|
// Save previous embedded file in the cache
|
|
this.subobjectCache[ currentEmbeddedFileName ] = currentEmbeddedText;
|
|
|
|
// New embedded text file
|
|
currentEmbeddedFileName = line.substring( 7 );
|
|
currentEmbeddedText = '';
|
|
|
|
} else {
|
|
|
|
currentEmbeddedText += line + '\n';
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
var lp = new LineParser( line, lineIndex + 1 );
|
|
|
|
lp.seekNonSpace();
|
|
|
|
if ( lp.isAtTheEnd() ) {
|
|
|
|
// Empty line
|
|
continue;
|
|
|
|
}
|
|
|
|
// Parse the line type
|
|
var lineType = lp.getToken();
|
|
|
|
switch ( lineType ) {
|
|
|
|
// Line type 0: Comment or META
|
|
case '0':
|
|
|
|
// Parse meta directive
|
|
var meta = lp.getToken();
|
|
|
|
if ( meta ) {
|
|
|
|
switch ( meta ) {
|
|
|
|
case '!COLOUR':
|
|
|
|
var material = this.parseColourMetaDirective( lp );
|
|
if ( material ) {
|
|
|
|
this.addMaterial( material );
|
|
|
|
} else {
|
|
|
|
console.warn( 'LDrawLoader: Error parsing material' + lp.getLineNumberString() );
|
|
|
|
}
|
|
break;
|
|
|
|
case '!CATEGORY':
|
|
|
|
category = lp.getToken();
|
|
break;
|
|
|
|
case '!KEYWORDS':
|
|
|
|
var 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 = '';
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
// Other meta directives are not implemented
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
// Line type 1: Sub-object file
|
|
case '1':
|
|
|
|
var material = parseColourCode( lp );
|
|
|
|
var posX = parseFloat( lp.getToken() );
|
|
var posY = parseFloat( lp.getToken() );
|
|
var posZ = parseFloat( lp.getToken() );
|
|
var m0 = parseFloat( lp.getToken() );
|
|
var m1 = parseFloat( lp.getToken() );
|
|
var m2 = parseFloat( lp.getToken() );
|
|
var m3 = parseFloat( lp.getToken() );
|
|
var m4 = parseFloat( lp.getToken() );
|
|
var m5 = parseFloat( lp.getToken() );
|
|
var m6 = parseFloat( lp.getToken() );
|
|
var m7 = parseFloat( lp.getToken() );
|
|
var m8 = parseFloat( lp.getToken() );
|
|
|
|
var matrix = new THREE.Matrix4().set(
|
|
m0, m1, m2, posX,
|
|
m3, m4, m5, posY,
|
|
m6, m7, m8, posZ,
|
|
0, 0, 0, 1
|
|
);
|
|
|
|
var fileName = lp.getRemainingString().trim().replace( "\\", "/" );
|
|
|
|
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,
|
|
originalFileName: fileName,
|
|
locationState: LDrawLoader.FILE_LOCATION_AS_IS,
|
|
url: null,
|
|
triedLowerCase: false
|
|
} );
|
|
|
|
break;
|
|
|
|
// Line type 2: Line segment
|
|
case '2':
|
|
|
|
var material = parseColourCode( lp, true );
|
|
|
|
lineSegments.push( {
|
|
material: material.userData.edgeMaterial,
|
|
colourCode: material.userData.code,
|
|
v0: parseVector( lp ),
|
|
v1: parseVector( lp )
|
|
} );
|
|
|
|
break;
|
|
|
|
// Line type 3: Triangle
|
|
case '3':
|
|
|
|
var material = parseColourCode( lp );
|
|
|
|
triangles.push( {
|
|
material: material,
|
|
colourCode: material.userData.code,
|
|
v0: parseVector( lp ),
|
|
v1: parseVector( lp ),
|
|
v2: parseVector( lp )
|
|
} );
|
|
|
|
break;
|
|
|
|
// Line type 4: Quadrilateral
|
|
case '4':
|
|
|
|
var material = parseColourCode( lp );
|
|
|
|
var v0 = parseVector( lp );
|
|
var v1 = parseVector( lp );
|
|
var v2 = parseVector( lp );
|
|
var v3 = parseVector( lp );
|
|
|
|
triangles.push( {
|
|
material: material,
|
|
colourCode: material.userData.code,
|
|
v0: v0,
|
|
v1: v1,
|
|
v2: v2
|
|
} );
|
|
|
|
triangles.push( {
|
|
material: material,
|
|
colourCode: material.userData.code,
|
|
v0: v0,
|
|
v1: v2,
|
|
v2: v3
|
|
} );
|
|
|
|
break;
|
|
|
|
// Line type 5: Optional line
|
|
case '5':
|
|
// Line type 5 is not implemented
|
|
break;
|
|
|
|
default:
|
|
throw 'LDrawLoader: Unknown line type "' + lineType + '"' + lp.getLineNumberString() + '.';
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ( parsingEmbeddedFiles ) {
|
|
|
|
this.subobjectCache[ currentEmbeddedFileName ] = currentEmbeddedText;
|
|
|
|
}
|
|
|
|
//
|
|
|
|
var groupObject = null;
|
|
|
|
if ( this.separateObjects ) {
|
|
|
|
groupObject = new THREE.Group();
|
|
|
|
if ( lineSegments.length > 0 ) {
|
|
|
|
groupObject.add( createObject( lineSegments, 2 ) );
|
|
|
|
|
|
}
|
|
|
|
if ( triangles.length > 0 ) {
|
|
|
|
groupObject.add( createObject( triangles, 3 ) );
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
groupObject = this.currentGroupObject;
|
|
|
|
}
|
|
|
|
groupObject.userData.category = category;
|
|
groupObject.userData.keywords = keywords;
|
|
groupObject.userData.subobjects = subobjects;
|
|
|
|
//console.timeEnd( 'LDrawLoader' );
|
|
|
|
return groupObject;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
return LDrawLoader;
|
|
|
|
} )();
|