super-three 0.136.0 → 0.136.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/LICENSE +1 -1
  2. package/build/three.js +1634 -1608
  3. package/build/three.min.js +1 -1
  4. package/build/three.module.js +4630 -4534
  5. package/examples/js/animation/MMDPhysics.js +1 -1
  6. package/examples/js/cameras/CinematicCamera.js +2 -7
  7. package/examples/js/exporters/MMDExporter.js +1 -1
  8. package/examples/js/geometries/ConvexGeometry.js +1 -1
  9. package/examples/js/loaders/BasisTextureLoader.js +1 -0
  10. package/examples/js/loaders/EXRLoader.js +25 -19
  11. package/examples/js/loaders/GLTFLoader.js +2 -2
  12. package/examples/js/loaders/HDRCubeTextureLoader.js +0 -2
  13. package/examples/js/loaders/LDrawLoader.js +1131 -859
  14. package/examples/js/loaders/LUT3dlLoader.js +17 -10
  15. package/examples/js/loaders/LUTCubeLoader.js +3 -4
  16. package/examples/js/loaders/RGBELoader.js +6 -6
  17. package/examples/js/loaders/RGBMLoader.js +1 -1
  18. package/examples/js/loaders/SVGLoader.js +3 -1
  19. package/examples/js/loaders/VRMLLoader.js +9 -21
  20. package/examples/js/math/ConvexHull.js +7 -13
  21. package/examples/js/misc/Volume.js +2 -2
  22. package/examples/js/modifiers/CurveModifier.js +8 -7
  23. package/examples/js/modifiers/EdgeSplitModifier.js +1 -1
  24. package/examples/js/modifiers/SimplifyModifier.js +1 -1
  25. package/examples/js/objects/Reflector.js +1 -13
  26. package/examples/js/objects/ReflectorForSSRPass.js +0 -10
  27. package/examples/js/objects/Refractor.js +1 -13
  28. package/examples/js/objects/Water.js +1 -13
  29. package/examples/js/postprocessing/GlitchPass.js +4 -5
  30. package/examples/js/postprocessing/SMAAPass.js +2 -9
  31. package/examples/js/postprocessing/SSAOPass.js +9 -21
  32. package/examples/js/postprocessing/SavePass.js +1 -5
  33. package/examples/js/shaders/DigitalGlitch.js +3 -3
  34. package/examples/js/shaders/MMDToonShader.js +0 -1
  35. package/examples/js/shaders/SSAOShader.js +1 -1
  36. package/examples/jsm/animation/MMDPhysics.js +1 -1
  37. package/examples/jsm/cameras/CinematicCamera.js +2 -5
  38. package/examples/jsm/{WebGL.js → capabilities/WebGL.js} +2 -2
  39. package/examples/jsm/{renderers/webgpu → capabilities}/WebGPU.js +1 -1
  40. package/examples/jsm/exporters/GLTFExporter.js +8 -24
  41. package/examples/jsm/exporters/MMDExporter.js +1 -1
  42. package/examples/jsm/geometries/ConvexGeometry.js +1 -1
  43. package/examples/jsm/libs/lil-gui.module.min.js +2 -2
  44. package/examples/jsm/loaders/BasisTextureLoader.js +7 -0
  45. package/examples/jsm/loaders/EXRLoader.js +22 -19
  46. package/examples/jsm/loaders/GLTFLoader.js +1 -3
  47. package/examples/jsm/loaders/HDRCubeTextureLoader.js +1 -4
  48. package/examples/jsm/loaders/LDrawLoader.js +1147 -885
  49. package/examples/jsm/loaders/LUT3dlLoader.js +17 -12
  50. package/examples/jsm/loaders/LUTCubeLoader.js +3 -5
  51. package/examples/jsm/loaders/RGBELoader.js +7 -8
  52. package/examples/jsm/loaders/RGBMLoader.js +1 -1
  53. package/examples/jsm/loaders/SVGLoader.js +6 -1
  54. package/examples/jsm/loaders/VRMLLoader.js +9 -22
  55. package/examples/jsm/math/ConvexHull.js +7 -13
  56. package/examples/jsm/misc/Volume.js +2 -2
  57. package/examples/jsm/modifiers/CurveModifier.js +9 -8
  58. package/examples/jsm/modifiers/EdgeSplitModifier.js +1 -1
  59. package/examples/jsm/modifiers/SimplifyModifier.js +1 -1
  60. package/examples/jsm/node-editor/NodeEditor.js +21 -22
  61. package/examples/jsm/nodes/utils/ColorSpaceNode.js +4 -13
  62. package/examples/jsm/objects/Lensflare.js +4 -4
  63. package/examples/jsm/objects/Reflector.js +1 -16
  64. package/examples/jsm/objects/ReflectorForSSRPass.js +0 -12
  65. package/examples/jsm/objects/Refractor.js +1 -16
  66. package/examples/jsm/objects/Water.js +1 -16
  67. package/examples/jsm/postprocessing/GlitchPass.js +7 -6
  68. package/examples/jsm/postprocessing/SMAAPass.js +2 -11
  69. package/examples/jsm/postprocessing/SSAOPass.js +13 -25
  70. package/examples/jsm/postprocessing/SavePass.js +1 -3
  71. package/examples/jsm/renderers/nodes/Nodes.js +2 -0
  72. package/examples/jsm/renderers/nodes/ShaderNode.js +58 -31
  73. package/examples/jsm/renderers/nodes/display/ColorSpaceNode.js +9 -46
  74. package/examples/jsm/renderers/nodes/inputs/TextureNode.js +5 -10
  75. package/examples/jsm/renderers/nodes/math/MathNode.js +2 -1
  76. package/examples/jsm/renderers/webgpu/WebGPUObjects.js +1 -1
  77. package/examples/jsm/renderers/webgpu/WebGPURenderer.js +3 -5
  78. package/examples/jsm/renderers/webgpu/WebGPUTextures.js +1 -2
  79. package/examples/jsm/renderers/webgpu/nodes/WebGPUNodeBuilder.js +10 -4
  80. package/examples/jsm/shaders/DigitalGlitch.js +3 -3
  81. package/examples/jsm/shaders/MMDToonShader.js +0 -1
  82. package/examples/jsm/shaders/SSAOShader.js +1 -1
  83. package/examples/jsm/utils/LDrawUtils.js +195 -0
  84. package/examples/jsm/webxr/VRButton.js +24 -0
  85. package/examples/jsm/webxr/XREstimatedLight.js +2 -0
  86. package/package.json +4 -1
  87. package/src/animation/AnimationMixer.js +1 -0
  88. package/src/cameras/CubeCamera.js +2 -0
  89. package/src/constants.js +3 -16
  90. package/src/extras/ImageUtils.js +63 -0
  91. package/src/extras/PMREMGenerator.js +32 -82
  92. package/src/loaders/MaterialLoader.js +1 -1
  93. package/src/materials/Material.js +4 -4
  94. package/src/math/Color.js +1 -1
  95. package/src/math/Quaternion.js +1 -1
  96. package/src/renderers/WebGLCubeRenderTarget.js +0 -2
  97. package/src/renderers/WebGLRenderer.js +1 -67
  98. package/src/renderers/shaders/ShaderChunk/emissivemap_fragment.glsl.js +0 -2
  99. package/src/renderers/shaders/ShaderChunk/encodings_pars_fragment.glsl.js +0 -4
  100. package/src/renderers/shaders/ShaderChunk/envmap_fragment.glsl.js +0 -2
  101. package/src/renderers/shaders/ShaderChunk/lightmap_fragment.glsl.js +1 -1
  102. package/src/renderers/shaders/ShaderChunk/lights_fragment_maps.glsl.js +1 -1
  103. package/src/renderers/shaders/ShaderChunk/lights_physical_fragment.glsl.js +2 -2
  104. package/src/renderers/shaders/ShaderChunk/map_fragment.glsl.js +10 -3
  105. package/src/renderers/shaders/ShaderChunk/map_particle_fragment.glsl.js +1 -2
  106. package/src/renderers/shaders/ShaderChunk/transmission_pars_fragment.glsl.js +8 -7
  107. package/src/renderers/shaders/ShaderLib/background.glsl.js +1 -3
  108. package/src/renderers/shaders/ShaderLib/equirect.glsl.js +1 -3
  109. package/src/renderers/shaders/ShaderLib/meshbasic.glsl.js +1 -1
  110. package/src/renderers/shaders/ShaderLib/meshmatcap.glsl.js +1 -2
  111. package/src/renderers/shaders/ShaderLib.js +2 -2
  112. package/src/renderers/webgl/WebGLCubeMaps.js +0 -4
  113. package/src/renderers/webgl/WebGLCubeUVMaps.js +29 -16
  114. package/src/renderers/webgl/WebGLMorphtargets.js +12 -0
  115. package/src/renderers/webgl/WebGLProgram.js +4 -16
  116. package/src/renderers/webgl/WebGLPrograms.js +8 -47
  117. package/src/renderers/webgl/WebGLState.js +81 -0
  118. package/src/renderers/webgl/WebGLTextures.js +75 -22
  119. package/src/renderers/webgl/WebGLUtils.js +86 -28
  120. package/src/renderers/webxr/WebXRManager.js +3 -3
  121. package/src/textures/Texture.js +2 -1
  122. package/src/textures/VideoTexture.js +1 -3
  123. package/examples/js/WebGL.js +0 -90
  124. package/examples/js/utils/RoughnessMipmapper.js +0 -268
  125. package/examples/jsm/utils/RoughnessMipmapper.js +0 -288
@@ -142,16 +142,45 @@
142
142
 
143
143
  }
144
144
 
145
- function smoothNormals( faces, lineSegments ) {
145
+ function generateFaceNormals( faces ) {
146
+
147
+ for ( let i = 0, l = faces.length; i < l; i ++ ) {
148
+
149
+ const face = faces[ i ];
150
+ const vertices = face.vertices;
151
+ const v0 = vertices[ 0 ];
152
+ const v1 = vertices[ 1 ];
153
+ const v2 = vertices[ 2 ];
154
+
155
+ _tempVec0.subVectors( v1, v0 );
156
+
157
+ _tempVec1.subVectors( v2, v1 );
158
+
159
+ face.faceNormal = new THREE.Vector3().crossVectors( _tempVec0, _tempVec1 ).normalize();
160
+
161
+ }
162
+
163
+ }
164
+
165
+ const _ray = new THREE.Ray();
166
+
167
+ function smoothNormals( faces, lineSegments, checkSubSegments = false ) {
168
+
169
+ // NOTE: 1e2 is pretty coarse but was chosen to quantize the resulting value because
170
+ // it allows edges to be smoothed as expected (see minifig arms).
171
+ // --
172
+ // And the vector values are initialize multiplied by 1 + 1e-10 to account for floating
173
+ // point errors on vertices along quantization boundaries. Ie after matrix multiplication
174
+ // vertices that should be merged might be set to "1.7" and "1.6999..." meaning they won't
175
+ // get merged. This added epsilon attempts to push these error values to the same quantized
176
+ // value for the sake of hashing. See "AT-ST mini" dishes. See mrdoob/three#23169.
177
+ const hashMultiplier = ( 1 + 1e-10 ) * 1e2;
146
178
 
147
179
  function hashVertex( v ) {
148
180
 
149
- // NOTE: 1e2 is pretty coarse but was chosen because it allows edges
150
- // to be smoothed as expected (see minifig arms). The errors between edges
151
- // could be due to matrix multiplication.
152
- const x = ~ ~ ( v.x * 1e2 );
153
- const y = ~ ~ ( v.y * 1e2 );
154
- const z = ~ ~ ( v.z * 1e2 );
181
+ const x = ~ ~ ( v.x * hashMultiplier );
182
+ const y = ~ ~ ( v.y * hashMultiplier );
183
+ const z = ~ ~ ( v.z * hashMultiplier );
155
184
  return `${x},${y},${z}`;
156
185
 
157
186
  }
@@ -160,9 +189,27 @@
160
189
 
161
190
  return `${hashVertex( v0 )}_${hashVertex( v1 )}`;
162
191
 
192
+ } // converts the two vertices to a ray with a normalized direction and origin of 0, 0, 0 projected
193
+ // onto the original line.
194
+
195
+
196
+ function toNormalizedRay( v0, v1, targetRay ) {
197
+
198
+ targetRay.direction.subVectors( v1, v0 ).normalize();
199
+ const scalar = v0.dot( targetRay.direction );
200
+ targetRay.origin.copy( v0 ).addScaledVector( targetRay.direction, - scalar );
201
+ return targetRay;
202
+
203
+ }
204
+
205
+ function hashRay( ray ) {
206
+
207
+ return hashEdge( ray.origin, ray.direction );
208
+
163
209
  }
164
210
 
165
211
  const hardEdges = new Set();
212
+ const hardEdgeRays = new Map();
166
213
  const halfEdgeList = {};
167
214
  const normals = []; // Save the list of hard edges by hash
168
215
 
@@ -173,7 +220,43 @@
173
220
  const v0 = vertices[ 0 ];
174
221
  const v1 = vertices[ 1 ];
175
222
  hardEdges.add( hashEdge( v0, v1 ) );
176
- hardEdges.add( hashEdge( v1, v0 ) );
223
+ hardEdges.add( hashEdge( v1, v0 ) ); // only generate the hard edge ray map if we're checking subsegments because it's more expensive to check
224
+ // and requires more memory.
225
+
226
+ if ( checkSubSegments ) {
227
+
228
+ // add both ray directions to the map
229
+ const ray = toNormalizedRay( v0, v1, new THREE.Ray() );
230
+ const rh1 = hashRay( ray );
231
+
232
+ if ( ! hardEdgeRays.has( rh1 ) ) {
233
+
234
+ toNormalizedRay( v1, v0, ray );
235
+ const rh2 = hashRay( ray );
236
+ const info = {
237
+ ray,
238
+ distances: []
239
+ };
240
+ hardEdgeRays.set( rh1, info );
241
+ hardEdgeRays.set( rh2, info );
242
+
243
+ } // store both segments ends in min, max order in the distances array to check if a face edge is a
244
+ // subsegment later.
245
+
246
+
247
+ const info = hardEdgeRays.get( rh1 );
248
+ let d0 = info.ray.direction.dot( v0 );
249
+ let d1 = info.ray.direction.dot( v1 );
250
+
251
+ if ( d0 > d1 ) {
252
+
253
+ [ d0, d1 ] = [ d1, d0 ];
254
+
255
+ }
256
+
257
+ info.distances.push( d0, d1 );
258
+
259
+ }
177
260
 
178
261
  } // track the half edges associated with each triangle
