LDrawLoader.js 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480
  1. /**
  2. * @author mrdoob / http://mrdoob.com/
  3. * @author yomboprime / https://github.com/yomboprime/
  4. *
  5. *
  6. */
  7. THREE.LDrawLoader = ( function () {
  8. function isPrimitiveType( type ) {
  9. return /primitive/i.test( type ) || type === 'Subpart';
  10. }
  11. function LineParser( line, lineNumber ) {
  12. this.line = line;
  13. this.lineLength = line.length;
  14. this.currentCharIndex = 0;
  15. this.currentChar = ' ';
  16. this.lineNumber = lineNumber;
  17. }
  18. LineParser.prototype = {
  19. constructor: LineParser,
  20. seekNonSpace: function () {
  21. while ( this.currentCharIndex < this.lineLength ) {
  22. this.currentChar = this.line.charAt( this.currentCharIndex );
  23. if ( this.currentChar !== ' ' && this.currentChar !== '\t' ) {
  24. return;
  25. }
  26. this.currentCharIndex ++;
  27. }
  28. },
  29. getToken: function () {
  30. var pos0 = this.currentCharIndex ++;
  31. // Seek space
  32. while ( this.currentCharIndex < this.lineLength ) {
  33. this.currentChar = this.line.charAt( this.currentCharIndex );
  34. if ( this.currentChar === ' ' || this.currentChar === '\t' ) {
  35. break;
  36. }
  37. this.currentCharIndex ++;
  38. }
  39. var pos1 = this.currentCharIndex;
  40. this.seekNonSpace();
  41. return this.line.substring( pos0, pos1 );
  42. },
  43. getRemainingString: function () {
  44. return this.line.substring( this.currentCharIndex, this.lineLength );
  45. },
  46. isAtTheEnd: function () {
  47. return this.currentCharIndex >= this.lineLength;
  48. },
  49. setToEnd: function () {
  50. this.currentCharIndex = this.lineLength;
  51. },
  52. getLineNumberString: function () {
  53. return this.lineNumber >= 0 ? " at line " + this.lineNumber : "";
  54. }
  55. };
  56. function sortByMaterial( a, b ) {
  57. if ( a.colourCode === b.colourCode ) {
  58. return 0;
  59. }
  60. if ( a.colourCode < b.colourCode ) {
  61. return - 1;
  62. }
  63. return 1;
  64. }
  65. function createObject( elements, elementSize ) {
  66. // Creates a THREE.LineSegments (elementSize = 2) or a THREE.Mesh (elementSize = 3 )
  67. // With per face / segment material, implemented with mesh groups and materials array
  68. // Sort the triangles or line segments by colour code to make later the mesh groups
  69. elements.sort( sortByMaterial );
  70. var vertices = [];
  71. var materials = [];
  72. var bufferGeometry = new THREE.BufferGeometry();
  73. bufferGeometry.clearGroups();
  74. var prevMaterial = null;
  75. var index0 = 0;
  76. var numGroupVerts = 0;
  77. for ( var iElem = 0, nElem = elements.length; iElem < nElem; iElem ++ ) {
  78. var elem = elements[ iElem ];
  79. var v0 = elem.v0;
  80. var v1 = elem.v1;
  81. // Note that LDraw coordinate system is rotated 180 deg. in the X axis w.r.t. Three.js's one
  82. vertices.push( v0.x, v0.y, v0.z, v1.x, v1.y, v1.z );
  83. if ( elementSize === 3 ) {
  84. vertices.push( elem.v2.x, elem.v2.y, elem.v2.z );
  85. }
  86. if ( prevMaterial !== elem.material ) {
  87. if ( prevMaterial !== null ) {
  88. bufferGeometry.addGroup( index0, numGroupVerts, materials.length - 1 );
  89. }
  90. materials.push( elem.material );
  91. prevMaterial = elem.material;
  92. index0 = iElem * elementSize;
  93. numGroupVerts = elementSize;
  94. } else {
  95. numGroupVerts += elementSize;
  96. }
  97. }
  98. if ( numGroupVerts > 0 ) {
  99. bufferGeometry.addGroup( index0, Infinity, materials.length - 1 );
  100. }
  101. bufferGeometry.addAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
  102. var object3d = null;
  103. if ( elementSize === 2 ) {
  104. object3d = new THREE.LineSegments( bufferGeometry, materials );
  105. } else if ( elementSize === 3 ) {
  106. bufferGeometry.computeVertexNormals();
  107. object3d = new THREE.Mesh( bufferGeometry, materials );
  108. }
  109. return object3d;
  110. }
  111. //
  112. function LDrawLoader( manager ) {
  113. this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;
  114. // This is a stack of 'parse scopes' with one level per subobject loaded file.
  115. // Each level contains a material lib and also other runtime variables passed between parent and child subobjects
  116. // When searching for a material code, the stack is read from top of the stack to bottom
  117. // Each material library is an object map keyed by colour codes.
  118. this.parseScopesStack = null;
  119. this.path = '';
  120. // Array of THREE.Material
  121. this.materials = [];
  122. // Not using THREE.Cache here because it returns the previous HTML error response instead of calling onError()
  123. // This also allows to handle the embedded text files ("0 FILE" lines)
  124. this.subobjectCache = {};
  125. // 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.
  126. this.fileMap = null;
  127. // Add default main triangle and line edge materials (used in piecess that can be coloured with a main color)
  128. this.setMaterials( [
  129. this.parseColourMetaDirective( new LineParser( "Main_Colour CODE 16 VALUE #FF8080 EDGE #333333" ) ),
  130. this.parseColourMetaDirective( new LineParser( "Edge_Colour CODE 24 VALUE #A0A0A0 EDGE #333333" ) )
  131. ] );
  132. // If this flag is set to true, each subobject will be a THREE.Object.
  133. // If not (the default), only one object which contains all the merged primitives will be created.
  134. this.separateObjects = false;
  135. }
  136. // Special surface finish tag types.
  137. // Note: "MATERIAL" tag (e.g. GLITTER, SPECKLE) is not implemented
  138. LDrawLoader.FINISH_TYPE_DEFAULT = 0;
  139. LDrawLoader.FINISH_TYPE_CHROME = 1;
  140. LDrawLoader.FINISH_TYPE_PEARLESCENT = 2;
  141. LDrawLoader.FINISH_TYPE_RUBBER = 3;
  142. LDrawLoader.FINISH_TYPE_MATTE_METALLIC = 4;
  143. LDrawLoader.FINISH_TYPE_METAL = 5;
  144. // State machine to search a subobject path.
  145. // The LDraw standard establishes these various possible subfolders.
  146. LDrawLoader.FILE_LOCATION_AS_IS = 0;
  147. LDrawLoader.FILE_LOCATION_TRY_PARTS = 1;
  148. LDrawLoader.FILE_LOCATION_TRY_P = 2;
  149. LDrawLoader.FILE_LOCATION_TRY_MODELS = 3;
  150. LDrawLoader.FILE_LOCATION_TRY_RELATIVE = 4;
  151. LDrawLoader.FILE_LOCATION_TRY_ABSOLUTE = 5;
  152. LDrawLoader.FILE_LOCATION_NOT_FOUND = 6;
  153. LDrawLoader.prototype = {
  154. constructor: LDrawLoader,
  155. load: function ( url, onLoad, onProgress, onError ) {
  156. if ( ! this.fileMap ) {
  157. this.fileMap = {};
  158. }
  159. var scope = this;
  160. var fileLoader = new THREE.FileLoader( this.manager );
  161. fileLoader.setPath( this.path );
  162. fileLoader.load( url, function ( text ) {
  163. processObject( text, onLoad );
  164. }, onProgress, onError );
  165. function processObject( text, onProcessed, subobject ) {
  166. var parseScope = scope.newParseScopeLevel();
  167. parseScope.url = url;
  168. var parentParseScope = scope.getParentParseScope();
  169. // Set current matrix
  170. if ( subobject ) {
  171. parseScope.currentMatrix.multiplyMatrices( parentParseScope.currentMatrix, subobject.matrix );
  172. parseScope.matrix.copy( subobject.matrix );
  173. parseScope.inverted = subobject.inverted;
  174. }
  175. // Add to cache
  176. var currentFileName = parentParseScope.currentFileName;
  177. if ( currentFileName !== null ) {
  178. currentFileName = parentParseScope.currentFileName.toLowerCase();
  179. }
  180. if ( scope.subobjectCache[ currentFileName ] === undefined ) {
  181. scope.subobjectCache[ currentFileName ] = text;
  182. }
  183. // Parse the object (returns a THREE.Group)
  184. scope.parse( text );
  185. var finishedCount = 0;
  186. onSubobjectFinish();
  187. function onSubobjectFinish() {
  188. finishedCount ++;
  189. if ( finishedCount === parseScope.subobjects.length + 1 ) {
  190. finalizeObject();
  191. } else {
  192. // Once the previous subobject has finished we can start processing the next one in the list.
  193. // The subobject processing shares scope in processing so it's important that they be loaded serially
  194. // to avoid race conditions.
  195. // Promise.resolve is used as an approach to asynchronously schedule a task _before_ this frame ends to
  196. // avoid stack overflow exceptions when loading many subobjects from the cache. RequestAnimationFrame
  197. // will work but causes the load to happen after the next frame which causes the load to take significantly longer.
  198. var subobject = parseScope.subobjects[ parseScope.subobjectIndex ];
  199. Promise.resolve().then( function () {
  200. loadSubobject( subobject );
  201. } );
  202. parseScope.subobjectIndex ++;
  203. }
  204. }
  205. function finalizeObject() {
  206. // TODO: Handle smoothing
  207. var isRoot = ! parentParseScope.isFromParse;
  208. if ( scope.separateObjects && ! isPrimitiveType( parseScope.type ) || isRoot ) {
  209. const objGroup = parseScope.groupObject;
  210. if ( parseScope.triangles.length > 0 ) {
  211. objGroup.add( createObject( parseScope.triangles, 3 ) );
  212. }
  213. if ( parseScope.lineSegments.length > 0 ) {
  214. objGroup.add( createObject( parseScope.lineSegments, 2 ) );
  215. }
  216. if ( parseScope.optionalSegments.length > 0 ) {
  217. objGroup.add( createObject( parseScope.optionalSegments, 2 ) );
  218. }
  219. if ( parentParseScope.groupObject ) {
  220. objGroup.name = parseScope.fileName;
  221. objGroup.matrix.copy( parseScope.matrix );
  222. objGroup.matrix.decompose( objGroup.position, objGroup.quaternion, objGroup.scale );
  223. objGroup.matrixAutoUpdate = false;
  224. parentParseScope.groupObject.add( objGroup );
  225. }
  226. } else {
  227. if ( scope.separateObjects ) {
  228. parseScope.lineSegments.forEach( ls => {
  229. ls.v0.applyMatrix4( parseScope.matrix );
  230. ls.v1.applyMatrix4( parseScope.matrix );
  231. } );
  232. parseScope.optionalSegments.forEach( ls => {
  233. ls.v0.applyMatrix4( parseScope.matrix );
  234. ls.v1.applyMatrix4( parseScope.matrix );
  235. } );
  236. parseScope.triangles.forEach( ls => {
  237. ls.v0 = ls.v0.clone().applyMatrix4( parseScope.matrix );
  238. ls.v1 = ls.v1.clone().applyMatrix4( parseScope.matrix );
  239. ls.v2 = ls.v2.clone().applyMatrix4( parseScope.matrix );
  240. } );
  241. }
  242. // TODO: we need to multiple matrices here
  243. // TODO: First, instead of tracking matrices anywhere else we
  244. // should just multiple everything here.
  245. var parentLineSegments = parentParseScope.lineSegments;
  246. var parentOptionalSegments = parentParseScope.optionalSegments;
  247. var parentTriangles = parentParseScope.triangles;
  248. var lineSegments = parseScope.lineSegments;
  249. var optionalSegments = parseScope.optionalSegments;
  250. var triangles = parseScope.triangles;
  251. for ( var i = 0, l = lineSegments.length; i < l; i ++ ) {
  252. parentLineSegments.push( lineSegments[ i ] );
  253. }
  254. for ( var i = 0, l = optionalSegments.length; i < l; i ++ ) {
  255. parentOptionalSegments.push( optionalSegments[ i ] );
  256. }
  257. for ( var i = 0, l = triangles.length; i < l; i ++ ) {
  258. parentTriangles.push( triangles[ i ] );
  259. }
  260. }
  261. scope.removeScopeLevel();
  262. if ( onProcessed ) {
  263. onProcessed( parseScope.groupObject );
  264. }
  265. }
  266. function loadSubobject( subobject ) {
  267. parseScope.mainColourCode = subobject.material.userData.code;
  268. parseScope.mainEdgeColourCode = subobject.material.userData.edgeMaterial.userData.code;
  269. parseScope.currentFileName = subobject.originalFileName;
  270. // If subobject was cached previously, use the cached one
  271. var cached = scope.subobjectCache[ subobject.originalFileName.toLowerCase() ];
  272. if ( cached ) {
  273. processObject( cached, function ( subobjectGroup ) {
  274. onSubobjectLoaded( subobjectGroup, subobject );
  275. onSubobjectFinish();
  276. }, subobject );
  277. return;
  278. }
  279. // Adjust file name to locate the subobject file path in standard locations (always under directory scope.path)
  280. // Update also subobject.locationState for the next try if this load fails.
  281. var subobjectURL = subobject.fileName;
  282. var newLocationState = LDrawLoader.FILE_LOCATION_NOT_FOUND;
  283. switch ( subobject.locationState ) {
  284. case LDrawLoader.FILE_LOCATION_AS_IS:
  285. newLocationState = subobject.locationState + 1;
  286. break;
  287. case LDrawLoader.FILE_LOCATION_TRY_PARTS:
  288. subobjectURL = 'parts/' + subobjectURL;
  289. newLocationState = subobject.locationState + 1;
  290. break;
  291. case LDrawLoader.FILE_LOCATION_TRY_P:
  292. subobjectURL = 'p/' + subobjectURL;
  293. newLocationState = subobject.locationState + 1;
  294. break;
  295. case LDrawLoader.FILE_LOCATION_TRY_MODELS:
  296. subobjectURL = 'models/' + subobjectURL;
  297. newLocationState = subobject.locationState + 1;
  298. break;
  299. case LDrawLoader.FILE_LOCATION_TRY_RELATIVE:
  300. subobjectURL = url.substring( 0, url.lastIndexOf( "/" ) + 1 ) + subobjectURL;
  301. newLocationState = subobject.locationState + 1;
  302. break;
  303. case LDrawLoader.FILE_LOCATION_TRY_ABSOLUTE:
  304. if ( subobject.triedLowerCase ) {
  305. // Try absolute path
  306. newLocationState = LDrawLoader.FILE_LOCATION_NOT_FOUND;
  307. } else {
  308. // Next attempt is lower case
  309. subobject.fileName = subobject.fileName.toLowerCase();
  310. subobjectURL = subobject.fileName;
  311. subobject.triedLowerCase = true;
  312. newLocationState = LDrawLoader.FILE_LOCATION_AS_IS;
  313. }
  314. break;
  315. case LDrawLoader.FILE_LOCATION_NOT_FOUND:
  316. // All location possibilities have been tried, give up loading this object
  317. console.warn( 'LDrawLoader: Subobject "' + subobject.originalFileName + '" could not be found.' );
  318. return;
  319. }
  320. subobject.locationState = newLocationState;
  321. subobject.url = subobjectURL;
  322. // Load the subobject
  323. // Use another file loader here so we can keep track of the subobject information
  324. // and use it when processing the next model.
  325. var fileLoader = new THREE.FileLoader( scope.manager );
  326. fileLoader.setPath( scope.path );
  327. fileLoader.load( subobjectURL, function ( text ) {
  328. processObject( text, function ( subobjectGroup ) {
  329. onSubobjectLoaded( subobjectGroup, subobject );
  330. onSubobjectFinish();
  331. }, subobject );
  332. }, undefined, function ( err ) {
  333. onSubobjectError( err, subobject );
  334. }, subobject );
  335. }
  336. function onSubobjectLoaded( subobjectGroup, subobject ) {
  337. if ( subobjectGroup === null ) {
  338. // Try to reload
  339. loadSubobject( subobject );
  340. return;
  341. }
  342. scope.fileMap[ subobject.originalFileName ] = subobject.url;
  343. }
  344. function onSubobjectError( err, subobject ) {
  345. // Retry download from a different default possible location
  346. loadSubobject( subobject );
  347. }
  348. }
  349. },
  350. setPath: function ( value ) {
  351. this.path = value;
  352. return this;
  353. },
  354. setMaterials: function ( materials ) {
  355. // Clears parse scopes stack, adds new scope with material library
  356. this.parseScopesStack = [];
  357. this.newParseScopeLevel( materials );
  358. this.getCurrentParseScope().isFromParse = false;
  359. this.materials = materials;
  360. return this;
  361. },
  362. setFileMap: function ( fileMap ) {
  363. this.fileMap = fileMap;
  364. return this;
  365. },
  366. newParseScopeLevel: function ( materials ) {
  367. // Adds a new scope level, assign materials to it and returns it
  368. var matLib = {};
  369. if ( materials ) {
  370. for ( var i = 0, n = materials.length; i < n; i ++ ) {
  371. var material = materials[ i ];
  372. matLib[ material.userData.code ] = material;
  373. }
  374. }
  375. var topParseScope = this.getCurrentParseScope();
  376. var newParseScope = {
  377. lib: matLib,
  378. url: null,
  379. // Subobjects
  380. subobjects: null,
  381. numSubobjects: 0,
  382. subobjectIndex: 0,
  383. inverted: false,
  384. // Current subobject
  385. currentFileName: null,
  386. mainColourCode: topParseScope ? topParseScope.mainColourCode : '16',
  387. mainEdgeColourCode: topParseScope ? topParseScope.mainEdgeColourCode : '24',
  388. currentMatrix: new THREE.Matrix4(),
  389. matrix: new THREE.Matrix4(),
  390. // If false, it is a root material scope previous to parse
  391. isFromParse: true,
  392. triangles: null,
  393. lineSegments: null,
  394. optionalSegments: null,
  395. };
  396. this.parseScopesStack.push( newParseScope );
  397. return newParseScope;
  398. },
  399. removeScopeLevel: function () {
  400. this.parseScopesStack.pop();
  401. return this;
  402. },
  403. addMaterial: function ( material ) {
  404. // Adds a material to the material library which is on top of the parse scopes stack. And also to the materials array
  405. var matLib = this.getCurrentParseScope().lib;
  406. if ( ! matLib[ material.userData.code ] ) {
  407. this.materials.push( material );
  408. }
  409. matLib[ material.userData.code ] = material;
  410. return this;
  411. },
  412. getMaterial: function ( colourCode ) {
  413. // Given a colour code search its material in the parse scopes stack
  414. if ( colourCode.startsWith( "0x2" ) ) {
  415. // Special 'direct' material value (RGB colour)
  416. var colour = colourCode.substring( 3 );
  417. return this.parseColourMetaDirective( new LineParser( "Direct_Color_" + colour + " CODE -1 VALUE #" + colour + " EDGE #" + colour + "" ) );
  418. }
  419. for ( var i = this.parseScopesStack.length - 1; i >= 0; i -- ) {
  420. var material = this.parseScopesStack[ i ].lib[ colourCode ];
  421. if ( material ) {
  422. return material;
  423. }
  424. }
  425. // Material was not found
  426. return null;
  427. },
  428. getParentParseScope: function () {
  429. if ( this.parseScopesStack.length > 1 ) {
  430. return this.parseScopesStack[ this.parseScopesStack.length - 2 ];
  431. }
  432. return null;
  433. },
  434. getCurrentParseScope: function () {
  435. if ( this.parseScopesStack.length > 0 ) {
  436. return this.parseScopesStack[ this.parseScopesStack.length - 1 ];
  437. }
  438. return null;
  439. },
  440. parseColourMetaDirective: function ( lineParser ) {
  441. // Parses a colour definition and returns a THREE.Material or null if error
  442. var code = null;
  443. // Triangle and line colours
  444. var colour = 0xFF00FF;
  445. var edgeColour = 0xFF00FF;
  446. // Transparency
  447. var alpha = 1;
  448. var isTransparent = false;
  449. // Self-illumination:
  450. var luminance = 0;
  451. var finishType = LDrawLoader.FINISH_TYPE_DEFAULT;
  452. var canHaveEnvMap = true;
  453. var edgeMaterial = null;
  454. var name = lineParser.getToken();
  455. if ( ! name ) {
  456. throw 'LDrawLoader: Material name was expected after "!COLOUR tag' + lineParser.getLineNumberString() + ".";
  457. }
  458. // Parse tag tokens and their parameters
  459. var token = null;
  460. while ( true ) {
  461. token = lineParser.getToken();
  462. if ( ! token ) {
  463. break;
  464. }
  465. switch ( token.toUpperCase() ) {
  466. case "CODE":
  467. code = lineParser.getToken();
  468. break;
  469. case "VALUE":
  470. colour = lineParser.getToken();
  471. if ( colour.startsWith( '0x' ) ) {
  472. colour = '#' + colour.substring( 2 );
  473. } else if ( ! colour.startsWith( '#' ) ) {
  474. throw 'LDrawLoader: Invalid colour while parsing material' + lineParser.getLineNumberString() + ".";
  475. }
  476. break;
  477. case "EDGE":
  478. edgeColour = lineParser.getToken();
  479. if ( edgeColour.startsWith( '0x' ) ) {
  480. edgeColour = '#' + edgeColour.substring( 2 );
  481. } else if ( ! edgeColour.startsWith( '#' ) ) {
  482. // Try to see if edge colour is a colour code
  483. edgeMaterial = this.getMaterial( edgeColour );
  484. if ( ! edgeMaterial ) {
  485. throw 'LDrawLoader: Invalid edge colour while parsing material' + lineParser.getLineNumberString() + ".";
  486. }
  487. // Get the edge material for this triangle material
  488. edgeMaterial = edgeMaterial.userData.edgeMaterial;
  489. }
  490. break;
  491. case 'ALPHA':
  492. alpha = parseInt( lineParser.getToken() );
  493. if ( isNaN( alpha ) ) {
  494. throw 'LDrawLoader: Invalid alpha value in material definition' + lineParser.getLineNumberString() + ".";
  495. }
  496. alpha = Math.max( 0, Math.min( 1, alpha / 255 ) );
  497. if ( alpha < 1 ) {
  498. isTransparent = true;
  499. }
  500. break;
  501. case 'LUMINANCE':
  502. luminance = parseInt( lineParser.getToken() );
  503. if ( isNaN( luminance ) ) {
  504. throw 'LDrawLoader: Invalid luminance value in material definition' + LineParser.getLineNumberString() + ".";
  505. }
  506. luminance = Math.max( 0, Math.min( 1, luminance / 255 ) );
  507. break;
  508. case 'CHROME':
  509. finishType = LDrawLoader.FINISH_TYPE_CHROME;
  510. break;
  511. case 'PEARLESCENT':
  512. finishType = LDrawLoader.FINISH_TYPE_PEARLESCENT;
  513. break;
  514. case 'RUBBER':
  515. finishType = LDrawLoader.FINISH_TYPE_RUBBER;
  516. break;
  517. case 'MATTE_METALLIC':
  518. finishType = LDrawLoader.FINISH_TYPE_MATTE_METALLIC;
  519. break;
  520. case 'METAL':
  521. finishType = LDrawLoader.FINISH_TYPE_METAL;
  522. break;
  523. case 'MATERIAL':
  524. // Not implemented
  525. lineParser.setToEnd();
  526. break;
  527. default:
  528. throw 'LDrawLoader: Unknown token "' + token + '" while parsing material' + lineParser.getLineNumberString() + ".";
  529. break;
  530. }
  531. }
  532. var material = null;
  533. switch ( finishType ) {
  534. case LDrawLoader.FINISH_TYPE_DEFAULT:
  535. material = new THREE.MeshStandardMaterial( { color: colour, roughness: 0.3, envMapIntensity: 0.3, metalness: 0 } );
  536. break;
  537. case LDrawLoader.FINISH_TYPE_PEARLESCENT:
  538. // Try to imitate pearlescency by setting the specular to the complementary of the color, and low shininess
  539. var specular = new THREE.Color( colour );
  540. var hsl = specular.getHSL( { h: 0, s: 0, l: 0 } );
  541. hsl.h = ( hsl.h + 0.5 ) % 1;
  542. hsl.l = Math.min( 1, hsl.l + ( 1 - hsl.l ) * 0.7 );
  543. specular.setHSL( hsl.h, hsl.s, hsl.l );
  544. material = new THREE.MeshPhongMaterial( { color: colour, specular: specular, shininess: 10, reflectivity: 0.3 } );
  545. break;
  546. case LDrawLoader.FINISH_TYPE_CHROME:
  547. // Mirror finish surface
  548. material = new THREE.MeshStandardMaterial( { color: colour, roughness: 0, metalness: 1 } );
  549. break;
  550. case LDrawLoader.FINISH_TYPE_RUBBER:
  551. // Rubber is best simulated with Lambert
  552. material = new THREE.MeshStandardMaterial( { color: colour, roughness: 0.9, metalness: 0 } );
  553. canHaveEnvMap = false;
  554. break;
  555. case LDrawLoader.FINISH_TYPE_MATTE_METALLIC:
  556. // Brushed metal finish
  557. material = new THREE.MeshStandardMaterial( { color: colour, roughness: 0.8, metalness: 0.4 } );
  558. break;
  559. case LDrawLoader.FINISH_TYPE_METAL:
  560. // Average metal finish
  561. material = new THREE.MeshStandardMaterial( { color: colour, roughness: 0.2, metalness: 0.85 } );
  562. break;
  563. default:
  564. // Should not happen
  565. break;
  566. }
  567. material.transparent = isTransparent;
  568. material.opacity = alpha;
  569. material.userData.canHaveEnvMap = canHaveEnvMap;
  570. if ( luminance !== 0 ) {
  571. material.emissive.set( material.color ).multiplyScalar( luminance );
  572. }
  573. if ( ! edgeMaterial ) {
  574. // This is the material used for edges
  575. edgeMaterial = new THREE.LineBasicMaterial( { color: edgeColour } );
  576. edgeMaterial.userData.code = code;
  577. edgeMaterial.name = name + " - Edge";
  578. edgeMaterial.userData.canHaveEnvMap = false;
  579. }
  580. material.userData.code = code;
  581. material.name = name;
  582. material.userData.edgeMaterial = edgeMaterial;
  583. return material;
  584. },
  585. //
  586. parse: function ( text ) {
  587. //console.time( 'LDrawLoader' );
  588. // Retrieve data from the parent parse scope
  589. var parentParseScope = this.getParentParseScope();
  590. // Main colour codes passed to this subobject (or default codes 16 and 24 if it is the root object)
  591. var mainColourCode = parentParseScope.mainColourCode;
  592. var mainEdgeColourCode = parentParseScope.mainEdgeColourCode;
  593. var url = parentParseScope.url;
  594. var currentParseScope = this.getCurrentParseScope();
  595. // Parse result variables
  596. var triangles;
  597. var lineSegments;
  598. var optionalSegments;
  599. var subobjects = [];
  600. var category = null;
  601. var keywords = null;
  602. if ( text.indexOf( '\r\n' ) !== - 1 ) {
  603. // This is faster than String.split with regex that splits on both
  604. text = text.replace( /\r\n/g, '\n' );
  605. }
  606. var lines = text.split( '\n' );
  607. var numLines = lines.length;
  608. var lineIndex = 0;
  609. var parsingEmbeddedFiles = false;
  610. var currentEmbeddedFileName = null;
  611. var currentEmbeddedText = null;
  612. var bfcCertified = false;
  613. var bfcCCW = true;
  614. var bfcInverted = false;
  615. var bfcCull = true;
  616. var type = '';
  617. var scope = this;
  618. function parseColourCode( lineParser, forEdge ) {
  619. // Parses next colour code and returns a THREE.Material
  620. var colourCode = lineParser.getToken();
  621. if ( ! forEdge && colourCode === '16' ) {
  622. colourCode = mainColourCode;
  623. }
  624. if ( forEdge && colourCode === '24' ) {
  625. colourCode = mainEdgeColourCode;
  626. }
  627. var material = scope.getMaterial( colourCode );
  628. if ( ! material ) {
  629. throw 'LDrawLoader: Unknown colour code "' + colourCode + '" is used' + lineParser.getLineNumberString() + ' but it was not defined previously.';
  630. }
  631. return material;
  632. }
  633. function parseVector( lp ) {
  634. var v = new THREE.Vector3( parseFloat( lp.getToken() ), parseFloat( lp.getToken() ), parseFloat( lp.getToken() ) );
  635. if ( ! scope.separateObjects ) {
  636. v.applyMatrix4( currentParseScope.currentMatrix );
  637. }
  638. return v;
  639. }
  640. // Parse all line commands
  641. for ( lineIndex = 0; lineIndex < numLines; lineIndex ++ ) {
  642. var line = lines[ lineIndex ];
  643. if ( line.length === 0 ) continue;
  644. if ( parsingEmbeddedFiles ) {
  645. if ( line.startsWith( '0 FILE ' ) ) {
  646. // Save previous embedded file in the cache
  647. this.subobjectCache[ currentEmbeddedFileName.toLowerCase() ] = currentEmbeddedText;
  648. // New embedded text file
  649. currentEmbeddedFileName = line.substring( 7 );
  650. currentEmbeddedText = '';
  651. } else {
  652. currentEmbeddedText += line + '\n';
  653. }
  654. continue;
  655. }
  656. var lp = new LineParser( line, lineIndex + 1 );
  657. lp.seekNonSpace();
  658. if ( lp.isAtTheEnd() ) {
  659. // Empty line
  660. continue;
  661. }
  662. // Parse the line type
  663. var lineType = lp.getToken();
  664. switch ( lineType ) {
  665. // Line type 0: Comment or META
  666. case '0':
  667. // Parse meta directive
  668. var meta = lp.getToken();
  669. if ( meta ) {
  670. switch ( meta ) {
  671. case '!LDRAW_ORG':
  672. type = lp.getToken();
  673. if ( ! parsingEmbeddedFiles ) {
  674. currentParseScope.triangles = [];
  675. currentParseScope.lineSegments = [];
  676. currentParseScope.optionalSegments = [];
  677. currentParseScope.type = type;
  678. var isRoot = ! parentParseScope.isFromParse;
  679. if ( isRoot || scope.separateObjects && ! isPrimitiveType( type ) ) {
  680. currentParseScope.groupObject = new THREE.Group();
  681. }
  682. triangles = currentParseScope.triangles;
  683. lineSegments = currentParseScope.lineSegments;
  684. optionalSegments = currentParseScope.optionalSegments;
  685. }
  686. break;
  687. case '!COLOUR':
  688. var material = this.parseColourMetaDirective( lp );
  689. if ( material ) {
  690. this.addMaterial( material );
  691. } else {
  692. console.warn( 'LDrawLoader: Error parsing material' + lp.getLineNumberString() );
  693. }
  694. break;
  695. case '!CATEGORY':
  696. category = lp.getToken();
  697. break;
  698. case '!KEYWORDS':
  699. var newKeywords = lp.getRemainingString().split( ',' );
  700. if ( newKeywords.length > 0 ) {
  701. if ( ! keywords ) {
  702. keywords = [];
  703. }
  704. newKeywords.forEach( function ( keyword ) {
  705. keywords.push( keyword.trim() );
  706. } );
  707. }
  708. break;
  709. case 'FILE':
  710. if ( lineIndex > 0 ) {
  711. // Start embedded text files parsing
  712. parsingEmbeddedFiles = true;
  713. currentEmbeddedFileName = lp.getRemainingString();
  714. currentEmbeddedText = '';
  715. bfcCertified = false;
  716. bfcCCW = true;
  717. }
  718. break;
  719. case 'BFC':
  720. // Changes to the backface culling state
  721. while ( ! lp.isAtTheEnd() ) {
  722. var token = lp.getToken();
  723. switch ( token ) {
  724. case 'CERTIFY':
  725. case 'NOCERTIFY':
  726. bfcCertified = token === 'CERTIFY';
  727. bfcCCW = true;
  728. break;
  729. case 'CW':
  730. case 'CCW':
  731. bfcCCW = token === 'CCW';
  732. break;
  733. case 'INVERTNEXT':
  734. bfcInverted = true;
  735. break;
  736. case 'CLIP':
  737. case 'NOCLIP':
  738. bfcCull = token === 'CLIP';
  739. break;
  740. default:
  741. console.warn( 'THREE.LDrawLoader: BFC directive "' + token + '" is unknown.' );
  742. break;
  743. }
  744. }
  745. break;
  746. default:
  747. // Other meta directives are not implemented
  748. break;
  749. }
  750. }
  751. break;
  752. // Line type 1: Sub-object file
  753. case '1':
  754. var material = parseColourCode( lp );
  755. var posX = parseFloat( lp.getToken() );
  756. var posY = parseFloat( lp.getToken() );
  757. var posZ = parseFloat( lp.getToken() );
  758. var m0 = parseFloat( lp.getToken() );
  759. var m1 = parseFloat( lp.getToken() );
  760. var m2 = parseFloat( lp.getToken() );
  761. var m3 = parseFloat( lp.getToken() );
  762. var m4 = parseFloat( lp.getToken() );
  763. var m5 = parseFloat( lp.getToken() );
  764. var m6 = parseFloat( lp.getToken() );
  765. var m7 = parseFloat( lp.getToken() );
  766. var m8 = parseFloat( lp.getToken() );
  767. var matrix = new THREE.Matrix4().set(
  768. m0, m1, m2, posX,
  769. m3, m4, m5, posY,
  770. m6, m7, m8, posZ,
  771. 0, 0, 0, 1
  772. );
  773. var fileName = lp.getRemainingString().trim().replace( "\\", "/" );
  774. if ( scope.fileMap[ fileName ] ) {
  775. // Found the subobject path in the preloaded file path map
  776. fileName = scope.fileMap[ fileName ];
  777. } else {
  778. // Standardized subfolders
  779. if ( fileName.startsWith( 's/' ) ) {
  780. fileName = 'parts/' + fileName;
  781. } else if ( fileName.startsWith( '48/' ) ) {
  782. fileName = 'p/' + fileName;
  783. }
  784. }
  785. // If the scale of the object is negated then the triangle winding order
  786. // needs to be flipped.
  787. if ( matrix.determinant() < 0 ) {
  788. bfcInverted = ! bfcInverted;
  789. }
  790. subobjects.push( {
  791. material: material,
  792. matrix: matrix,
  793. fileName: fileName,
  794. originalFileName: fileName,
  795. locationState: LDrawLoader.FILE_LOCATION_AS_IS,
  796. url: null,
  797. triedLowerCase: false,
  798. inverted: bfcInverted !== currentParseScope.inverted
  799. } );
  800. bfcInverted = false;
  801. break;
  802. // Line type 2: Line segment
  803. // Line type 5: Optional Line segment
  804. case '2':
  805. case '5':
  806. var material = parseColourCode( lp, true );
  807. var arr = lineType === '2' ? lineSegments : optionalSegments;
  808. arr.push( {
  809. material: material.userData.edgeMaterial,
  810. colourCode: material.userData.code,
  811. v0: parseVector( lp ),
  812. v1: parseVector( lp )
  813. } );
  814. break;
  815. // Line type 3: Triangle
  816. case '3':
  817. var material = parseColourCode( lp );
  818. var inverted = currentParseScope.inverted;
  819. var ccw = bfcCCW !== inverted;
  820. var doubleSided = ! bfcCertified || ! bfcCull;
  821. var v0, v1, v2;
  822. if ( ccw === true ) {
  823. v0 = parseVector( lp );
  824. v1 = parseVector( lp );
  825. v2 = parseVector( lp );
  826. } else {
  827. v2 = parseVector( lp );
  828. v1 = parseVector( lp );
  829. v0 = parseVector( lp );
  830. }
  831. triangles.push( {
  832. material: material,
  833. colourCode: material.userData.code,
  834. v0: v0,
  835. v1: v1,
  836. v2: v2
  837. } );
  838. if ( doubleSided === true ) {
  839. triangles.push( {
  840. material: material,
  841. colourCode: material.userData.code,
  842. v0: v0,
  843. v1: v2,
  844. v2: v1
  845. } );
  846. }
  847. break;
  848. // Line type 4: Quadrilateral
  849. case '4':
  850. var material = parseColourCode( lp );
  851. var inverted = currentParseScope.inverted;
  852. var ccw = bfcCCW !== inverted;
  853. var doubleSided = ! bfcCertified || ! bfcCull;
  854. var v0, v1, v2, v3;
  855. if ( ccw === true ) {
  856. v0 = parseVector( lp );
  857. v1 = parseVector( lp );
  858. v2 = parseVector( lp );
  859. v3 = parseVector( lp );
  860. } else {
  861. v3 = parseVector( lp );
  862. v2 = parseVector( lp );
  863. v1 = parseVector( lp );
  864. v0 = parseVector( lp );
  865. }
  866. triangles.push( {
  867. material: material,
  868. colourCode: material.userData.code,
  869. v0: v0,
  870. v1: v1,
  871. v2: v2
  872. } );
  873. triangles.push( {
  874. material: material,
  875. colourCode: material.userData.code,
  876. v0: v0,
  877. v1: v2,
  878. v2: v3
  879. } );
  880. if ( doubleSided === true ) {
  881. triangles.push( {
  882. material: material,
  883. colourCode: material.userData.code,
  884. v0: v0,
  885. v1: v2,
  886. v2: v1
  887. } );
  888. triangles.push( {
  889. material: material,
  890. colourCode: material.userData.code,
  891. v0: v0,
  892. v1: v3,
  893. v2: v2
  894. } );
  895. }
  896. break;
  897. default:
  898. throw 'LDrawLoader: Unknown line type "' + lineType + '"' + lp.getLineNumberString() + '.';
  899. break;
  900. }
  901. }
  902. if ( parsingEmbeddedFiles ) {
  903. this.subobjectCache[ currentEmbeddedFileName.toLowerCase() ] = currentEmbeddedText;
  904. }
  905. currentParseScope.category = category;
  906. currentParseScope.keywords = keywords;
  907. currentParseScope.subobjects = subobjects;
  908. currentParseScope.numSubobjects = subobjects.length;
  909. currentParseScope.subobjectIndex = 0;
  910. }
  911. };
  912. return LDrawLoader;
  913. } )();