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