179
262
 
@@ -192,7 +275,58 @@
192
275
  const v1 = vertices[ next ];
193
276
  const hash = hashEdge( v0, v1 ); // don't add the triangle if the edge is supposed to be hard
194
277
 
195
- if ( hardEdges.has( hash ) ) continue;
278
+ if ( hardEdges.has( hash ) ) {
279
+
280
+ continue;
281
+
282
+ } // if checking subsegments then check to see if this edge lies on a hard edge ray and whether its within any ray bounds
283
+
284
+
285
+ if ( checkSubSegments ) {
286
+
287
+ toNormalizedRay( v0, v1, _ray );
288
+ const rayHash = hashRay( _ray );
289
+
290
+ if ( hardEdgeRays.has( rayHash ) ) {
291
+
292
+ const info = hardEdgeRays.get( rayHash );
293
+ const {
294
+ ray,
295
+ distances
296
+ } = info;
297
+ let d0 = ray.direction.dot( v0 );
298
+ let d1 = ray.direction.dot( v1 );
299
+
300
+ if ( d0 > d1 ) {
301
+
302
+ [ d0, d1 ] = [ d1, d0 ];
303
+
304
+ } // return early if the face edge is found to be a subsegment of a line edge meaning the edge will have "hard" normals
305
+
306
+
307
+ let found = false;
308
+
309
+ for ( let i = 0, l = distances.length; i < l; i += 2 ) {
310
+
311
+ if ( d0 >= distances[ i ] && d1 <= distances[ i + 1 ] ) {
312
+
313
+ found = true;
314
+ break;
315
+
316
+ }
317
+
318
+ }
319
+
320
+ if ( found ) {
321
+
322
+ continue;
323
+
324
+ }
325
+
326
+ }
327
+
328
+ }
329
+
196
330
  const info = {
197
331
  index: index,
198
332
  tri: tri
@@ -433,6 +567,12 @@
433
567
 
434
568
  }
435
569
 
570
+ getVector() {
571
+
572
+ return new THREE.Vector3( parseFloat( this.getToken() ), parseFloat( this.getToken() ), parseFloat( this.getToken() ) );
573
+
574
+ }
575
+
436
576
  getRemainingString() {
437
577
 
438
578
  return this.line.substring( this.currentCharIndex, this.lineLength );
@@ -459,1239 +599,1368 @@
459
599
 
460
600
  }
461
601
 
462
- class LDrawFileCache {
602
+ class LDrawParsedCache {
463
603
 
464
604
  constructor( loader ) {
465
605
 
466
- this.cache = {};
467
606
  this.loader = loader;
607
+ this.cache = {};
468
608
 
469
609
  }
470
610
 
471
- setData( key, contents ) {
611
+ cloneResult( original ) {
472
612
 
473
- this.cache[ key.toLowerCase() ] = contents;
613
+ const result = {}; // vertices are transformed and normals computed before being converted to geometry
614
+ // so these pieces must be cloned.
474
615
 
475
- }
616
+ result.faces = original.faces.map( face => {
476
617
 
477
- async loadData( fileName ) {
618
+ return {
619
+ colorCode: face.colorCode,
620
+ material: face.material,
621
+ vertices: face.vertices.map( v => v.clone() ),
622
+ normals: face.normals.map( () => null ),
623
+ faceNormal: null
624
+ };
478
625
 
479
- const key = fileName.toLowerCase();
626
+ } );
627
+ result.conditionalSegments = original.conditionalSegments.map( face => {
480
628
 
481
- if ( key in this.cache ) {
629
+ return {
630
+ colorCode: face.colorCode,
631
+ material: face.material,
632
+ vertices: face.vertices.map( v => v.clone() ),
633
+ controlPoints: face.controlPoints.map( v => v.clone() )
634
+ };
482
635
 
483
- return this.cache[ key ];
636
+ } );
637
+ result.lineSegments = original.lineSegments.map( face => {
484
638
 
485
- }
639
+ return {
640
+ colorCode: face.colorCode,
641
+ material: face.material,
642
+ vertices: face.vertices.map( v => v.clone() )
643
+ };
486
644
 
487
- this.cache[ fileName ] = new Promise( async ( resolve, reject ) => {
645
+ } ); // none if this is subsequently modified
488
646
 
489
- let triedLowerCase = false;
490
- let locationState = FILE_LOCATION_AS_IS;
647
+ result.type = original.type;
648
+ result.category = original.category;
649
+ result.keywords = original.keywords;
650
+ result.subobjects = original.subobjects;
651
+ result.totalFaces = original.totalFaces;
652
+ result.startingConstructionStep = original.startingConstructionStep;
653
+ result.materials = original.materials;
654
+ return result;
491
655
 
492
- while ( locationState !== FILE_LOCATION_NOT_FOUND ) {
656
+ }
493
657
 
494
- let subobjectURL = fileName;
658
+ async fetchData( fileName ) {
495
659
 
496
- switch ( locationState ) {
660
+ let triedLowerCase = false;
661
+ let locationState = FILE_LOCATION_AS_IS;
497
662
 
498
- case FILE_LOCATION_AS_IS:
499
- locationState = locationState + 1;
500
- break;
663
+ while ( locationState !== FILE_LOCATION_NOT_FOUND ) {
501
664
 
502
- case FILE_LOCATION_TRY_PARTS:
503
- subobjectURL = 'parts/' + subobjectURL;
504
- locationState = locationState + 1;
505
- break;
665
+ let subobjectURL = fileName;
506
666
 
507
- case FILE_LOCATION_TRY_P:
508
- subobjectURL = 'p/' + subobjectURL;
509
- locationState = locationState + 1;
510
- break;
667
+ switch ( locationState ) {
511
668
 
512
- case FILE_LOCATION_TRY_MODELS:
513
- subobjectURL = 'models/' + subobjectURL;
514
- locationState = locationState + 1;
515
- break;
669
+ case FILE_LOCATION_AS_IS:
670
+ locationState = locationState + 1;
671
+ break;
516
672
 
517
- case FILE_LOCATION_TRY_RELATIVE:
518
- subobjectURL = fileName.substring( 0, fileName.lastIndexOf( '/' ) + 1 ) + subobjectURL;
519
- locationState = locationState + 1;
520
- break;
673
+ case FILE_LOCATION_TRY_PARTS:
674
+ subobjectURL = 'parts/' + subobjectURL;
675
+ locationState = locationState + 1;
676
+ break;
521
677
 
522
- case FILE_LOCATION_TRY_ABSOLUTE:
523
- if ( triedLowerCase ) {
678
+ case FILE_LOCATION_TRY_P:
679
+ subobjectURL = 'p/' + subobjectURL;
680
+ locationState = locationState + 1;
681
+ break;
524
682
 
525
- // Try absolute path
526
- locationState = FILE_LOCATION_NOT_FOUND;
683
+ case FILE_LOCATION_TRY_MODELS:
684
+ subobjectURL = 'models/' + subobjectURL;
685
+ locationState = locationState + 1;
686
+ break;
527
687
 
528
- } else {
688
+ case FILE_LOCATION_TRY_RELATIVE:
689
+ subobjectURL = fileName.substring( 0, fileName.lastIndexOf( '/' ) + 1 ) + subobjectURL;
690
+ locationState = locationState + 1;
691
+ break;
529
692
 
530
- // Next attempt is lower case
531
- fileName = fileName.toLowerCase();
532
- subobjectURL = fileName;
533
- triedLowerCase = true;
534
- locationState = FILE_LOCATION_AS_IS;
693
+ case FILE_LOCATION_TRY_ABSOLUTE:
694
+ if ( triedLowerCase ) {
535
695
 
536
- }
696
+ // Try absolute path
697
+ locationState = FILE_LOCATION_NOT_FOUND;
537
698
 
538
- break;
699
+ } else {
539
700
 
540
- }
701
+ // Next attempt is lower case
702
+ fileName = fileName.toLowerCase();
703
+ subobjectURL = fileName;
704
+ triedLowerCase = true;
705
+ locationState = FILE_LOCATION_AS_IS;
706
+
707
+ }
541
708
 
542
- const loader = this.loader;
543
- const fileLoader = new THREE.FileLoader( loader.manager );
544
- fileLoader.setPath( loader.partsLibraryPath );
545
- fileLoader.setRequestHeader( loader.requestHeader );
546
- fileLoader.setWithCredentials( loader.withCredentials );
709
+ break;
710
+
711
+ }
547
712
 
548
- try {
713
+ const loader = this.loader;
714
+ const fileLoader = new THREE.FileLoader( loader.manager );
715
+ fileLoader.setPath( loader.partsLibraryPath );
716
+ fileLoader.setRequestHeader( loader.requestHeader );
717
+ fileLoader.setWithCredentials( loader.withCredentials );
549
718
 
550
- const text = await fileLoader.loadAsync( subobjectURL );
551
- this.setData( fileName, text );
552
- resolve( text );
553
- return;
719
+ try {
554
720
 
555
- } catch {
721
+ const text = await fileLoader.loadAsync( subobjectURL );
722
+ return text;
556
723
 
557
- continue;
724
+ } catch {
558
725
 
559
- }
726
+ continue;
560
727
 
561
728
  }
562
729
 
563
- reject();
730
+ }
564
731
 
565
- } );
566
- return this.cache[ fileName ];
732
+ throw new Error( 'LDrawLoader: Subobject "' + fileName + '" could not be loaded.' );
567
733
 
568
734
  }
569
735
 
570
- }
571
-
572
- function sortByMaterial( a, b ) {
736
+ parse( text ) {
573
737
 
574
- if ( a.colourCode === b.colourCode ) {
738
+ const loader = this.loader; // final results
575
739
 
576
- return 0;
740
+ const faces = [];
741
+ const lineSegments = [];
742
+ const conditionalSegments = [];
743
+ const subobjects = [];
744
+ const materials = {};
577
745
 
578
- }
746
+ const getLocalMaterial = colorCode => {
579
747
 
580
- if ( a.colourCode < b.colourCode ) {
748
+ return colorCode in materials ? materials[ colorCode ] : null;
581
749
 
582
- return - 1;
750
+ };
583
751
 
584
- }
752
+ let type = 'Model';
753
+ let category = null;
754
+ let keywords = null;
755
+ let totalFaces = 0; // split into lines
585
756
 
586
- return 1;
757
+ if ( text.indexOf( '\r\n' ) !== - 1 ) {
587
758
 
588
- }
759
+ // This is faster than String.split with regex that splits on both
760
+ text = text.replace( /\r\n/g, '\n' );
589
761
 
590
- function createObject( elements, elementSize, isConditionalSegments = false, totalElements = null ) {
762
+ }
591
763
 
592
- // Creates a THREE.LineSegments (elementSize = 2) or a THREE.Mesh (elementSize = 3 )
593
- // With per face / segment material, implemented with mesh groups and materials array
594
- // Sort the faces or line segments by colour code to make later the mesh groups
595
- elements.sort( sortByMaterial );
764
+ const lines = text.split( '\n' );
765
+ const numLines = lines.length;
766
+ let parsingEmbeddedFiles = false;
767
+ let currentEmbeddedFileName = null;
768
+ let currentEmbeddedText = null;
769
+ let bfcCertified = false;
770
+ let bfcCCW = true;
771
+ let bfcInverted = false;
772
+ let bfcCull = true;
773
+ let startingConstructionStep = false; // Parse all line commands
596
774
 
597
- if ( totalElements === null ) {
775
+ for ( let lineIndex = 0; lineIndex < numLines; lineIndex ++ ) {
598
776
 
599
- totalElements = elements.length;
777
+ const line = lines[ lineIndex ];
778
+ if ( line.length === 0 ) continue;
600
779
 
601
- }
780
+ if ( parsingEmbeddedFiles ) {
602
781
 
603
- const positions = new Float32Array( elementSize * totalElements * 3 );
604
- const normals = elementSize === 3 ? new Float32Array( elementSize * totalElements * 3 ) : null;
605
- const materials = [];
606
- const quadArray = new Array( 6 );
607
- const bufferGeometry = new THREE.BufferGeometry();
608
- let prevMaterial = null;
609
- let index0 = 0;
610
- let numGroupVerts = 0;
611
- let offset = 0;
782
+ if ( line.startsWith( '0 FILE ' ) ) {
612
783
 
613
- for ( let iElem = 0, nElem = elements.length; iElem < nElem; iElem ++ ) {
784
+ // Save previous embedded file in the cache
785
+ this.setData( currentEmbeddedFileName, currentEmbeddedText ); // New embedded text file
614
786
 
615
- const elem = elements[ iElem ];
616
- let vertices = elem.vertices;
787
+ currentEmbeddedFileName = line.substring( 7 );
788
+ currentEmbeddedText = '';
617
789
 
618
- if ( vertices.length === 4 ) {
790
+ } else {
619
791
 
620
- quadArray[ 0 ] = vertices[ 0 ];
621
- quadArray[ 1 ] = vertices[ 1 ];
622
- quadArray[ 2 ] = vertices[ 2 ];
623
- quadArray[ 3 ] = vertices[ 0 ];
624
- quadArray[ 4 ] = vertices[ 2 ];
625
- quadArray[ 5 ] = vertices[ 3 ];
626
- vertices = quadArray;
792
+ currentEmbeddedText += line + '\n';
627
793
 
628
- }
794
+ }
629
795
 
630
- for ( let j = 0, l = vertices.length; j < l; j ++ ) {
796
+ continue;
631
797
 
632
- const v = vertices[ j ];
633
- const index = offset + j * 3;
634
- positions[ index + 0 ] = v.x;
635
- positions[ index + 1 ] = v.y;
636
- positions[ index + 2 ] = v.z;
798
+ }
637
799
 
638
- }
800
+ const lp = new LineParser( line, lineIndex + 1 );
801
+ lp.seekNonSpace();
639
802
 
640
- if ( elementSize === 3 ) {
803
+ if ( lp.isAtTheEnd() ) {
641
804
 
642
- let elemNormals = elem.normals;
805
+ // Empty line
806
+ continue;
643
807
 
644
- if ( elemNormals.length === 4 ) {
808
+ } // Parse the line type
645
809
 
646
- quadArray[ 0 ] = elemNormals[ 0 ];
647
- quadArray[ 1 ] = elemNormals[ 1 ];
648
- quadArray[ 2 ] = elemNormals[ 2 ];
649
- quadArray[ 3 ] = elemNormals[ 0 ];
650
- quadArray[ 4 ] = elemNormals[ 2 ];
651
- quadArray[ 5 ] = elemNormals[ 3 ];
652
- elemNormals = quadArray;
653
810
 
654
- }
811
+ const lineType = lp.getToken();
812
+ let material;
813
+ let colorCode;
814
+ let segment;
815
+ let ccw;
816
+ let doubleSided;
817
+ let v0, v1, v2, v3, c0, c1;
655
818
 
656
- for ( let j = 0, l = elemNormals.length; j < l; j ++ ) {
819
+ switch ( lineType ) {
657
820
 
658
- let n = elem.faceNormal;
821
+ // Line type 0: Comment or META
822
+ case '0':
823
+ // Parse meta directive
824
+ const meta = lp.getToken();
659
825
 
660
- if ( elemNormals[ j ] ) {
826
+ if ( meta ) {
661
827
 
662
- n = elemNormals[ j ].norm;
828
+ switch ( meta ) {
663
829
 
664
- }
830
+ case '!LDRAW_ORG':
831
+ type = lp.getToken();
832
+ break;
665
833
 
666
- const index = offset + j * 3;
667
- normals[ index + 0 ] = n.x;
668
- normals[ index + 1 ] = n.y;
669
- normals[ index + 2 ] = n.z;
834
+ case '!COLOUR':
835
+ material = loader.parseColorMetaDirective( lp );
670
836
 
671
- }
837
+ if ( material ) {
672
838
 
673
- }
839
+ materials[ material.userData.code ] = material;
674
840
 
675
- if ( prevMaterial !== elem.material ) {
841
+ } else {
676
842
 
677
- if ( prevMaterial !== null ) {
843
+ console.warn( 'LDrawLoader: Error parsing material' + lp.getLineNumberString() );
678
844
 
679
- bufferGeometry.addGroup( index0, numGroupVerts, materials.length - 1 );
845
+ }
680
846
 
681
- }
847
+ break;
682
848
 
683
- materials.push( elem.material );
684
- prevMaterial = elem.material;
685
- index0 = offset / 3;
686
- numGroupVerts = vertices.length;
849
+ case '!CATEGORY':
850
+ category = lp.getToken();
851
+ break;
687
852
 
688
- } else {
853
+ case '!KEYWORDS':
854
+ const newKeywords = lp.getRemainingString().split( ',' );
689
855
 
690
- numGroupVerts += vertices.length;
856
+ if ( newKeywords.length > 0 ) {
691
857
 
692
- }
858
+ if ( ! keywords ) {
693
859
 
694
- offset += 3 * vertices.length;
860
+ keywords = [];
695
861
 
696
- }
862
+ }
697
863
 
698
- if ( numGroupVerts > 0 ) {
864
+ newKeywords.forEach( function ( keyword ) {
699
865
 
700
- bufferGeometry.addGroup( index0, Infinity, materials.length - 1 );
866
+ keywords.push( keyword.trim() );
701
867
 
702
- }
868
+ } );
703
869
 
704
- bufferGeometry.setAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );
870
+ }
705
871
 
706
- if ( normals !== null ) {
872
+ break;
707
873
 
708
- bufferGeometry.setAttribute( 'normal', new THREE.BufferAttribute( normals, 3 ) );
874
+ case 'FILE':
875
+ if ( lineIndex > 0 ) {
709
876
 
710
- }
877
+ // Start embedded text files parsing
878
+ parsingEmbeddedFiles = true;
879
+ currentEmbeddedFileName = lp.getRemainingString();
880
+ currentEmbeddedText = '';
881
+ bfcCertified = false;
882
+ bfcCCW = true;
711
883
 
712
- let object3d = null;
884
+ }
713
885
 
714
- if ( elementSize === 2 ) {
886
+ break;
715
887
 
716
- object3d = new THREE.LineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );
888
+ case 'BFC':
889
+ // Changes to the backface culling state
890
+ while ( ! lp.isAtTheEnd() ) {
717
891
 
718
- } else if ( elementSize === 3 ) {
892
+ const token = lp.getToken();
719
893
 
720
- object3d = new THREE.Mesh( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );
894
+ switch ( token ) {
721
895
 
722
- }
896
+ case 'CERTIFY':
897
+ case 'NOCERTIFY':
898
+ bfcCertified = token === 'CERTIFY';
899
+ bfcCCW = true;
900
+ break;
723
901
 
724
- if ( isConditionalSegments ) {
902
+ case 'CW':
903
+ case 'CCW':
904
+ bfcCCW = token === 'CCW';
905
+ break;
725
906
 
726
- object3d.isConditionalLine = true;
727
- const controlArray0 = new Float32Array( elements.length * 3 * 2 );
728
- const controlArray1 = new Float32Array( elements.length * 3 * 2 );
729
- const directionArray = new Float32Array( elements.length * 3 * 2 );
907
+ case 'INVERTNEXT':
908
+ bfcInverted = true;
909
+ break;
730
910
 
731
- for ( let i = 0, l = elements.length; i < l; i ++ ) {
911
+ case 'CLIP':
912
+ case 'NOCLIP':
913
+ bfcCull = token === 'CLIP';
914
+ break;
732
915
 
733
- const os = elements[ i ];
734
- const vertices = os.vertices;
735
- const controlPoints = os.controlPoints;
736
- const c0 = controlPoints[ 0 ];
737
- const c1 = controlPoints[ 1 ];
738
- const v0 = vertices[ 0 ];
739
- const v1 = vertices[ 1 ];
740
- const index = i * 3 * 2;
741
- controlArray0[ index + 0 ] = c0.x;
742
- controlArray0[ index + 1 ] = c0.y;
743
- controlArray0[ index + 2 ] = c0.z;
744
- controlArray0[ index + 3 ] = c0.x;
745
- controlArray0[ index + 4 ] = c0.y;
746
- controlArray0[ index + 5 ] = c0.z;
747
- controlArray1[ index + 0 ] = c1.x;
748
- controlArray1[ index + 1 ] = c1.y;
749
- controlArray1[ index + 2 ] = c1.z;
750
- controlArray1[ index + 3 ] = c1.x;
751
- controlArray1[ index + 4 ] = c1.y;
752
- controlArray1[ index + 5 ] = c1.z;
753
- directionArray[ index + 0 ] = v1.x - v0.x;
754
- directionArray[ index + 1 ] = v1.y - v0.y;
755
- directionArray[ index + 2 ] = v1.z - v0.z;
756
- directionArray[ index + 3 ] = v1.x - v0.x;
757
- directionArray[ index + 4 ] = v1.y - v0.y;
758
- directionArray[ index + 5 ] = v1.z - v0.z;
916
+ default:
917
+ console.warn( 'THREE.LDrawLoader: BFC directive "' + token + '" is unknown.' );
918
+ break;
759
919
 
760
- }
920
+ }
761
921
 
762
- bufferGeometry.setAttribute( 'control0', new THREE.BufferAttribute( controlArray0, 3, false ) );
763
- bufferGeometry.setAttribute( 'control1', new THREE.BufferAttribute( controlArray1, 3, false ) );
764
- bufferGeometry.setAttribute( 'direction', new THREE.BufferAttribute( directionArray, 3, false ) );
922
+ }
765
923
 
766
- }
924
+ break;
767
925
 
768
- return object3d;
926
+ case 'STEP':
927
+ startingConstructionStep = true;
928
+ break;
769
929
 
770
- } //
930
+ default:
931
+ // Other meta directives are not implemented
932
+ break;
771
933
 
934
+ }
772
935
 
773
- class LDrawLoader extends THREE.Loader {
936
+ }
774
937
 
775
- constructor( manager ) {
938
+ break;
939
+ // Line type 1: Sub-object file
776
940
 
777
- super( manager ); // Array of THREE.Material
941
+ case '1':
942
+ colorCode = lp.getToken();
943
+ material = getLocalMaterial( colorCode );
944
+ const posX = parseFloat( lp.getToken() );
945
+ const posY = parseFloat( lp.getToken() );
946
+ const posZ = parseFloat( lp.getToken() );
947
+ const m0 = parseFloat( lp.getToken() );
948
+ const m1 = parseFloat( lp.getToken() );
949
+ const m2 = parseFloat( lp.getToken() );
950
+ const m3 = parseFloat( lp.getToken() );
951
+ const m4 = parseFloat( lp.getToken() );
952
+ const m5 = parseFloat( lp.getToken() );
953
+ const m6 = parseFloat( lp.getToken() );
954
+ const m7 = parseFloat( lp.getToken() );
955
+ const m8 = parseFloat( lp.getToken() );
956
+ const matrix = new THREE.Matrix4().set( m0, m1, m2, posX, m3, m4, m5, posY, m6, m7, m8, posZ, 0, 0, 0, 1 );
957
+ let fileName = lp.getRemainingString().trim().replace( /\\/g, '/' );
778
958
 
779
- this.materials = []; // Not using THREE.Cache here because it returns the previous HTML error response instead of calling onError()
780
- // This also allows to handle the embedded text files ("0 FILE" lines)
959
+ if ( loader.fileMap[ fileName ] ) {
781
960
 
782
- this.cache = new LDrawFileCache( this ); // This object is a map from file names to paths. It agilizes the paths search. If it is not set then files will be searched by trial and error.
961
+ // Found the subobject path in the preloaded file path map
962
+ fileName = loader.fileMap[ fileName ];
783
963
 
784
- this.fileMap = null;
785
- this.rootParseScope = this.newParseScopeLevel(); // Add default main triangle and line edge materials (used in pieces that can be coloured with a main color)
964
+ } else {
786
965
 
787
- this.setMaterials( [ this.parseColourMetaDirective( new LineParser( 'Main_Colour CODE 16 VALUE #FF8080 EDGE #333333' ) ), this.parseColourMetaDirective( new LineParser( 'Edge_Colour CODE 24 VALUE #A0A0A0 EDGE #333333' ) ) ] ); // If this flag is set to true, each subobject will be a Object.
788
- // If not (the default), only one object which contains all the merged primitives will be created.
966
+ // Standardized subfolders
967
+ if ( fileName.startsWith( 's/' ) ) {
789
968
 
790
- this.separateObjects = false; // If this flag is set to true the vertex normals will be smoothed.
969
+ fileName = 'parts/' + fileName;
791
970
 
792
- this.smoothNormals = true; // The path to load parts from the LDraw parts library from.
971
+ } else if ( fileName.startsWith( '48/' ) ) {
793
972
 
794
- this.partsLibraryPath = '';
973
+ fileName = 'p/' + fileName;
795
974
 
796
- }
975
+ }
797
976
 
798
- setPartsLibraryPath( path ) {
977
+ }
799
978
 
800
- this.partsLibraryPath = path;
801
- return this;
979
+ subobjects.push( {
980
+ material: material,
981
+ colorCode: colorCode,
982
+ matrix: matrix,
983
+ fileName: fileName,
984
+ inverted: bfcInverted,
985
+ startingConstructionStep: startingConstructionStep
986
+ } );
987
+ bfcInverted = false;
988
+ break;
989
+ // Line type 2: Line segment
802
990
 
803
- }
991
+ case '2':
992
+ colorCode = lp.getToken();
993
+ material = getLocalMaterial( colorCode );
994
+ v0 = lp.getVector();
995
+ v1 = lp.getVector();
996
+ segment = {
997
+ material: material,
998
+ colorCode: colorCode,
999
+ vertices: [ v0, v1 ]
1000
+ };
1001
+ lineSegments.push( segment );
1002
+ break;
1003
+ // Line type 5: Conditional Line segment
804
1004
 
805
- async preloadMaterials( url ) {
1005
+ case '5':
1006
+ colorCode = lp.getToken();
1007
+ material = getLocalMaterial( colorCode );
1008
+ v0 = lp.getVector();
1009
+ v1 = lp.getVector();
1010
+ c0 = lp.getVector();
1011
+ c1 = lp.getVector();
1012
+ segment = {
1013
+ material: material,
1014
+ colorCode: colorCode,
1015
+ vertices: [ v0, v1 ],
1016
+ controlPoints: [ c0, c1 ]
1017
+ };
1018
+ conditionalSegments.push( segment );
1019
+ break;
1020
+ // Line type 3: Triangle
806
1021
 
807
- const fileLoader = new THREE.FileLoader( this.manager );
808
- fileLoader.setPath( this.path );
809
- fileLoader.setRequestHeader( this.requestHeader );
810
- fileLoader.setWithCredentials( this.withCredentials );
811
- const text = await fileLoader.loadAsync( url );
812
- const colorLineRegex = /^0 !COLOUR/;
813
- const lines = text.split( /[\n\r]/g );
814
- const materials = [];
1022
+ case '3':
1023
+ colorCode = lp.getToken();
1024
+ material = getLocalMaterial( colorCode );
1025
+ ccw = bfcCCW;
1026
+ doubleSided = ! bfcCertified || ! bfcCull;
815
1027
 
816
- for ( let i = 0, l = lines.length; i < l; i ++ ) {
1028
+ if ( ccw === true ) {
817
1029
 
818
- const line = lines[ i ];
1030
+ v0 = lp.getVector();
1031
+ v1 = lp.getVector();
1032
+ v2 = lp.getVector();
819
1033
 
820
- if ( colorLineRegex.test( line ) ) {
1034
+ } else {
821
1035
 
822
- const directive = line.replace( colorLineRegex, '' );
823
- const material = this.parseColourMetaDirective( new LineParser( directive ) );
824
- materials.push( material );
1036
+ v2 = lp.getVector();
1037
+ v1 = lp.getVector();
1038
+ v0 = lp.getVector();
1039
+
1040
+ }
1041
+
1042
+ faces.push( {
1043
+ material: material,
1044
+ colorCode: colorCode,
1045
+ faceNormal: null,
1046
+ vertices: [ v0, v1, v2 ],
1047
+ normals: [ null, null, null ]
1048
+ } );
1049
+ totalFaces ++;
1050
+
1051
+ if ( doubleSided === true ) {
1052
+
1053
+ faces.push( {
1054
+ material: material,
1055
+ colorCode: colorCode,
1056
+ faceNormal: null,
1057
+ vertices: [ v2, v1, v0 ],
1058
+ normals: [ null, null, null ]
1059
+ } );
1060
+ totalFaces ++;
1061
+
1062
+ }
1063
+
1064
+ break;
1065
+ // Line type 4: Quadrilateral
1066
+
1067
+ case '4':
1068
+ colorCode = lp.getToken();
1069
+ material = getLocalMaterial( colorCode );
1070
+ ccw = bfcCCW;
1071
+ doubleSided = ! bfcCertified || ! bfcCull;
1072
+
1073
+ if ( ccw === true ) {
1074
+
1075
+ v0 = lp.getVector();
1076
+ v1 = lp.getVector();
1077
+ v2 = lp.getVector();
1078
+ v3 = lp.getVector();
1079
+
1080
+ } else {
1081
+
1082
+ v3 = lp.getVector();
1083
+ v2 = lp.getVector();
1084
+ v1 = lp.getVector();
1085
+ v0 = lp.getVector();
1086
+
1087
+ } // specifically place the triangle diagonal in the v0 and v1 slots so we can
1088
+ // account for the doubling of vertices later when smoothing normals.
1089
+
1090
+
1091
+ faces.push( {
1092
+ material: material,
1093
+ colorCode: colorCode,
1094
+ faceNormal: null,
1095
+ vertices: [ v0, v1, v2, v3 ],
1096
+ normals: [ null, null, null, null ]
1097
+ } );
1098
+ totalFaces += 2;
1099
+
1100
+ if ( doubleSided === true ) {
1101
+
1102
+ faces.push( {
1103
+ material: material,
1104
+ colorCode: colorCode,
1105
+ faceNormal: null,
1106
+ vertices: [ v3, v2, v1, v0 ],
1107
+ normals: [ null, null, null, null ]
1108
+ } );
1109
+ totalFaces += 2;
1110
+
1111
+ }
1112
+
1113
+ break;
1114
+
1115
+ default:
1116
+ throw new Error( 'LDrawLoader: Unknown line type "' + lineType + '"' + lp.getLineNumberString() + '.' );
825
1117
 
826
1118
  }
827
1119
 
828
1120
  }
829
1121
 
830
- this.setMaterials( materials );
1122
+ if ( parsingEmbeddedFiles ) {
1123
+
1124
+ this.setData( currentEmbeddedFileName, currentEmbeddedText );
1125
+
1126
+ }
1127
+
1128
+ return {
1129
+ faces,
1130
+ conditionalSegments,
1131
+ lineSegments,
1132
+ type,
1133
+ category,
1134
+ keywords,
1135
+ subobjects,
1136
+ totalFaces,
1137
+ startingConstructionStep,
1138
+ materials
1139
+ };
831
1140
 
832
1141
  }
833
1142
 
834
- load( url, onLoad, onProgress, onError ) {
1143
+ loadData( fileName ) {
1144
+
1145
+ const key = fileName.toLowerCase();
835
1146
 
836
- if ( ! this.fileMap ) {
1147
+ if ( ! ( key in this.cache ) ) {
837
1148
 
838
- this.fileMap = {};
1149
+ this.cache[ key ] = this.fetchData( fileName ).then( text => {
1150
+
1151
+ return this.parse( text );
1152
+
1153
+ } );
839
1154
 
840
1155
  }
841
1156
 
842
- const fileLoader = new THREE.FileLoader( this.manager );
843
- fileLoader.setPath( this.path );
844
- fileLoader.setRequestHeader( this.requestHeader );
845
- fileLoader.setWithCredentials( this.withCredentials );
846
- fileLoader.load( url, text => {
1157
+ return this.cache[ key ].then( result => {
847
1158
 
848
- this.processObject( text, null, url, this.rootParseScope ).then( function ( result ) {
1159
+ return this.cloneResult( result );
849
1160
 
850
- onLoad( result.groupObject );
1161
+ } );
851
1162
 
852
- } );
1163
+ }
853
1164
 
854
- }, onProgress, onError );
1165
+ setData( fileName, text ) {
1166
+
1167
+ const key = fileName.toLowerCase();
1168
+ this.cache[ key ] = Promise.resolve( this.parse( text ) );
855
1169
 
856
1170
  }
857
1171
 
858
- parse( text, path, onLoad ) {
1172
+ }
859
1173
 
860
- // Async parse. This function calls onParse with the parsed THREE.Object3D as parameter
861
- this.processObject( text, null, path, this.rootParseScope ).then( function ( result ) {
1174
+ function sortByMaterial( a, b ) {
862
1175
 
863
- onLoad( result.groupObject );
1176
+ if ( a.colorCode === b.colorCode ) {
864
1177
 
865
- } );
1178
+ return 0;
866
1179
 
867
1180
  }
868
1181
 
869
- setMaterials( materials ) {
1182
+ if ( a.colorCode < b.colorCode ) {
870
1183
 
871
- // Clears parse scopes stack, adds new scope with material library
872
- this.rootParseScope = this.newParseScopeLevel( materials );
873
- this.rootParseScope.isFromParse = false;
874
- this.materials = materials;
875
- return this;
1184
+ return - 1;
876
1185
 
877
1186
  }
878
1187
 
879
- setFileMap( fileMap ) {
1188
+ return 1;
880
1189
 
881
- this.fileMap = fileMap;
882
- return this;
1190
+ }
883
1191
 
884
- }
1192
+ function createObject( elements, elementSize, isConditionalSegments = false, totalElements = null ) {
885
1193
 
886
- newParseScopeLevel( materials = null, parentScope = null ) {
1194
+ // Creates a THREE.LineSegments (elementSize = 2) or a THREE.Mesh (elementSize = 3 )
1195
+ // With per face / segment material, implemented with mesh groups and materials array
1196
+ // Sort the faces or line segments by color code to make later the mesh groups
1197
+ elements.sort( sortByMaterial );
887
1198
 
888
- // Adds a new scope level, assign materials to it and returns it
889
- const matLib = {};
1199
+ if ( totalElements === null ) {
890
1200
 
891
- if ( materials ) {
1201
+ totalElements = elements.length;
892
1202
 
893
- for ( let i = 0, n = materials.length; i < n; i ++ ) {
1203
+ }
894
1204
 
895
- const material = materials[ i ];
896
- matLib[ material.userData.code ] = material;
1205
+ const positions = new Float32Array( elementSize * totalElements * 3 );
1206
+ const normals = elementSize === 3 ? new Float32Array( elementSize * totalElements * 3 ) : null;
1207
+ const materials = [];
1208
+ const quadArray = new Array( 6 );
1209
+ const bufferGeometry = new THREE.BufferGeometry();
1210
+ let prevMaterial = null;
1211
+ let index0 = 0;
1212
+ let numGroupVerts = 0;
1213
+ let offset = 0;
897
1214
 
898
- }
1215
+ for ( let iElem = 0, nElem = elements.length; iElem < nElem; iElem ++ ) {
899
1216
 
900
- }
1217
+ const elem = elements[ iElem ];
1218
+ let vertices = elem.vertices;
901
1219
 
902
- const newParseScope = {
903
- parentScope: parentScope,
904
- lib: matLib,
905
- url: null,
906
- // Subobjects
907
- subobjects: null,
908
- numSubobjects: 0,
909
- subobjectIndex: 0,
910
- inverted: false,
911
- category: null,
912
- keywords: null,
913
- // Current subobject
914
- currentFileName: null,
915
- mainColourCode: parentScope ? parentScope.mainColourCode : '16',
916
- mainEdgeColourCode: parentScope ? parentScope.mainEdgeColourCode : '24',
917
- currentMatrix: new THREE.Matrix4(),
918
- matrix: new THREE.Matrix4(),
919
- type: 'Model',
920
- groupObject: null,
921
- // If false, it is a root material scope previous to parse
922
- isFromParse: true,
923
- faces: [],
924
- lineSegments: [],
925
- conditionalSegments: [],
926
- totalFaces: 0,
927
- // If true, this object is the start of a construction step
928
- startingConstructionStep: false
929
- };
930
- return newParseScope;
1220
+ if ( vertices.length === 4 ) {
1221
+
1222
+ quadArray[ 0 ] = vertices[ 0 ];
1223
+ quadArray[ 1 ] = vertices[ 1 ];
1224
+ quadArray[ 2 ] = vertices[ 2 ];
1225
+ quadArray[ 3 ] = vertices[ 0 ];
1226
+ quadArray[ 4 ] = vertices[ 2 ];
1227
+ quadArray[ 5 ] = vertices[ 3 ];
1228
+ vertices = quadArray;
931
1229
 
932
- }
1230
+ }
933
1231
 
934
- addMaterial( material, parseScope ) {
1232
+ for ( let j = 0, l = vertices.length; j < l; j ++ ) {
935
1233
 
936
- // Adds a material to the material library which is on top of the parse scopes stack. And also to the materials array
937
- const matLib = parseScope.lib;
1234
+ const v = vertices[ j ];
1235
+ const index = offset + j * 3;
1236
+ positions[ index + 0 ] = v.x;
1237
+ positions[ index + 1 ] = v.y;
1238
+ positions[ index + 2 ] = v.z;
938
1239
 
939
- if ( ! matLib[ material.userData.code ] ) {
1240
+ } // create the normals array if this is a set of faces
940
1241
 
941
- this.materials.push( material );
942
1242
 
943
- }
1243
+ if ( elementSize === 3 ) {
944
1244
 
945
- matLib[ material.userData.code ] = material;
946
- return this;
1245
+ if ( ! elem.faceNormal ) {
947
1246
 
948
- }
1247
+ const v0 = vertices[ 0 ];
1248
+ const v1 = vertices[ 1 ];
1249
+ const v2 = vertices[ 2 ];
949
1250
 
950
- getMaterial( colourCode, parseScope = this.rootParseScope ) {
1251
+ _tempVec0.subVectors( v1, v0 );
951
1252
 
952
- // Given a colour code search its material in the parse scopes stack
953
- if ( colourCode.startsWith( '0x2' ) ) {
1253
+ _tempVec1.subVectors( v2, v1 );
954
1254
 
955
- // Special 'direct' material value (RGB colour)
956
- const colour = colourCode.substring( 3 );
957
- return this.parseColourMetaDirective( new LineParser( 'Direct_Color_' + colour + ' CODE -1 VALUE #' + colour + ' EDGE #' + colour + '' ) );
1255
+ elem.faceNormal = new THREE.Vector3().crossVectors( _tempVec0, _tempVec1 ).normalize();
958
1256
 
959
- }
1257
+ }
960
1258
 
961
- while ( parseScope ) {
1259
+ let elemNormals = elem.normals;
962
1260
 
963
- const material = parseScope.lib[ colourCode ];
1261
+ if ( elemNormals.length === 4 ) {
964
1262
 
965
- if ( material ) {
1263
+ quadArray[ 0 ] = elemNormals[ 0 ];
1264
+ quadArray[ 1 ] = elemNormals[ 1 ];
1265
+ quadArray[ 2 ] = elemNormals[ 2 ];
1266
+ quadArray[ 3 ] = elemNormals[ 0 ];
1267
+ quadArray[ 4 ] = elemNormals[ 2 ];
1268
+ quadArray[ 5 ] = elemNormals[ 3 ];
1269
+ elemNormals = quadArray;
966
1270
 
967
- return material;
1271
+ }
968
1272
 
969
- } else {
1273
+ for ( let j = 0, l = elemNormals.length; j < l; j ++ ) {
970
1274
 
971
- parseScope = parseScope.parentScope;
1275
+ // use face normal if a vertex normal is not provided
1276
+ let n = elem.faceNormal;
972
1277
 
973
- }
1278
+ if ( elemNormals[ j ] ) {
974
1279
 
975
- } // Material was not found
1280
+ n = elemNormals[ j ].norm;
976
1281
 
1282
+ }
977
1283
 
978
- return null;
1284
+ const index = offset + j * 3;
1285
+ normals[ index + 0 ] = n.x;
1286
+ normals[ index + 1 ] = n.y;
1287
+ normals[ index + 2 ] = n.z;
979
1288
 
980
- }
1289
+ }
981
1290
 
982
- parseColourMetaDirective( lineParser ) {
1291
+ }
983
1292
 
984
- // Parses a colour definition and returns a THREE.Material
985
- let code = null; // Triangle and line colours
1293
+ if ( prevMaterial !== elem.material ) {
986
1294
 
987
- let colour = 0xFF00FF;
988
- let edgeColour = 0xFF00FF; // Transparency
1295
+ if ( prevMaterial !== null ) {
989
1296
 
990
- let alpha = 1;
991
- let isTransparent = false; // Self-illumination:
1297
+ bufferGeometry.addGroup( index0, numGroupVerts, materials.length - 1 );
992
1298
 
993
- let luminance = 0;
994
- let finishType = FINISH_TYPE_DEFAULT;
995
- let edgeMaterial = null;
996
- const name = lineParser.getToken();
1299
+ }
997
1300
 
998
- if ( ! name ) {
1301
+ const material = elem.material;
999
1302
 
1000
- throw 'LDrawLoader: Material name was expected after "!COLOUR tag' + lineParser.getLineNumberString() + '.';
1303
+ if ( elementSize === 3 ) {
1001
1304
 
1002
- } // Parse tag tokens and their parameters
1305
+ materials.push( material );
1003
1306
 
1307
+ } else if ( elementSize === 2 ) {
1004
1308
 
1005
- let token = null;
1309
+ if ( isConditionalSegments ) {
1006
1310
 
1007
- while ( true ) {
1311
+ materials.push( material.userData.edgeMaterial.userData.conditionalEdgeMaterial );
1008
1312
 
1009
- token = lineParser.getToken();
1313
+ } else {
1010
1314
 
1011
- if ( ! token ) {
1315
+ materials.push( material.userData.edgeMaterial );
1012
1316
 
1013
- break;
1317
+ }
1014
1318
 
1015
1319
  }
1016
1320
 
1017
- switch ( token.toUpperCase() ) {
1321
+ prevMaterial = elem.material;
1322
+ index0 = offset / 3;
1323
+ numGroupVerts = vertices.length;
1018
1324
 
1019
- case 'CODE':
1020
- code = lineParser.getToken();
1021
- break;
1325
+ } else {
1022
1326
 
1023
- case 'VALUE':
1024
- colour = lineParser.getToken();
1327
+ numGroupVerts += vertices.length;
1025
1328
 
1026
- if ( colour.startsWith( '0x' ) ) {
1329
+ }
1027
1330
 
1028
- colour = '#' + colour.substring( 2 );
1331
+ offset += 3 * vertices.length;
1029
1332
 
1030
- } else if ( ! colour.startsWith( '#' ) ) {
1333
+ }
1031
1334
 
1032
- throw 'LDrawLoader: Invalid colour while parsing material' + lineParser.getLineNumberString() + '.';
1335
+ if ( numGroupVerts > 0 ) {
1033
1336
 
1034
- }
1337
+ bufferGeometry.addGroup( index0, Infinity, materials.length - 1 );
1035
1338
 
1036
- break;
1339
+ }
1037
1340
 
1038
- case 'EDGE':
1039
- edgeColour = lineParser.getToken();
1341
+ bufferGeometry.setAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );
1040
1342
 
1041
- if ( edgeColour.startsWith( '0x' ) ) {
1343
+ if ( normals !== null ) {
1042
1344
 
1043
- edgeColour = '#' + edgeColour.substring( 2 );
1345
+ bufferGeometry.setAttribute( 'normal', new THREE.BufferAttribute( normals, 3 ) );
1044
1346
 
1045
- } else if ( ! edgeColour.startsWith( '#' ) ) {
1347
+ }
1046
1348
 
1047
- // Try to see if edge colour is a colour code
1048
- edgeMaterial = this.getMaterial( edgeColour );
1349
+ let object3d = null;
1049
1350
 
1050
- if ( ! edgeMaterial ) {
1351
+ if ( elementSize === 2 ) {
1051
1352
 
1052
- throw 'LDrawLoader: Invalid edge colour while parsing material' + lineParser.getLineNumberString() + '.';
1353
+ object3d = new THREE.LineSegments( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );
1053
1354
 
1054
- } // Get the edge material for this triangle material
1355
+ } else if ( elementSize === 3 ) {
1055
1356
 
1357
+ object3d = new THREE.Mesh( bufferGeometry, materials.length === 1 ? materials[ 0 ] : materials );
1056
1358
 
1057
- edgeMaterial = edgeMaterial.userData.edgeMaterial;
1359
+ }
1058
1360
 
1059
- }
1361
+ if ( isConditionalSegments ) {
1060
1362
 
1061
- break;
1363
+ object3d.isConditionalLine = true;
1364
+ const controlArray0 = new Float32Array( elements.length * 3 * 2 );
1365
+ const controlArray1 = new Float32Array( elements.length * 3 * 2 );
1366
+ const directionArray = new Float32Array( elements.length * 3 * 2 );
1062
1367
 
1063
- case 'ALPHA':
1064
- alpha = parseInt( lineParser.getToken() );
1368
+ for ( let i = 0, l = elements.length; i < l; i ++ ) {
1065
1369
 
1066
- if ( isNaN( alpha ) ) {
1370
+ const os = elements[ i ];
1371
+ const vertices = os.vertices;
1372
+ const controlPoints = os.controlPoints;
1373
+ const c0 = controlPoints[ 0 ];
1374
+ const c1 = controlPoints[ 1 ];
1375
+ const v0 = vertices[ 0 ];
1376
+ const v1 = vertices[ 1 ];
1377
+ const index = i * 3 * 2;
1378
+ controlArray0[ index + 0 ] = c0.x;
1379
+ controlArray0[ index + 1 ] = c0.y;
1380
+ controlArray0[ index + 2 ] = c0.z;
1381
+ controlArray0[ index + 3 ] = c0.x;
1382
+ controlArray0[ index + 4 ] = c0.y;
1383
+ controlArray0[ index + 5 ] = c0.z;
1384
+ controlArray1[ index + 0 ] = c1.x;
1385
+ controlArray1[ index + 1 ] = c1.y;
1386
+ controlArray1[ index + 2 ] = c1.z;
1387
+ controlArray1[ index + 3 ] = c1.x;
1388
+ controlArray1[ index + 4 ] = c1.y;
1389
+ controlArray1[ index + 5 ] = c1.z;
1390
+ directionArray[ index + 0 ] = v1.x - v0.x;
1391
+ directionArray[ index + 1 ] = v1.y - v0.y;
1392
+ directionArray[ index + 2 ] = v1.z - v0.z;
1393
+ directionArray[ index + 3 ] = v1.x - v0.x;
1394
+ directionArray[ index + 4 ] = v1.y - v0.y;
1395
+ directionArray[ index + 5 ] = v1.z - v0.z;
1067
1396
 
1068
- throw 'LDrawLoader: Invalid alpha value in material definition' + lineParser.getLineNumberString() + '.';
1397
+ }
1069
1398
 
1070
- }
1399
+ bufferGeometry.setAttribute( 'control0', new THREE.BufferAttribute( controlArray0, 3, false ) );
1400
+ bufferGeometry.setAttribute( 'control1', new THREE.BufferAttribute( controlArray1, 3, false ) );
1401
+ bufferGeometry.setAttribute( 'direction', new THREE.BufferAttribute( directionArray, 3, false ) );
1071
1402
 
1072
- alpha = Math.max( 0, Math.min( 1, alpha / 255 ) );
1403
+ }
1073
1404
 
1074
- if ( alpha < 1 ) {
1405
+ return object3d;
1075
1406
 
1076
- isTransparent = true;
1407
+ } //
1077
1408
 
1078
- }
1079
1409
 
1080
- break;
1410
+ class LDrawLoader extends THREE.Loader {
1081
1411
 
1082
- case 'LUMINANCE':
1083
- luminance = parseInt( lineParser.getToken() );
1412
+ constructor( manager ) {
1084
1413
 
1085
- if ( isNaN( luminance ) ) {
1414
+ super( manager ); // Array of THREE.Material
1415
+
1416
+ this.materials = []; // Not using THREE.Cache here because it returns the previous HTML error response instead of calling onError()
1417
+ // This also allows to handle the embedded text files ("0 FILE" lines)
1086
1418
 
1087
- throw 'LDrawLoader: Invalid luminance value in material definition' + LineParser.getLineNumberString() + '.';
1419
+ 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.
1088
1420
 
1089
- }
1421
+ this.fileMap = {};
1422
+ this.rootParseScope = this.newParseScopeLevel(); // Add default main triangle and line edge materials (used in pieces that can be colored with a main color)
1090
1423
 
1091
- luminance = Math.max( 0, Math.min( 1, luminance / 255 ) );
1092
- break;
1424
+ 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.
1093
1425
 
1094
- case 'CHROME':
1095
- finishType = FINISH_TYPE_CHROME;
1096
- break;
1426
+ this.smoothNormals = true; // The path to load parts from the LDraw parts library from.
1097
1427
 
1098
- case 'PEARLESCENT':
1099
- finishType = FINISH_TYPE_PEARLESCENT;
1100
- break;
1428
+ this.partsLibraryPath = '';
1101
1429
 
1102
- case 'RUBBER':
1103
- finishType = FINISH_TYPE_RUBBER;
1104
- break;
1430
+ }
1105
1431
 
1106
- case 'MATTE_METALLIC':
1107
- finishType = FINISH_TYPE_MATTE_METALLIC;
1108
- break;
1432
+ setPartsLibraryPath( path ) {
1109
1433
 
1110
- case 'METAL':
1111
- finishType = FINISH_TYPE_METAL;
1112
- break;
1434
+ this.partsLibraryPath = path;
1435
+ return this;
1113
1436
 
1114
- case 'MATERIAL':
1115
- // Not implemented
1116
- lineParser.setToEnd();
1117
- break;
1437
+ }
1118
1438
 
1119
- default:
1120
- throw 'LDrawLoader: Unknown token "' + token + '" while parsing material' + lineParser.getLineNumberString() + '.';
1121
- break;
1439
+ async preloadMaterials( url ) {
1440
+
1441
+ const fileLoader = new THREE.FileLoader( this.manager );
1442
+ fileLoader.setPath( this.path );
1443
+ fileLoader.setRequestHeader( this.requestHeader );
1444
+ fileLoader.setWithCredentials( this.withCredentials );
1445
+ const text = await fileLoader.loadAsync( url );
1446
+ const colorLineRegex = /^0 !COLOUR/;
1447
+ const lines = text.split( /[\n\r]/g );
1448
+ const materials = [];
1449
+
1450
+ for ( let i = 0, l = lines.length; i < l; i ++ ) {
1451
+
1452
+ const line = lines[ i ];
1453
+
1454
+ if ( colorLineRegex.test( line ) ) {
1455
+
1456
+ const directive = line.replace( colorLineRegex, '' );
1457
+ const material = this.parseColorMetaDirective( new LineParser( directive ) );
1458
+ materials.push( material );
1122
1459
 
1123
1460
  }
1124
1461
 
1125
1462
  }
1126
1463
 
1127
- let material = null;
1128
-
1129
- switch ( finishType ) {
1464
+ this.setMaterials( materials );
1130
1465
 
1131
- case FINISH_TYPE_DEFAULT:
1132
- material = new THREE.MeshStandardMaterial( {
1133
- color: colour,
1134
- roughness: 0.3,
1135
- metalness: 0
1136
- } );
1137
- break;
1466
+ }
1138
1467
 
1139
- case FINISH_TYPE_PEARLESCENT:
1140
- // Try to imitate pearlescency by making the surface glossy
1141
- material = new THREE.MeshStandardMaterial( {
1142
- color: colour,
1143
- roughness: 0.3,
1144
- metalness: 0.25
1145
- } );
1146
- break;
1468
+ load( url, onLoad, onProgress, onError ) {
1147
1469
 
1148
- case FINISH_TYPE_CHROME:
1149
- // Mirror finish surface
1150
- material = new THREE.MeshStandardMaterial( {
1151
- color: colour,
1152
- roughness: 0,
1153
- metalness: 1
1154
- } );
1155
- break;
1470
+ const fileLoader = new THREE.FileLoader( this.manager );
1471
+ fileLoader.setPath( this.path );
1472
+ fileLoader.setRequestHeader( this.requestHeader );
1473
+ fileLoader.setWithCredentials( this.withCredentials );
1474
+ fileLoader.load( url, text => {
1156
1475
 
1157
- case FINISH_TYPE_RUBBER:
1158
- // Rubber finish
1159
- material = new THREE.MeshStandardMaterial( {
1160
- color: colour,
1161
- roughness: 0.9,
1162
- metalness: 0
1163
- } );
1164
- break;
1476
+ const parsedInfo = this.parseCache.parse( text );
1477
+ this.processObject( parsedInfo, null, url, this.rootParseScope ).then( function ( result ) {
1165
1478
 
1166
- case FINISH_TYPE_MATTE_METALLIC:
1167
- // Brushed metal finish
1168
- material = new THREE.MeshStandardMaterial( {
1169
- color: colour,
1170
- roughness: 0.8,
1171
- metalness: 0.4
1172
- } );
1173
- break;
1479
+ onLoad( result.groupObject );
1174
1480
 
1175
- case FINISH_TYPE_METAL:
1176
- // Average metal finish
1177
- material = new THREE.MeshStandardMaterial( {
1178
- color: colour,
1179
- roughness: 0.2,
1180
- metalness: 0.85
1181
- } );
1182
- break;
1481
+ } );
1183
1482
 
1184
- default:
1185
- // Should not happen
1186
- break;
1483
+ }, onProgress, onError );
1187
1484
 
1188
- }
1485
+ }
1189
1486
 
1190
- material.transparent = isTransparent;
1191
- material.premultipliedAlpha = true;
1192
- material.opacity = alpha;
1193
- material.depthWrite = ! isTransparent;
1194
- material.polygonOffset = true;
1195
- material.polygonOffsetFactor = 1;
1487
+ parse( text, path, onLoad ) {
1196
1488
 
1197
- if ( luminance !== 0 ) {
1489
+ // Async parse. This function calls onParse with the parsed THREE.Object3D as parameter
1490
+ const parsedInfo = this.parseCache.parse( text );
1491
+ this.processObject( parsedInfo, null, path, this.rootParseScope ).then( function ( result ) {
1198
1492
 
1199
- material.emissive.set( material.color ).multiplyScalar( luminance );
1493
+ onLoad( result.groupObject );
1200
1494
 
1201
- }
1495
+ } );
1202
1496
 
1203
- if ( ! edgeMaterial ) {
1497
+ }
1204
1498
 
1205
- // This is the material used for edges
1206
- edgeMaterial = new THREE.LineBasicMaterial( {
1207
- color: edgeColour,
1208
- transparent: isTransparent,
1209
- opacity: alpha,
1210
- depthWrite: ! isTransparent
1211
- } );
1212
- edgeMaterial.userData.code = code;
1213
- edgeMaterial.name = name + ' - Edge'; // This is the material used for conditional edges
1499
+ setMaterials( materials ) {
1214
1500
 
1215
- edgeMaterial.userData.conditionalEdgeMaterial = new LDrawConditionalLineMaterial( {
1216
- fog: true,
1217
- transparent: isTransparent,
1218
- depthWrite: ! isTransparent,
1219
- color: edgeColour,
1220
- opacity: alpha
1221
- } );
1501
+ // Clears parse scopes stack, adds new scope with material library
1502
+ this.rootParseScope = this.newParseScopeLevel( materials );
1503
+ this.rootParseScope.isFromParse = false;
1504
+ this.materials = materials;
1505
+ return this;
1222
1506
 
1223
- }
1507
+ }
1224
1508
 
1225
- material.userData.code = code;
1226
- material.name = name;
1227
- material.userData.edgeMaterial = edgeMaterial;
1228
- return material;
1509
+ setFileMap( fileMap ) {
1229
1510
 
1230
- } //
1511
+ this.fileMap = fileMap;
1512
+ return this;
1231
1513
 
1514
+ }
1232
1515
 
1233
- objectParse( text, parseScope ) {
1516
+ newParseScopeLevel( materials = null, parentScope = null ) {
1234
1517
 
1235
- // Retrieve data from the parent parse scope
1236
- const currentParseScope = parseScope;
1237
- const parentParseScope = currentParseScope.parentScope; // Main colour codes passed to this subobject (or default codes 16 and 24 if it is the root object)
1518
+ // Adds a new scope level, assign materials to it and returns it
1519
+ const matLib = {};
1238
1520
 
1239
- const mainColourCode = currentParseScope.mainColourCode;
1240
- const mainEdgeColourCode = currentParseScope.mainEdgeColourCode; // Parse result variables
1521
+ if ( materials ) {
1241
1522
 
1242
- let faces;
1243
- let lineSegments;
1244
- let conditionalSegments;
1245
- const subobjects = [];
1246
- let category = null;
1247
- let keywords = null;
1523
+ for ( let i = 0, n = materials.length; i < n; i ++ ) {
1248
1524
 
1249
- if ( text.indexOf( '\r\n' ) !== - 1 ) {
1525
+ const material = materials[ i ];
1526
+ matLib[ material.userData.code ] = material;
1250
1527
 
1251
- // This is faster than String.split with regex that splits on both
1252
- text = text.replace( /\r\n/g, '\n' );
1528
+ }
1253
1529
 
1254
1530
  }
1255
1531
 
1256
- const lines = text.split( '\n' );
1257
- const numLines = lines.length;
1258
- let parsingEmbeddedFiles = false;
1259
- let currentEmbeddedFileName = null;
1260
- let currentEmbeddedText = null;
1261
- let bfcCertified = false;
1262
- let bfcCCW = true;
1263
- let bfcInverted = false;
1264
- let bfcCull = true;
1265
- let type = '';
1266
- let startingConstructionStep = false;
1267
- const scope = this;
1532
+ const newParseScope = {
1533
+ parentScope: parentScope,
1534
+ lib: matLib,
1535
+ url: null,
1536
+ // Subobjects
1537
+ subobjects: null,
1538
+ numSubobjects: 0,
1539
+ subobjectIndex: 0,
1540
+ inverted: false,
1541
+ category: null,
1542
+ keywords: null,
1543
+ // Current subobject
1544
+ currentFileName: null,
1545
+ mainColorCode: parentScope ? parentScope.mainColorCode : '16',
1546
+ mainEdgeColorCode: parentScope ? parentScope.mainEdgeColorCode : '24',
1547
+ matrix: new THREE.Matrix4(),
1548
+ type: 'Model',
1549
+ groupObject: null,
1550
+ // If false, it is a root material scope previous to parse
1551
+ isFromParse: true,
1552
+ faces: [],
1553
+ lineSegments: [],
1554
+ conditionalSegments: [],
1555
+ totalFaces: 0,
1556
+ faceMaterials: new Set(),
1557
+ // If true, this object is the start of a construction step
1558
+ startingConstructionStep: false
1559
+ };
1560
+ return newParseScope;
1268
1561
 
1269
- function parseColourCode( lineParser, forEdge ) {
1562
+ }
1270
1563
 
1271
- // Parses next colour code and returns a THREE.Material
1272
- let colourCode = lineParser.getToken();
1564
+ addMaterial( material, parseScope ) {
1273
1565
 
1274
- if ( ! forEdge && colourCode === '16' ) {
1566
+ // Adds a material to the material library which is on top of the parse scopes stack. And also to the materials array
1567
+ const matLib = parseScope.lib;
1275
1568
 
1276
- colourCode = mainColourCode;
1569
+ if ( ! matLib[ material.userData.code ] ) {
1277
1570
 
1278
- }
1571
+ this.materials.push( material );
1279
1572
 
1280
- if ( forEdge && colourCode === '24' ) {
1573
+ }
1281
1574
 
1282
- colourCode = mainEdgeColourCode;
1575
+ matLib[ material.userData.code ] = material;
1576
+ return this;
1283
1577
 
1284
- }
1578
+ }
1285
1579
 
1286
- const material = scope.getMaterial( colourCode, currentParseScope );
1580
+ getMaterial( colorCode, parseScope = this.rootParseScope ) {
1287
1581
 
1288
- if ( ! material ) {
1582
+ // Given a color code search its material in the parse scopes stack
1583
+ if ( colorCode.startsWith( '0x2' ) ) {
1289
1584
 
1290
- throw 'LDrawLoader: Unknown colour code "' + colourCode + '" is used' + lineParser.getLineNumberString() + ' but it was not defined previously.';
1585
+ // Special 'direct' material value (RGB color)
1586
+ const color = colorCode.substring( 3 );
1587
+ return this.parseColorMetaDirective( new LineParser( 'Direct_Color_' + color + ' CODE -1 VALUE #' + color + ' EDGE #' + color + '' ) );
1291
1588
 
1292
- }
1589
+ }
1293
1590
 
1294
- return material;
1591
+ while ( parseScope ) {
1295
1592
 
1296
- }
1593
+ const material = parseScope.lib[ colorCode ];
1297
1594
 
1298
- function parseVector( lp ) {
1595
+ if ( material ) {
1299
1596
 
1300
- const v = new THREE.Vector3( parseFloat( lp.getToken() ), parseFloat( lp.getToken() ), parseFloat( lp.getToken() ) );
1597
+ return material;
1301
1598
 
1302
- if ( ! scope.separateObjects ) {
1599
+ } else {
1303
1600
 
1304
- v.applyMatrix4( currentParseScope.currentMatrix );
1601
+ parseScope = parseScope.parentScope;
1305
1602
 
1306
1603
  }
1307
1604
 
1308
- return v;
1605
+ } // Material was not found
1309
1606
 
1310
- } // Parse all line commands
1311
1607
 
1608
+ return null;
1312
1609
 
1313
- for ( let lineIndex = 0; lineIndex < numLines; lineIndex ++ ) {
1610
+ }
1314
1611
 
1315
- const line = lines[ lineIndex ];
1316
- if ( line.length === 0 ) continue;
1612
+ parseColorMetaDirective( lineParser ) {
1317
1613
 
1318
- if ( parsingEmbeddedFiles ) {
1614
+ // Parses a color definition and returns a THREE.Material
1615
+ let code = null; // Triangle and line colors
1319
1616
 
1320
- if ( line.startsWith( '0 FILE ' ) ) {
1617
+ let color = 0xFF00FF;
1618
+ let edgeColor = 0xFF00FF; // Transparency
1321
1619
 
1322
- // Save previous embedded file in the cache
1323
- this.cache.setData( currentEmbeddedFileName.toLowerCase(), currentEmbeddedText ); // New embedded text file
1620
+ let alpha = 1;
1621
+ let isTransparent = false; // Self-illumination:
1324
1622
 
1325
- currentEmbeddedFileName = line.substring( 7 );
1326
- currentEmbeddedText = '';
1623
+ let luminance = 0;
1624
+ let finishType = FINISH_TYPE_DEFAULT;
1625
+ let edgeMaterial = null;
1626
+ const name = lineParser.getToken();
1327
1627
 
1328
- } else {
1628
+ if ( ! name ) {
1329
1629
 
1330
- currentEmbeddedText += line + '\n';
1630
+ throw new Error( 'LDrawLoader: Material name was expected after "!COLOUR tag' + lineParser.getLineNumberString() + '.' );
1331
1631
 
1332
- }
1632
+ } // Parse tag tokens and their parameters
1333
1633
 
1334
- continue;
1634
+
1635
+ let token = null;
1636
+
1637
+ while ( true ) {
1638
+
1639
+ token = lineParser.getToken();
1640
+
1641
+ if ( ! token ) {
1642
+
1643
+ break;
1335
1644
 
1336
1645
  }
1337
1646
 
1338
- const lp = new LineParser( line, lineIndex + 1 );
1339
- lp.seekNonSpace();
1647
+ switch ( token.toUpperCase() ) {
1340
1648
 
1341
- if ( lp.isAtTheEnd() ) {
1649
+ case 'CODE':
1650
+ code = lineParser.getToken();
1651
+ break;
1342
1652
 
1343
- // Empty line
1344
- continue;
1653
+ case 'VALUE':
1654
+ color = lineParser.getToken();
1345
1655
 
1346
- } // Parse the line type
1656
+ if ( color.startsWith( '0x' ) ) {
1347
1657
 
1658
+ color = '#' + color.substring( 2 );
1348
1659
 
1349
- const lineType = lp.getToken();
1350
- let material;
1351
- let segment;
1352
- let inverted;
1353
- let ccw;
1354
- let doubleSided;
1355
- let v0, v1, v2, v3, c0, c1, faceNormal;
1660
+ } else if ( ! color.startsWith( '#' ) ) {
1356
1661
 
1357
- switch ( lineType ) {
1662
+ throw new Error( 'LDrawLoader: Invalid color while parsing material' + lineParser.getLineNumberString() + '.' );
1358
1663
 
1359
- // Line type 0: Comment or META
1360
- case '0':
1361
- // Parse meta directive
1362
- const meta = lp.getToken();
1664
+ }
1363
1665
 
1364
- if ( meta ) {
1666
+ break;
1365
1667
 
1366
- switch ( meta ) {
1668
+ case 'EDGE':
1669
+ edgeColor = lineParser.getToken();
1367
1670
 
1368
- case '!LDRAW_ORG':
1369
- type = lp.getToken();
1370
- currentParseScope.type = type; // If the scale of the object is negated then the triangle winding order
1371
- // needs to be flipped.
1671
+ if ( edgeColor.startsWith( '0x' ) ) {
1372
1672
 
1373
- if ( currentParseScope.matrix.determinant() < 0 && ( scope.separateObjects && isPrimitiveType( type ) || ! scope.separateObjects ) ) {
1673
+ edgeColor = '#' + edgeColor.substring( 2 );
1374
1674
 
1375
- currentParseScope.inverted = ! currentParseScope.inverted;
1675
+ } else if ( ! edgeColor.startsWith( '#' ) ) {
1376
1676
 
1377
- }
1677
+ // Try to see if edge color is a color code
1678
+ edgeMaterial = this.getMaterial( edgeColor );
1378
1679
 
1379
- faces = currentParseScope.faces;
1380
- lineSegments = currentParseScope.lineSegments;
1381
- conditionalSegments = currentParseScope.conditionalSegments;
1382
- break;
1680
+ if ( ! edgeMaterial ) {
1383
1681
 
1384
- case '!COLOUR':
1385
- material = this.parseColourMetaDirective( lp );
1682
+ throw new Error( 'LDrawLoader: Invalid edge color while parsing material' + lineParser.getLineNumberString() + '.' );
1386
1683
 
1387
- if ( material ) {
1684
+ } // Get the edge material for this triangle material
1388
1685
 
1389
- this.addMaterial( material, parseScope );
1390
1686
 
1391
- } else {
1687
+ edgeMaterial = edgeMaterial.userData.edgeMaterial;
1392
1688
 
1393
- console.warn( 'LDrawLoader: Error parsing material' + lp.getLineNumberString() );
1689
+ }
1394
1690
 
1395
- }
1691
+ break;
1396
1692
 
1397
- break;
1693
+ case 'ALPHA':
1694
+ alpha = parseInt( lineParser.getToken() );
1398
1695
 
1399
- case '!CATEGORY':
1400
- category = lp.getToken();
1401
- break;
1696
+ if ( isNaN( alpha ) ) {
1402
1697
 
1403
- case '!KEYWORDS':
1404
- const newKeywords = lp.getRemainingString().split( ',' );
1698
+ throw new Error( 'LDrawLoader: Invalid alpha value in material definition' + lineParser.getLineNumberString() + '.' );
1405
1699
 
1406
- if ( newKeywords.length > 0 ) {
1700
+ }
1407
1701
 
1408
- if ( ! keywords ) {
1702
+ alpha = Math.max( 0, Math.min( 1, alpha / 255 ) );
1409
1703
 
1410
- keywords = [];
1704
+ if ( alpha < 1 ) {
1411
1705
 
1412
- }
1706
+ isTransparent = true;
1413
1707
 
1414
- newKeywords.forEach( function ( keyword ) {
1708
+ }
1415
1709
 
1416
- keywords.push( keyword.trim() );
1710
+ break;
1417
1711
 
1418
- } );
1712
+ case 'LUMINANCE':
1713
+ luminance = parseInt( lineParser.getToken() );
1419
1714
 
1420
- }
1715
+ if ( isNaN( luminance ) ) {
1421
1716
 
1422
- break;
1717
+ throw new Error( 'LDrawLoader: Invalid luminance value in material definition' + LineParser.getLineNumberString() + '.' );
1423
1718
 
1424
- case 'FILE':
1425
- if ( lineIndex > 0 ) {
1719
+ }
1426
1720
 
1427
- // Start embedded text files parsing
1428
- parsingEmbeddedFiles = true;
1429
- currentEmbeddedFileName = lp.getRemainingString();
1430
- currentEmbeddedText = '';
1431
- bfcCertified = false;
1432
- bfcCCW = true;
1721
+ luminance = Math.max( 0, Math.min( 1, luminance / 255 ) );
1722
+ break;
1723
+
1724
+ case 'CHROME':
1725
+ finishType = FINISH_TYPE_CHROME;
1726
+ break;
1727
+
1728
+ case 'PEARLESCENT':
1729
+ finishType = FINISH_TYPE_PEARLESCENT;
1730
+ break;
1731
+
1732
+ case 'RUBBER':
1733
+ finishType = FINISH_TYPE_RUBBER;
1734
+ break;
1433
1735
 
1434
- }
1736
+ case 'MATTE_METALLIC':
1737
+ finishType = FINISH_TYPE_MATTE_METALLIC;
1738
+ break;
1435
1739
 
1436
- break;
1740
+ case 'METAL':
1741
+ finishType = FINISH_TYPE_METAL;
1742
+ break;
1437
1743
 
1438
- case 'BFC':
1439
- // Changes to the backface culling state
1440
- while ( ! lp.isAtTheEnd() ) {
1744
+ case 'MATERIAL':
1745
+ // Not implemented
1746
+ lineParser.setToEnd();
1747
+ break;
1441
1748
 
1442
- const token = lp.getToken();
1749
+ default:
1750
+ throw new Error( 'LDrawLoader: Unknown token "' + token + '" while parsing material' + lineParser.getLineNumberString() + '.' );
1443
1751
 
1444
- switch ( token ) {
1752
+ }
1445
1753
 
1446
- case 'CERTIFY':
1447
- case 'NOCERTIFY':
1448
- bfcCertified = token === 'CERTIFY';
1449
- bfcCCW = true;
1450
- break;
1754
+ }
1451
1755
 
1452
- case 'CW':
1453
- case 'CCW':
1454
- bfcCCW = token === 'CCW';
1455
- break;
1756
+ let material = null;
1456
1757
 
1457
- case 'INVERTNEXT':
1458
- bfcInverted = true;
1459
- break;
1758
+ switch ( finishType ) {
1460
1759
 
1461
- case 'CLIP':
1462
- case 'NOCLIP':
1463
- bfcCull = token === 'CLIP';
1464
- break;
1760
+ case FINISH_TYPE_DEFAULT:
1761
+ material = new THREE.MeshStandardMaterial( {
1762
+ color: color,
1763
+ roughness: 0.3,
1764
+ metalness: 0
1765
+ } );
1766
+ break;
1465
1767
 
1466
- default:
1467
- console.warn( 'THREE.LDrawLoader: BFC directive "' + token + '" is unknown.' );
1468
- break;
1768
+ case FINISH_TYPE_PEARLESCENT:
1769
+ // Try to imitate pearlescency by making the surface glossy
1770
+ material = new THREE.MeshStandardMaterial( {
1771
+ color: color,
1772
+ roughness: 0.3,
1773
+ metalness: 0.25
1774
+ } );
1775
+ break;
1469
1776
 
1470
- }
1777
+ case FINISH_TYPE_CHROME:
1778
+ // Mirror finish surface
1779
+ material = new THREE.MeshStandardMaterial( {
1780
+ color: color,
1781
+ roughness: 0,
1782
+ metalness: 1
1783
+ } );
1784
+ break;
1471
1785
 
1472
- }
1786
+ case FINISH_TYPE_RUBBER:
1787
+ // Rubber finish
1788
+ material = new THREE.MeshStandardMaterial( {
1789
+ color: color,
1790
+ roughness: 0.9,
1791
+ metalness: 0
1792
+ } );
1793
+ break;
1473
1794
 
1474
- break;
1795
+ case FINISH_TYPE_MATTE_METALLIC:
1796
+ // Brushed metal finish
1797
+ material = new THREE.MeshStandardMaterial( {
1798
+ color: color,
1799
+ roughness: 0.8,
1800
+ metalness: 0.4
1801
+ } );
1802
+ break;
1475
1803
 
1476
- case 'STEP':
1477
- startingConstructionStep = true;
1478
- break;
1804
+ case FINISH_TYPE_METAL:
1805
+ // Average metal finish
1806
+ material = new THREE.MeshStandardMaterial( {
1807
+ color: color,
1808
+ roughness: 0.2,
1809
+ metalness: 0.85
1810
+ } );
1811
+ break;
1479
1812
 
1480
- default:
1481
- // Other meta directives are not implemented
1482
- break;
1813
+ default:
1814
+ // Should not happen
1815
+ break;
1483
1816
 
1484
- }
1817
+ }
1485
1818
 
1486
- }
1819
+ material.transparent = isTransparent;
1820
+ material.premultipliedAlpha = true;
1821
+ material.opacity = alpha;
1822
+ material.depthWrite = ! isTransparent;
1823
+ material.polygonOffset = true;
1824
+ material.polygonOffsetFactor = 1;
1487
1825
 
1488
- break;
1489
- // Line type 1: Sub-object file
1826
+ if ( luminance !== 0 ) {
1490
1827
 
1491
- case '1':
1492
- material = parseColourCode( lp );
1493
- const posX = parseFloat( lp.getToken() );
1494
- const posY = parseFloat( lp.getToken() );
1495
- const posZ = parseFloat( lp.getToken() );
1496
- const m0 = parseFloat( lp.getToken() );
1497
- const m1 = parseFloat( lp.getToken() );
1498
- const m2 = parseFloat( lp.getToken() );
1499
- const m3 = parseFloat( lp.getToken() );
1500
- const m4 = parseFloat( lp.getToken() );
1501
- const m5 = parseFloat( lp.getToken() );
1502
- const m6 = parseFloat( lp.getToken() );
1503
- const m7 = parseFloat( lp.getToken() );
1504
- const m8 = parseFloat( lp.getToken() );
1505
- const matrix = new THREE.Matrix4().set( m0, m1, m2, posX, m3, m4, m5, posY, m6, m7, m8, posZ, 0, 0, 0, 1 );
1506
- let fileName = lp.getRemainingString().trim().replace( /\\/g, '/' );
1828
+ material.emissive.set( material.color ).multiplyScalar( luminance );
1507
1829
 
1508
- if ( scope.fileMap[ fileName ] ) {
1830
+ }
1509
1831
 
1510
- // Found the subobject path in the preloaded file path map
1511
- fileName = scope.fileMap[ fileName ];
1832
+ if ( ! edgeMaterial ) {
1512
1833
 
1513
- } else {
1834
+ // This is the material used for edges
1835
+ edgeMaterial = new THREE.LineBasicMaterial( {
1836
+ color: edgeColor,
1837
+ transparent: isTransparent,
1838
+ opacity: alpha,
1839
+ depthWrite: ! isTransparent
1840
+ } );
1841
+ edgeMaterial.userData.code = code;
1842
+ edgeMaterial.name = name + ' - Edge'; // This is the material used for conditional edges
1514
1843
 
1515
- // Standardized subfolders
1516
- if ( fileName.startsWith( 's/' ) ) {
1844
+ edgeMaterial.userData.conditionalEdgeMaterial = new LDrawConditionalLineMaterial( {
1845
+ fog: true,
1846
+ transparent: isTransparent,
1847
+ depthWrite: ! isTransparent,
1848
+ color: edgeColor,
1849
+ opacity: alpha
1850
+ } );
1517
1851
 
1518
- fileName = 'parts/' + fileName;
1852
+ }
1519
1853
 
1520
- } else if ( fileName.startsWith( '48/' ) ) {
1854
+ material.userData.code = code;
1855
+ material.name = name;
1856
+ material.userData.edgeMaterial = edgeMaterial;
1857
+ return material;
1521
1858
 
1522
- fileName = 'p/' + fileName;
1859
+ } //
1523
1860
 
1524
- }
1525
1861
 
1526
- }
1862
+ objectParse( info, parseScope ) {
1527
1863
 
1528
- subobjects.push( {
1529
- material: material,
1530
- matrix: matrix,
1531
- fileName: fileName,
1532
- inverted: bfcInverted !== currentParseScope.inverted,
1533
- startingConstructionStep: startingConstructionStep
1534
- } );
1535
- bfcInverted = false;
1536
- break;
1537
- // Line type 2: Line segment
1864
+ // Retrieve data from the parent parse scope
1865
+ const currentParseScope = parseScope;
1866
+ const parentParseScope = currentParseScope.parentScope; // Main color codes passed to this subobject (or default codes 16 and 24 if it is the root object)
1538
1867
 
1539
- case '2':
1540
- material = parseColourCode( lp, true );
1541
- v0 = parseVector( lp );
1542
- v1 = parseVector( lp );
1543
- segment = {
1544
- material: material.userData.edgeMaterial,
1545
- colourCode: material.userData.code,
1546
- v0: v0,
1547
- v1: v1,
1548
- vertices: [ v0, v1 ]
1549
- };
1550
- lineSegments.push( segment );
1551
- break;
1552
- // Line type 5: Conditional Line segment
1868
+ const mainColorCode = currentParseScope.mainColorCode;
1869
+ const mainEdgeColorCode = currentParseScope.mainEdgeColorCode;
1553
1870
 
1554
- case '5':
1555
- material = parseColourCode( lp, true );
1556
- v0 = parseVector( lp );
1557
- v1 = parseVector( lp );
1558
- c0 = parseVector( lp );
1559
- c1 = parseVector( lp );
1560
- segment = {
1561
- material: material.userData.edgeMaterial.userData.conditionalEdgeMaterial,
1562
- colourCode: material.userData.code,
1563
- vertices: [ v0, v1 ],
1564
- controlPoints: [ c0, c1 ]
1565
- };
1566
- conditionalSegments.push( segment );
1567
- break;
1568
- // Line type 3: Triangle
1871
+ const parseColorCode = ( colorCode, forEdge ) => {
1569
1872
 
1570
- case '3':
1571
- material = parseColourCode( lp );
1572
- inverted = currentParseScope.inverted;
1573
- ccw = bfcCCW !== inverted;
1574
- doubleSided = ! bfcCertified || ! bfcCull;
1873
+ // Parses next color code and returns a THREE.Material
1874
+ if ( ! forEdge && colorCode === '16' ) {
1575
1875
 
1576
- if ( ccw === true ) {
1876
+ colorCode = mainColorCode;
1577
1877
 
1578
- v0 = parseVector( lp );
1579
- v1 = parseVector( lp );
1580
- v2 = parseVector( lp );
1878
+ }
1581
1879
 
1582
- } else {
1880
+ if ( forEdge && colorCode === '24' ) {
1583
1881
 
1584
- v2 = parseVector( lp );
1585
- v1 = parseVector( lp );
1586
- v0 = parseVector( lp );
1882
+ colorCode = mainEdgeColorCode;
1587
1883
 
1588
- }
1884
+ }
1589
1885
 
1590
- _tempVec0.subVectors( v1, v0 );
1886
+ const material = this.getMaterial( colorCode, currentParseScope );
1591
1887
 
1592
- _tempVec1.subVectors( v2, v1 );
1888
+ if ( ! material ) {
1593
1889
 
1594
- faceNormal = new THREE.Vector3().crossVectors( _tempVec0, _tempVec1 ).normalize();
1595
- faces.push( {
1596
- material: material,
1597
- colourCode: material.userData.code,
1598
- faceNormal: faceNormal,
1599
- vertices: [ v0, v1, v2 ],
1600
- normals: [ null, null, null ]
1601
- } );
1602
- currentParseScope.totalFaces ++;
1890
+ throw new Error( 'LDrawLoader: Unknown color code "' + colorCode + '" is used but it was not defined previously.' );
1603
1891
 
1604
- if ( doubleSided === true ) {
1892
+ }
1605
1893
 
1606
- faces.push( {
1607
- material: material,
1608
- colourCode: material.userData.code,
1609
- faceNormal: faceNormal,
1610
- vertices: [ v2, v1, v0 ],
1611
- normals: [ null, null, null ]
1612
- } );
1613
- currentParseScope.totalFaces ++;
1894
+ return material;
1614
1895
 
1615
- }
1896
+ };
1616
1897
 
1617
- break;
1618
- // Line type 4: Quadrilateral
1898
+ const faces = info.faces;
1899
+ const lineSegments = info.lineSegments;
1900
+ const conditionalSegments = info.conditionalSegments;
1901
+ const materials = info.materials;
1619
1902
 
1620
- case '4':
1621
- material = parseColourCode( lp );
1622
- inverted = currentParseScope.inverted;
1623
- ccw = bfcCCW !== inverted;
1624
- doubleSided = ! bfcCertified || ! bfcCull;
1903
+ if ( currentParseScope.inverted ) {
1625
1904
 
1626
- if ( ccw === true ) {
1905
+ faces.reverse();
1627
1906
 
1628
- v0 = parseVector( lp );
1629
- v1 = parseVector( lp );
1630
- v2 = parseVector( lp );
1631
- v3 = parseVector( lp );
1907
+ }
1632
1908
 
1633
- } else {
1909
+ for ( const colorCode in materials ) {
1634
1910
 
1635
- v3 = parseVector( lp );
1636
- v2 = parseVector( lp );
1637
- v1 = parseVector( lp );
1638
- v0 = parseVector( lp );
1911
+ this.addMaterial( materials[ colorCode ], currentParseScope );
1639
1912
 
1640
- }
1913
+ }
1641
1914
 
1642
- _tempVec0.subVectors( v1, v0 );
1915
+ for ( let i = 0, l = faces.length; i < l; i ++ ) {
1643
1916
 
1644
- _tempVec1.subVectors( v2, v1 );
1917
+ const face = faces[ i ];
1645
1918
 
1646
- faceNormal = new THREE.Vector3().crossVectors( _tempVec0, _tempVec1 ).normalize(); // specifically place the triangle diagonal in the v0 and v1 slots so we can
1647
- // account for the doubling of vertices later when smoothing normals.
1919
+ if ( face.material === null ) {
1648
1920
 
1649
- faces.push( {
1650
- material: material,
1651
- colourCode: material.userData.code,
1652
- faceNormal: faceNormal,
1653
- vertices: [ v0, v1, v2, v3 ],
1654
- normals: [ null, null, null, null ]
1655
- } );
1656
- currentParseScope.totalFaces += 2;
1921
+ face.material = parseColorCode( face.colorCode, false );
1657
1922
 
1658
- if ( doubleSided === true ) {
1923
+ }
1659
1924
 
1660
- faces.push( {
1661
- material: material,
1662
- colourCode: material.userData.code,
1663
- faceNormal: faceNormal,
1664
- vertices: [ v3, v2, v1, v0 ],
1665
- normals: [ null, null, null, null ]
1666
- } );
1667
- currentParseScope.totalFaces += 2;
1925
+ }
1668
1926
 
1669
- }
1927
+ for ( let i = 0, l = lineSegments.length; i < l; i ++ ) {
1670
1928
 
1671
- break;
1929
+ const ls = lineSegments[ i ];
1672
1930
 
1673
- default:
1674
- throw 'LDrawLoader: Unknown line type "' + lineType + '"' + lp.getLineNumberString() + '.';
1675
- break;
1931
+ if ( ls.material === null ) {
1932
+
1933
+ ls.material = parseColorCode( ls.colorCode, true );
1676
1934
 
1677
1935
  }
1678
1936
 
1679
1937
  }
1680
1938
 
1681
- if ( parsingEmbeddedFiles ) {
1939
+ for ( let i = 0, l = conditionalSegments.length; i < l; i ++ ) {
1682
1940
 
1683
- this.cache.setData( currentEmbeddedFileName.toLowerCase(), currentEmbeddedText );
1941
+ const cs = conditionalSegments[ i ];
1942
+
1943
+ if ( cs.material === null ) {
1944
+
1945
+ cs.material = parseColorCode( cs.colorCode, true );
1946
+
1947
+ }
1684
1948
 
1685
1949
  }
1686
1950
 
1687
- currentParseScope.category = category;
1688
- currentParseScope.keywords = keywords;
1689
- currentParseScope.subobjects = subobjects;
1690
- currentParseScope.numSubobjects = subobjects.length;
1951
+ currentParseScope.faces = info.faces;
1952
+ currentParseScope.conditionalSegments = info.conditionalSegments;
1953
+ currentParseScope.lineSegments = info.lineSegments;
1954
+ currentParseScope.category = info.category;
1955
+ currentParseScope.keywords = info.keywords;
1956
+ currentParseScope.subobjects = info.subobjects;
1957
+ currentParseScope.numSubobjects = info.subobjects.length;
1691
1958
  currentParseScope.subobjectIndex = 0;
1959
+ currentParseScope.type = info.type;
1960
+ currentParseScope.totalFaces = info.totalFaces;
1692
1961
  const isRoot = ! parentParseScope.isFromParse;
1693
1962
 
1694
- if ( isRoot || scope.separateObjects && ! isPrimitiveType( type ) ) {
1963
+ if ( isRoot || ! isPrimitiveType( info.type ) ) {
1695
1964
 
1696
1965
  currentParseScope.groupObject = new THREE.Group();
1697
1966
  currentParseScope.groupObject.userData.startingConstructionStep = currentParseScope.startingConstructionStep;
@@ -1736,17 +2005,21 @@
1736
2005
  // is added directly into the parent model (meaning it will never get smoothed by
1737
2006
  // being added to a part)
1738
2007
 
1739
- const doSmooth = isPartType( subobjectParseScope.type ) || ! isPartType( subobjectParseScope.type ) && ! isModelType( subobjectParseScope.type ) && isModelType( subobjectParseScope.parentScope.type );
2008
+ const doSmooth = isPartType( subobjectParseScope.type ) || isPrimitiveType( subobjectParseScope.type ) && isModelType( subobjectParseScope.parentScope.type );
1740
2009
 
1741
2010
  if ( this.smoothNormals && doSmooth ) {
1742
2011
 
1743
- smoothNormals( subobjectParseScope.faces, subobjectParseScope.lineSegments );
2012
+ 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 --
2013
+ // there may be cases where a single edge line crosses over polygon edges that are broken up by multiple materials.
2014
+
2015
+ const checkSubSegments = subobjectParseScope.faceMaterials.size > 1;
2016
+ smoothNormals( subobjectParseScope.faces, subobjectParseScope.lineSegments, checkSubSegments );
1744
2017
 
1745
2018
  }
1746
2019
 
1747
2020
  const isRoot = ! parentParseScope.isFromParse;
1748
2021
 
1749
- if ( this.separateObjects && ! isPrimitiveType( subobjectParseScope.type ) || isRoot ) {
2022
+ if ( ! isPrimitiveType( subobjectParseScope.type ) || isRoot ) {
1750
2023
 
1751
2024
  const objGroup = subobjectParseScope.groupObject;
1752
2025
 
@@ -1780,26 +2053,23 @@
1780
2053
 
1781
2054
  } else {
1782
2055
 
1783
- const separateObjects = this.separateObjects;
1784
2056
  const parentLineSegments = parentParseScope.lineSegments;
1785
2057
  const parentConditionalSegments = parentParseScope.conditionalSegments;
1786
2058
  const parentFaces = parentParseScope.faces;
2059
+ const parentFaceMaterials = parentParseScope.faceMaterials;
1787
2060
  const lineSegments = subobjectParseScope.lineSegments;
1788
2061
  const conditionalSegments = subobjectParseScope.conditionalSegments;
1789
2062
  const faces = subobjectParseScope.faces;
2063
+ const faceMaterials = subobjectParseScope.faceMaterials;
2064
+ const matrix = subobjectParseScope.matrix;
2065
+ const matrixScaleInverted = matrix.determinant() < 0;
1790
2066
 
1791
2067
  for ( let i = 0, l = lineSegments.length; i < l; i ++ ) {
1792
2068
 
1793
2069
  const ls = lineSegments[ i ];
1794
-
1795
- if ( separateObjects ) {
1796
-
1797
- const vertices = ls.vertices;
1798
- vertices[ 0 ].applyMatrix4( subobjectParseScope.matrix );
1799
- vertices[ 1 ].applyMatrix4( subobjectParseScope.matrix );
1800
-
1801
- }
1802
-
2070
+ const vertices = ls.vertices;
2071
+ vertices[ 0 ].applyMatrix4( matrix );
2072
+ vertices[ 1 ].applyMatrix4( matrix );
1803
2073
  parentLineSegments.push( ls );
1804
2074
 
1805
2075
  }
@@ -1807,18 +2077,12 @@
1807
2077
  for ( let i = 0, l = conditionalSegments.length; i < l; i ++ ) {
1808
2078
 
1809
2079
  const os = conditionalSegments[ i ];
1810
-
1811
- if ( separateObjects ) {
1812
-
1813
- const vertices = os.vertices;
1814
- const controlPoints = os.controlPoints;
1815
- vertices[ 0 ].applyMatrix4( subobjectParseScope.matrix );
1816
- vertices[ 1 ].applyMatrix4( subobjectParseScope.matrix );
1817
- controlPoints[ 0 ].applyMatrix4( subobjectParseScope.matrix );
1818
- controlPoints[ 1 ].applyMatrix4( subobjectParseScope.matrix );
1819
-
1820
- }
1821
-
2080
+ const vertices = os.vertices;
2081
+ const controlPoints = os.controlPoints;
2082
+ vertices[ 0 ].applyMatrix4( matrix );
2083
+ vertices[ 1 ].applyMatrix4( matrix );
2084
+ controlPoints[ 0 ].applyMatrix4( matrix );
2085
+ controlPoints[ 1 ].applyMatrix4( matrix );
1822
2086
  parentConditionalSegments.push( os );
1823
2087
 
1824
2088
  }
@@ -1826,22 +2090,19 @@
1826
2090
  for ( let i = 0, l = faces.length; i < l; i ++ ) {
1827
2091
 
1828
2092
  const tri = faces[ i ];
2093
+ const vertices = tri.vertices;
1829
2094
 
1830
- if ( separateObjects ) {
1831
-
1832
- const vertices = tri.vertices;
2095
+ for ( let i = 0, l = vertices.length; i < l; i ++ ) {
1833
2096
 
1834
- for ( let i = 0, l = vertices.length; i < l; i ++ ) {
2097
+ vertices[ i ].applyMatrix4( matrix );
1835
2098
 
1836
- vertices[ i ] = vertices[ i ].clone().applyMatrix4( subobjectParseScope.matrix );
2099
+ } // If the scale of the object is negated then the triangle winding order
2100
+ // needs to be flipped.
1837
2101
 
1838
- }
1839
-
1840
- _tempVec0.subVectors( vertices[ 1 ], vertices[ 0 ] );
1841
2102
 
1842
- _tempVec1.subVectors( vertices[ 2 ], vertices[ 1 ] );
2103
+ if ( matrixScaleInverted !== subobjectParseScope.inverted ) {
1843
2104
 
1844
- tri.faceNormal.crossVectors( _tempVec0, _tempVec1 ).normalize();
2105
+ vertices.reverse();
1845
2106
 
1846
2107
  }
1847
2108
 
@@ -1850,12 +2111,13 @@
1850
2111
  }
1851
2112
 
1852
2113
  parentParseScope.totalFaces += subobjectParseScope.totalFaces;
2114
+ faceMaterials.forEach( material => parentFaceMaterials.add( material ) );
1853
2115
 
1854
2116
  }
1855
2117
 
1856
2118
  }
1857
2119
 
1858
- async processObject( text, subobject, url, parentScope ) {
2120
+ async processObject( parsedInfo, subobject, url, parentScope ) {
1859
2121
 
1860
2122
  const scope = this;
1861
2123
  const parseScope = this.newParseScopeLevel( null, parentScope );
@@ -1864,18 +2126,28 @@
1864
2126
 
1865
2127
  if ( subobject ) {
1866
2128
 
1867
- parseScope.currentMatrix.multiplyMatrices( parentParseScope.currentMatrix, subobject.matrix );
1868
2129
  parseScope.matrix.copy( subobject.matrix );
1869
2130
  parseScope.inverted = subobject.inverted;
1870
2131
  parseScope.startingConstructionStep = subobject.startingConstructionStep;
1871
- parseScope.mainColourCode = subobject.material.userData.code;
1872
- parseScope.mainEdgeColourCode = subobject.material.userData.edgeMaterial.userData.code;
1873
2132
  parseScope.fileName = subobject.fileName;
1874
2133
 
2134
+ if ( subobject.colorCode === '16' && parseScope.parentScope ) {
2135
+
2136
+ const parentScope = parseScope.parentScope;
2137
+ parseScope.mainColorCode = parentScope.mainColorCode;
2138
+ parseScope.mainEdgeColorCode = parentScope.mainEdgeColorCode;
2139
+
2140
+ } else if ( subobject.colorCode !== '16' ) {
2141
+
2142
+ parseScope.mainColorCode = subobject.colorCode;
2143
+ parseScope.mainEdgeColorCode = subobject.colorCode;
2144
+
2145
+ }
2146
+
1875
2147
  } // Parse the object
1876
2148
 
1877
2149
 
1878
- this.objectParse( text, parseScope );
2150
+ this.objectParse( parsedInfo, parseScope );
1879
2151
  const subobjects = parseScope.subobjects;
1880
2152
  const promises = [];
1881
2153
 
@@ -1907,13 +2179,13 @@
1907
2179
 
1908
2180
  function loadSubobject( subobject ) {
1909
2181
 
1910
- return scope.cache.loadData( subobject.fileName ).then( function ( text ) {
2182
+ return scope.parseCache.loadData( subobject.fileName ).then( function ( parsedInfo ) {
1911
2183
 
1912
- return scope.processObject( text, subobject, url, parseScope );
2184
+ return scope.processObject( parsedInfo, subobject, url, parseScope );
1913
2185
 
1914
- } ).catch( function () {
2186
+ } ).catch( function ( err ) {
1915
2187
 
1916
- console.warn( 'LDrawLoader: Subobject "' + subobject.fileName + '" could not be found.' );
2188
+ console.warn( err );
1917
2189
  return null;
1918
2190
 
1919
2191
  } );