three-stdlib 2.7.0 → 2.8.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 (78) hide show
  1. package/{Nodes-ec4e1143.js → Nodes-894ac9dc.js} +0 -0
  2. package/{Nodes-427f68b0.js → Nodes-af575af7.js} +0 -0
  3. package/cameras/CinematicCamera.cjs.js +1 -1
  4. package/cameras/CinematicCamera.js +3 -8
  5. package/controls/ArcballControls.cjs.js +1 -1
  6. package/controls/ArcballControls.d.ts +6 -9
  7. package/controls/ArcballControls.js +188 -234
  8. package/controls/FirstPersonControls.cjs.js +1 -1
  9. package/controls/FirstPersonControls.d.ts +4 -5
  10. package/controls/FirstPersonControls.js +36 -45
  11. package/controls/TransformControls.cjs.js +1 -1
  12. package/controls/TransformControls.d.ts +2 -1
  13. package/controls/TransformControls.js +25 -26
  14. package/exporters/GLTFExporter.cjs.js +1 -1
  15. package/exporters/GLTFExporter.js +9 -18
  16. package/geometries/TeapotGeometry.js +2 -2
  17. package/index.cjs.js +1 -1
  18. package/loaders/EXRLoader.cjs.js +1 -1
  19. package/loaders/EXRLoader.js +21 -10
  20. package/loaders/GLTFLoader.cjs.js +1 -1
  21. package/loaders/GLTFLoader.js +5 -6
  22. package/loaders/HDRCubeTextureLoader.cjs.js +1 -1
  23. package/loaders/HDRCubeTextureLoader.js +1 -3
  24. package/loaders/LDrawLoader.cjs.js +1 -1
  25. package/loaders/LDrawLoader.js +1450 -1105
  26. package/loaders/LUT3dlLoader.cjs.js +1 -1
  27. package/loaders/LUT3dlLoader.js +18 -11
  28. package/loaders/LUTCubeLoader.cjs.js +1 -1
  29. package/loaders/LUTCubeLoader.js +4 -5
  30. package/loaders/NodeMaterialLoader.cjs.js +1 -1
  31. package/loaders/PCDLoader.cjs.js +1 -1
  32. package/loaders/PCDLoader.js +2 -2
  33. package/loaders/RGBELoader.cjs.js +1 -1
  34. package/loaders/RGBELoader.js +6 -6
  35. package/loaders/STLLoader.js +7 -7
  36. package/loaders/VRMLLoader.cjs.js +1 -1
  37. package/loaders/VRMLLoader.js +10 -18
  38. package/modifiers/CurveModifier.cjs.js +1 -1
  39. package/modifiers/CurveModifier.js +9 -8
  40. package/nodes/accessors/CameraNode.js +12 -12
  41. package/nodes/accessors/PositionNode.js +3 -3
  42. package/nodes/accessors/ReflectNode.js +3 -3
  43. package/nodes/core/FunctionNode.js +3 -3
  44. package/nodes/core/InputNode.js +3 -3
  45. package/nodes/core/Node.js +6 -6
  46. package/nodes/core/TempNode.js +6 -6
  47. package/nodes/effects/BlurNode.js +3 -3
  48. package/nodes/math/MathNode.js +3 -3
  49. package/nodes/utils/VelocityNode.js +6 -6
  50. package/objects/Lensflare.cjs.js +1 -1
  51. package/objects/Lensflare.js +3 -11
  52. package/objects/Reflector.cjs.js +1 -1
  53. package/objects/Reflector.js +16 -12
  54. package/objects/ReflectorForSSRPass.cjs.js +1 -1
  55. package/objects/ReflectorForSSRPass.js +1 -9
  56. package/objects/Refractor.cjs.js +1 -1
  57. package/objects/Refractor.js +7 -12
  58. package/objects/Water.cjs.js +1 -1
  59. package/objects/Water.js +5 -16
  60. package/package.json +2 -2
  61. package/postprocessing/GlitchPass.cjs.js +1 -1
  62. package/postprocessing/GlitchPass.js +36 -33
  63. package/postprocessing/SMAAPass.cjs.js +1 -1
  64. package/postprocessing/SMAAPass.js +93 -96
  65. package/postprocessing/SSAOPass.cjs.js +1 -1
  66. package/postprocessing/SSAOPass.js +151 -152
  67. package/postprocessing/SavePass.cjs.js +1 -1
  68. package/postprocessing/SavePass.js +27 -28
  69. package/renderers/nodes/accessors/UVNode.js +1 -3
  70. package/renderers/nodes/core/AttributeNode.js +1 -3
  71. package/renderers/nodes/core/Node.js +4 -12
  72. package/renderers/nodes/core/NodeBuilder.js +6 -18
  73. package/renderers/webgpu/WebGPUTextures.cjs.js +1 -1
  74. package/renderers/webgpu/WebGPUTextures.js +1 -2
  75. package/utils/LDrawUtils.cjs.js +1 -0
  76. package/utils/LDrawUtils.js +144 -0
  77. package/webxr/ARButton.js +6 -6
  78. package/webxr/VRButton.js +6 -6
@@ -1,4 +1,4 @@
1
- import { Vector3, Loader, FileLoader, Matrix4, MeshStandardMaterial, Color, MeshPhongMaterial, LineBasicMaterial, ShaderMaterial, UniformsUtils, UniformsLib, Group, BufferGeometry, Float32BufferAttribute, LineSegments, Mesh, BufferAttribute } from 'three';
1
+ import { Vector3, Ray, Loader, FileLoader, MeshStandardMaterial, LineBasicMaterial, ShaderMaterial, UniformsUtils, UniformsLib, Color, BufferGeometry, BufferAttribute, LineSegments, Mesh, Matrix4, Group } from 'three';
2
2
 
3
3
  // Note: "MATERIAL" tag (e.g. GLITTER, SPECKLE) is not implemented
4
4
 
@@ -17,186 +17,335 @@ const FILE_LOCATION_TRY_MODELS = 3;
17
17
  const FILE_LOCATION_TRY_RELATIVE = 4;
18
18
  const FILE_LOCATION_TRY_ABSOLUTE = 5;
19
19
  const FILE_LOCATION_NOT_FOUND = 6;
20
- const conditionalLineVertShader =
21
- /* glsl */
22
- `
23
- attribute vec3 control0;
24
- attribute vec3 control1;
25
- attribute vec3 direction;
26
- varying float discardFlag;
27
-
28
- #include <common>
29
- #include <color_pars_vertex>
30
- #include <fog_pars_vertex>
31
- #include <logdepthbuf_pars_vertex>
32
- #include <clipping_planes_pars_vertex>
33
- void main() {
34
- #include <color_vertex>
35
-
36
- vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
37
- gl_Position = projectionMatrix * mvPosition;
38
-
39
- // Transform the line segment ends and control points into camera clip space
40
- vec4 c0 = projectionMatrix * modelViewMatrix * vec4( control0, 1.0 );
41
- vec4 c1 = projectionMatrix * modelViewMatrix * vec4( control1, 1.0 );
42
- vec4 p0 = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
43
- vec4 p1 = projectionMatrix * modelViewMatrix * vec4( position + direction, 1.0 );
44
-
45
- c0.xy /= c0.w;
46
- c1.xy /= c1.w;
47
- p0.xy /= p0.w;
48
- p1.xy /= p1.w;
49
-
50
- // Get the direction of the segment and an orthogonal vector
51
- vec2 dir = p1.xy - p0.xy;
52
- vec2 norm = vec2( -dir.y, dir.x );
53
-
54
- // Get control point directions from the line
55
- vec2 c0dir = c0.xy - p1.xy;
56
- vec2 c1dir = c1.xy - p1.xy;
57
-
58
- // If the vectors to the controls points are pointed in different directions away
59
- // from the line segment then the line should not be drawn.
60
- float d0 = dot( normalize( norm ), normalize( c0dir ) );
61
- float d1 = dot( normalize( norm ), normalize( c1dir ) );
62
- discardFlag = float( sign( d0 ) != sign( d1 ) );
63
-
64
- #include <logdepthbuf_vertex>
65
- #include <clipping_planes_vertex>
66
- #include <fog_vertex>
67
- }
68
- `;
69
- const conditionalLineFragShader =
70
- /* glsl */
71
- `
72
- uniform vec3 diffuse;
73
- uniform float opacity;
74
- varying float discardFlag;
75
-
76
- #include <common>
77
- #include <color_pars_fragment>
78
- #include <fog_pars_fragment>
79
- #include <logdepthbuf_pars_fragment>
80
- #include <clipping_planes_pars_fragment>
81
- void main() {
82
-
83
- if ( discardFlag > 0.5 ) discard;
84
-
85
- #include <clipping_planes_fragment>
86
- vec3 outgoingLight = vec3( 0.0 );
87
- vec4 diffuseColor = vec4( diffuse, opacity );
88
- #include <logdepthbuf_fragment>
89
- #include <color_fragment>
90
- outgoingLight = diffuseColor.rgb; // simple shader
91
- gl_FragColor = vec4( outgoingLight, diffuseColor.a );
92
- #include <tonemapping_fragment>
93
- #include <encodings_fragment>
94
- #include <fog_fragment>
95
- #include <premultiplied_alpha_fragment>
96
- }
97
- `;
20
+ const MAIN_COLOUR_CODE = '16';
21
+ const MAIN_EDGE_COLOUR_CODE = '24';
98
22
 
99
23
  const _tempVec0 = new Vector3();
100
24
 
101
25
  const _tempVec1 = new Vector3();
102
26
 
103
- function smoothNormals(triangles, lineSegments) {
27
+ class LDrawConditionalLineMaterial extends ShaderMaterial {
28
+ constructor(parameters) {
29
+ super({
30
+ uniforms: UniformsUtils.merge([UniformsLib.fog, {
31
+ diffuse: {
32
+ value: new Color()
33
+ },
34
+ opacity: {
35
+ value: 1.0
36
+ }
37
+ }]),
38
+ vertexShader:
39
+ /* glsl */
40
+ `
41
+ attribute vec3 control0;
42
+ attribute vec3 control1;
43
+ attribute vec3 direction;
44
+ varying float discardFlag;
45
+
46
+ #include <common>
47
+ #include <color_pars_vertex>
48
+ #include <fog_pars_vertex>
49
+ #include <logdepthbuf_pars_vertex>
50
+ #include <clipping_planes_pars_vertex>
51
+
52
+ void main() {
53
+ #include <color_vertex>
54
+
55
+ vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
56
+ gl_Position = projectionMatrix * mvPosition;
57
+
58
+ // Transform the line segment ends and control points into camera clip space
59
+ vec4 c0 = projectionMatrix * modelViewMatrix * vec4(control0, 1.0);
60
+ vec4 c1 = projectionMatrix * modelViewMatrix * vec4(control1, 1.0);
61
+ vec4 p0 = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
62
+ vec4 p1 = projectionMatrix * modelViewMatrix * vec4(position + direction, 1.0);
63
+
64
+ c0.xy /= c0.w;
65
+ c1.xy /= c1.w;
66
+ p0.xy /= p0.w;
67
+ p1.xy /= p1.w;
68
+
69
+ // Get the direction of the segment and an orthogonal vector
70
+ vec2 dir = p1.xy - p0.xy;
71
+ vec2 norm = vec2(-dir.y, dir.x);
72
+
73
+ // Get control point directions from the line
74
+ vec2 c0dir = c0.xy - p1.xy;
75
+ vec2 c1dir = c1.xy - p1.xy;
76
+
77
+ // If the vectors to the controls points are pointed in different directions away
78
+ // from the line segment then the line should not be drawn.
79
+ float d0 = dot(normalize(norm), normalize(c0dir));
80
+ float d1 = dot(normalize(norm), normalize(c1dir));
81
+ discardFlag = float(sign(d0) != sign(d1));
82
+
83
+ #include <logdepthbuf_vertex>
84
+ #include <clipping_planes_vertex>
85
+ #include <fog_vertex>
86
+ }
87
+ `,
88
+ fragmentShader:
89
+ /* glsl */
90
+ `
91
+ uniform vec3 diffuse;
92
+ uniform float opacity;
93
+ varying float discardFlag;
94
+
95
+ #include <common>
96
+ #include <color_pars_fragment>
97
+ #include <fog_pars_fragment>
98
+ #include <logdepthbuf_pars_fragment>
99
+ #include <clipping_planes_pars_fragment>
100
+
101
+ void main() {
102
+ if (discardFlag > 0.5) discard;
103
+
104
+ #include <clipping_planes_fragment>
105
+ vec3 outgoingLight = vec3(0.0);
106
+ vec4 diffuseColor = vec4(diffuse, opacity);
107
+ #include <logdepthbuf_fragment>
108
+ #include <color_fragment>
109
+ outgoingLight = diffuseColor.rgb; // simple shader
110
+ gl_FragColor = vec4(outgoingLight, diffuseColor.a);
111
+ #include <tonemapping_fragment>
112
+ #include <encodings_fragment>
113
+ #include <fog_fragment>
114
+ #include <premultiplied_alpha_fragment>
115
+ }
116
+ `
117
+ });
118
+ Object.defineProperties(this, {
119
+ opacity: {
120
+ get: function () {
121
+ return this.uniforms.opacity.value;
122
+ },
123
+ set: function (value) {
124
+ this.uniforms.opacity.value = value;
125
+ }
126
+ },
127
+ color: {
128
+ get: function () {
129
+ return this.uniforms.diffuse.value;
130
+ }
131
+ }
132
+ });
133
+ this.setValues(parameters);
134
+ this.isLDrawConditionalLineMaterial = true;
135
+ }
136
+
137
+ }
138
+
139
+ class ConditionalLineSegments extends LineSegments {
140
+ constructor(geometry, material) {
141
+ super(geometry, material);
142
+ this.isConditionalLine = true;
143
+ }
144
+
145
+ }
146
+
147
+ function generateFaceNormals(faces) {
148
+ for (let i = 0, l = faces.length; i < l; i++) {
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 Vector3().crossVectors(_tempVec0, _tempVec1).normalize();
160
+ }
161
+ }
162
+
163
+ const _ray = new Ray();
164
+
165
+ function smoothNormals(faces, lineSegments, checkSubSegments = false) {
166
+ // NOTE: 1e2 is pretty coarse but was chosen to quantize the resulting value because
167
+ // it allows edges to be smoothed as expected (see minifig arms).
168
+ // --
169
+ // And the vector values are initialize multiplied by 1 + 1e-10 to account for floating
170
+ // point errors on vertices along quantization boundaries. Ie after matrix multiplication
171
+ // vertices that should be merged might be set to "1.7" and "1.6999..." meaning they won't
172
+ // get merged. This added epsilon attempts to push these error values to the same quantized
173
+ // value for the sake of hashing. See "AT-ST mini" dishes. See mrdoob/three#23169.
174
+ const hashMultiplier = (1 + 1e-10) * 1e2;
175
+
104
176
  function hashVertex(v) {
105
- // NOTE: 1e2 is pretty coarse but was chosen because it allows edges
106
- // to be smoothed as expected (see minifig arms). The errors between edges
107
- // could be due to matrix multiplication.
108
- const x = ~~(v.x * 1e2);
109
- const y = ~~(v.y * 1e2);
110
- const z = ~~(v.z * 1e2);
177
+ const x = ~~(v.x * hashMultiplier);
178
+ const y = ~~(v.y * hashMultiplier);
179
+ const z = ~~(v.z * hashMultiplier);
111
180
  return `${x},${y},${z}`;
112
181
  }
113
182
 
114
183
  function hashEdge(v0, v1) {
115
184
  return `${hashVertex(v0)}_${hashVertex(v1)}`;
185
+ } // converts the two vertices to a ray with a normalized direction and origin of 0, 0, 0 projected
186
+ // onto the original line.
187
+
188
+
189
+ function toNormalizedRay(v0, v1, targetRay) {
190
+ targetRay.direction.subVectors(v1, v0).normalize();
191
+ const scalar = v0.dot(targetRay.direction);
192
+ targetRay.origin.copy(v0).addScaledVector(targetRay.direction, -scalar);
193
+ return targetRay;
194
+ }
195
+
196
+ function hashRay(ray) {
197
+ return hashEdge(ray.origin, ray.direction);
116
198
  }
117
199
 
118
200
  const hardEdges = new Set();
201
+ const hardEdgeRays = new Map();
119
202
  const halfEdgeList = {};
120
- const fullHalfEdgeList = {};
121
203
  const normals = []; // Save the list of hard edges by hash
122
204
 
123
205
  for (let i = 0, l = lineSegments.length; i < l; i++) {
124
206
  const ls = lineSegments[i];
125
- const v0 = ls.v0;
126
- const v1 = ls.v1;
207
+ const vertices = ls.vertices;
208
+ const v0 = vertices[0];
209
+ const v1 = vertices[1];
127
210
  hardEdges.add(hashEdge(v0, v1));
128
- hardEdges.add(hashEdge(v1, v0));
211
+ hardEdges.add(hashEdge(v1, v0)); // only generate the hard edge ray map if we're checking subsegments because it's more expensive to check
212
+ // and requires more memory.
213
+
214
+ if (checkSubSegments) {
215
+ // add both ray directions to the map
216
+ const ray = toNormalizedRay(v0, v1, new Ray());
217
+ const rh1 = hashRay(ray);
218
+
219
+ if (!hardEdgeRays.has(rh1)) {
220
+ toNormalizedRay(v1, v0, ray);
221
+ const rh2 = hashRay(ray);
222
+ const info = {
223
+ ray,
224
+ distances: []
225
+ };
226
+ hardEdgeRays.set(rh1, info);
227
+ hardEdgeRays.set(rh2, info);
228
+ } // store both segments ends in min, max order in the distances array to check if a face edge is a
229
+ // subsegment later.
230
+
231
+
232
+ const info = hardEdgeRays.get(rh1);
233
+ let d0 = info.ray.direction.dot(v0);
234
+ let d1 = info.ray.direction.dot(v1);
235
+
236
+ if (d0 > d1) {
237
+ [d0, d1] = [d1, d0];
238
+ }
239
+
240
+ info.distances.push(d0, d1);
241
+ }
129
242
  } // track the half edges associated with each triangle
130
243
 
131
244
 
132
- for (let i = 0, l = triangles.length; i < l; i++) {
133
- const tri = triangles[i];
245
+ for (let i = 0, l = faces.length; i < l; i++) {
246
+ const tri = faces[i];
247
+ const vertices = tri.vertices;
248
+ const vertCount = vertices.length;
134
249
 
135
- for (let i2 = 0, l2 = 3; i2 < l2; i2++) {
250
+ for (let i2 = 0; i2 < vertCount; i2++) {
136
251
  const index = i2;
137
- const next = (i2 + 1) % 3;
138
- const v0 = tri[`v${index}`];
139
- const v1 = tri[`v${next}`];
252
+ const next = (i2 + 1) % vertCount;
253
+ const v0 = vertices[index];
254
+ const v1 = vertices[next];
140
255
  const hash = hashEdge(v0, v1); // don't add the triangle if the edge is supposed to be hard
141
256
 
142
- if (hardEdges.has(hash)) continue;
143
- halfEdgeList[hash] = tri;
144
- fullHalfEdgeList[hash] = tri;
257
+ if (hardEdges.has(hash)) {
258
+ continue;
259
+ } // if checking subsegments then check to see if this edge lies on a hard edge ray and whether its within any ray bounds
260
+
261
+
262
+ if (checkSubSegments) {
263
+ toNormalizedRay(v0, v1, _ray);
264
+ const rayHash = hashRay(_ray);
265
+
266
+ if (hardEdgeRays.has(rayHash)) {
267
+ const info = hardEdgeRays.get(rayHash);
268
+ const {
269
+ ray,
270
+ distances
271
+ } = info;
272
+ let d0 = ray.direction.dot(v0);
273
+ let d1 = ray.direction.dot(v1);
274
+
275
+ if (d0 > d1) {
276
+ [d0, d1] = [d1, d0];
277
+ } // return early if the face edge is found to be a subsegment of a line edge meaning the edge will have "hard" normals
278
+
279
+
280
+ let found = false;
281
+
282
+ for (let i = 0, l = distances.length; i < l; i += 2) {
283
+ if (d0 >= distances[i] && d1 <= distances[i + 1]) {
284
+ found = true;
285
+ break;
286
+ }
287
+ }
288
+
289
+ if (found) {
290
+ continue;
291
+ }
292
+ }
293
+ }
294
+
295
+ const info = {
296
+ index: index,
297
+ tri: tri
298
+ };
299
+ halfEdgeList[hash] = info;
145
300
  }
146
- } // NOTE: Some of the normals wind up being skewed in an unexpected way because
147
- // quads provide more "influence" to some vertex normals than a triangle due to
148
- // the fact that a quad is made up of two triangles and all triangles are weighted
149
- // equally. To fix this quads could be tracked separately so their vertex normals
150
- // are weighted appropriately or we could try only adding a normal direction
151
- // once per normal.
152
- // Iterate until we've tried to connect all triangles to share normals
301
+ } // Iterate until we've tried to connect all faces to share normals
153
302
 
154
303
 
155
304
  while (true) {
156
- // Stop if there are no more triangles left
157
- const halfEdges = Object.keys(halfEdgeList);
158
- if (halfEdges.length === 0) break; // Exhaustively find all connected triangles
305
+ // Stop if there are no more faces left
306
+ let halfEdge = null;
159
307
 
160
- let i = 0;
161
- const queue = [fullHalfEdgeList[halfEdges[0]]];
308
+ for (const key in halfEdgeList) {
309
+ halfEdge = halfEdgeList[key];
310
+ break;
311
+ }
162
312
 
163
- while (i < queue.length) {
164
- // initialize all vertex normals in this triangle
165
- const tri = queue[i];
166
- i++;
167
- const faceNormal = tri.faceNormal;
313
+ if (halfEdge === null) {
314
+ break;
315
+ } // Exhaustively find all connected faces
168
316
 
169
- if (tri.n0 === null) {
170
- tri.n0 = faceNormal.clone();
171
- normals.push(tri.n0);
172
- }
173
317
 
174
- if (tri.n1 === null) {
175
- tri.n1 = faceNormal.clone();
176
- normals.push(tri.n1);
177
- }
318
+ const queue = [halfEdge];
178
319
 
179
- if (tri.n2 === null) {
180
- tri.n2 = faceNormal.clone();
181
- normals.push(tri.n2);
182
- } // Check if any edge is connected to another triangle edge
320
+ while (queue.length > 0) {
321
+ // initialize all vertex normals in this triangle
322
+ const tri = queue.pop().tri;
323
+ const vertices = tri.vertices;
324
+ const vertNormals = tri.normals;
325
+ const faceNormal = tri.faceNormal; // Check if any edge is connected to another triangle edge
183
326
 
327
+ const vertCount = vertices.length;
184
328
 
185
- for (let i2 = 0, l2 = 3; i2 < l2; i2++) {
329
+ for (let i2 = 0; i2 < vertCount; i2++) {
186
330
  const index = i2;
187
- const next = (i2 + 1) % 3;
188
- const v0 = tri[`v${index}`];
189
- const v1 = tri[`v${next}`]; // delete this triangle from the list so it won't be found again
331
+ const next = (i2 + 1) % vertCount;
332
+ const v0 = vertices[index];
333
+ const v1 = vertices[next]; // delete this triangle from the list so it won't be found again
190
334
 
191
335
  const hash = hashEdge(v0, v1);
192
336
  delete halfEdgeList[hash];
193
337
  const reverseHash = hashEdge(v1, v0);
194
- const otherTri = fullHalfEdgeList[reverseHash];
195
-
196
- if (otherTri) {
197
- // NOTE: If the angle between triangles is > 67.5 degrees then assume it's
338
+ const otherInfo = halfEdgeList[reverseHash];
339
+
340
+ if (otherInfo) {
341
+ const otherTri = otherInfo.tri;
342
+ const otherIndex = otherInfo.index;
343
+ const otherNormals = otherTri.normals;
344
+ const otherVertCount = otherNormals.length;
345
+ const otherFaceNormal = otherTri.faceNormal; // NOTE: If the angle between faces is > 67.5 degrees then assume it's
198
346
  // hard edge. There are some cases where the line segments do not line up exactly
199
347
  // with or span multiple triangle edges (see Lunar Vehicle wheels).
348
+
200
349
  if (Math.abs(otherTri.faceNormal.dot(tri.faceNormal)) < 0.25) {
201
350
  continue;
202
351
  } // if this triangle has already been traversed then it won't be in
@@ -205,33 +354,63 @@ function smoothNormals(triangles, lineSegments) {
205
354
 
206
355
 
207
356
  if (reverseHash in halfEdgeList) {
208
- queue.push(otherTri);
357
+ queue.push(otherInfo);
209
358
  delete halfEdgeList[reverseHash];
210
- } // Find the matching edge in this triangle and copy the normal vector over
359
+ } // share the first normal
360
+
211
361
 
362
+ const otherNext = (otherIndex + 1) % otherVertCount;
212
363
 
213
- for (let i3 = 0, l3 = 3; i3 < l3; i3++) {
214
- const otherIndex = i3;
215
- const otherNext = (i3 + 1) % 3;
216
- const otherV0 = otherTri[`v${otherIndex}`];
217
- const otherV1 = otherTri[`v${otherNext}`];
218
- const otherHash = hashEdge(otherV0, otherV1);
364
+ if (vertNormals[index] && otherNormals[otherNext] && vertNormals[index] !== otherNormals[otherNext]) {
365
+ otherNormals[otherNext].norm.add(vertNormals[index].norm);
366
+ vertNormals[index].norm = otherNormals[otherNext].norm;
367
+ }
219
368
 
220
- if (otherHash === reverseHash) {
221
- if (otherTri[`n${otherIndex}`] === null) {
222
- const norm = tri[`n${next}`];
223
- otherTri[`n${otherIndex}`] = norm;
224
- norm.add(otherTri.faceNormal);
225
- }
369
+ let sharedNormal1 = vertNormals[index] || otherNormals[otherNext];
226
370
 
227
- if (otherTri[`n${otherNext}`] === null) {
228
- const norm = tri[`n${index}`];
229
- otherTri[`n${otherNext}`] = norm;
230
- norm.add(otherTri.faceNormal);
231
- }
371
+ if (sharedNormal1 === null) {
372
+ // it's possible to encounter an edge of a triangle that has already been traversed meaning
373
+ // both edges already have different normals defined and shared. To work around this we create
374
+ // a wrapper object so when those edges are merged the normals can be updated everywhere.
375
+ sharedNormal1 = {
376
+ norm: new Vector3()
377
+ };
378
+ normals.push(sharedNormal1.norm);
379
+ }
232
380
 
233
- break;
234
- }
381
+ if (vertNormals[index] === null) {
382
+ vertNormals[index] = sharedNormal1;
383
+ sharedNormal1.norm.add(faceNormal);
384
+ }
385
+
386
+ if (otherNormals[otherNext] === null) {
387
+ otherNormals[otherNext] = sharedNormal1;
388
+ sharedNormal1.norm.add(otherFaceNormal);
389
+ } // share the second normal
390
+
391
+
392
+ if (vertNormals[next] && otherNormals[otherIndex] && vertNormals[next] !== otherNormals[otherIndex]) {
393
+ otherNormals[otherIndex].norm.add(vertNormals[next].norm);
394
+ vertNormals[next].norm = otherNormals[otherIndex].norm;
395
+ }
396
+
397
+ let sharedNormal2 = vertNormals[next] || otherNormals[otherIndex];
398
+
399
+ if (sharedNormal2 === null) {
400
+ sharedNormal2 = {
401
+ norm: new Vector3()
402
+ };
403
+ normals.push(sharedNormal2.norm);
404
+ }
405
+
406
+ if (vertNormals[next] === null) {
407
+ vertNormals[next] = sharedNormal2;
408
+ sharedNormal2.norm.add(faceNormal);
409
+ }
410
+
411
+ if (otherNormals[otherIndex] === null) {
412
+ otherNormals[otherIndex] = sharedNormal2;
413
+ sharedNormal2.norm.add(otherFaceNormal);
235
414
  }
236
415
  }
237
416
  }
@@ -244,6 +423,10 @@ function smoothNormals(triangles, lineSegments) {
244
423
  }
245
424
  }
246
425
 
426
+ function isPartType(type) {
427
+ return type === 'Part' || type === 'Unofficial_Part';
428
+ }
429
+
247
430
  function isPrimitiveType(type) {
248
431
  return /primitive/i.test(type) || type === 'Subpart';
249
432
  }
@@ -287,6 +470,10 @@ class LineParser {
287
470
  return this.line.substring(pos0, pos1);
288
471
  }
289
472
 
473
+ getVector() {
474
+ return new Vector3(parseFloat(this.getToken()), parseFloat(this.getToken()), parseFloat(this.getToken()));
475
+ }
476
+
290
477
  getRemainingString() {
291
478
  return this.line.substring(this.currentCharIndex, this.lineLength);
292
479
  }
@@ -303,1193 +490,1351 @@ class LineParser {
303
490
  return this.lineNumber >= 0 ? ' at line ' + this.lineNumber : '';
304
491
  }
305
492
 
306
- }
493
+ } // Fetches and parses an intermediate representation of LDraw parts files.
307
494
 
308
- function sortByMaterial(a, b) {
309
- if (a.colourCode === b.colourCode) {
310
- return 0;
495
+
496
+ class LDrawParsedCache {
497
+ constructor(loader) {
498
+ this.loader = loader;
499
+ this._cache = {};
311
500
  }
312
501
 
313
- if (a.colourCode < b.colourCode) {
314
- return -1;
502
+ cloneResult(original) {
503
+ const result = {}; // vertices are transformed and normals computed before being converted to geometry
504
+ // so these pieces must be cloned.
505
+
506
+ result.faces = original.faces.map(face => {
507
+ return {
508
+ colorCode: face.colorCode,
509
+ material: face.material,
510
+ vertices: face.vertices.map(v => v.clone()),
511
+ normals: face.normals.map(() => null),
512
+ faceNormal: null
513
+ };
514
+ });
515
+ result.conditionalSegments = original.conditionalSegments.map(face => {
516
+ return {
517
+ colorCode: face.colorCode,
518
+ material: face.material,
519
+ vertices: face.vertices.map(v => v.clone()),
520
+ controlPoints: face.controlPoints.map(v => v.clone())
521
+ };
522
+ });
523
+ result.lineSegments = original.lineSegments.map(face => {
524
+ return {
525
+ colorCode: face.colorCode,
526
+ material: face.material,
527
+ vertices: face.vertices.map(v => v.clone())
528
+ };
529
+ }); // none if this is subsequently modified
530
+
531
+ result.type = original.type;
532
+ result.category = original.category;
533
+ result.keywords = original.keywords;
534
+ result.subobjects = original.subobjects;
535
+ result.totalFaces = original.totalFaces;
536
+ result.startingConstructionStep = original.startingConstructionStep;
537
+ result.materials = original.materials;
538
+ result.group = null;
539
+ return result;
315
540
  }
316
541
 
317
- return 1;
318
- }
542
+ async fetchData(fileName) {
543
+ let triedLowerCase = false;
544
+ let locationState = FILE_LOCATION_AS_IS;
319
545
 
320
- function createObject(elements, elementSize, isConditionalSegments) {
321
- // Creates a LineSegments (elementSize = 2) or a Mesh (elementSize = 3 )
322
- // With per face / segment material, implemented with mesh groups and materials array
323
- // Sort the triangles or line segments by colour code to make later the mesh groups
324
- elements.sort(sortByMaterial);
325
- const positions = [];
326
- const normals = [];
327
- const materials = [];
328
- const bufferGeometry = new BufferGeometry();
329
- let prevMaterial = null;
330
- let index0 = 0;
331
- let numGroupVerts = 0;
546
+ while (locationState !== FILE_LOCATION_NOT_FOUND) {
547
+ let subobjectURL = fileName;
332
548
 
333
- for (let iElem = 0, nElem = elements.length; iElem < nElem; iElem++) {
334
- const elem = elements[iElem];
335
- const v0 = elem.v0;
336
- const v1 = elem.v1; // Note that LDraw coordinate system is rotated 180 deg. in the X axis w.r.t. Three.js's one
549
+ switch (locationState) {
550
+ case FILE_LOCATION_AS_IS:
551
+ locationState = locationState + 1;
552
+ break;
337
553
 
338
- positions.push(v0.x, v0.y, v0.z, v1.x, v1.y, v1.z);
554
+ case FILE_LOCATION_TRY_PARTS:
555
+ subobjectURL = 'parts/' + subobjectURL;
556
+ locationState = locationState + 1;
557
+ break;
339
558
 
340
- if (elementSize === 3) {
341
- positions.push(elem.v2.x, elem.v2.y, elem.v2.z);
342
- const n0 = elem.n0 || elem.faceNormal;
343
- const n1 = elem.n1 || elem.faceNormal;
344
- const n2 = elem.n2 || elem.faceNormal;
345
- normals.push(n0.x, n0.y, n0.z);
346
- normals.push(n1.x, n1.y, n1.z);
347
- normals.push(n2.x, n2.y, n2.z);
348
- }
559
+ case FILE_LOCATION_TRY_P:
560
+ subobjectURL = 'p/' + subobjectURL;
561
+ locationState = locationState + 1;
562
+ break;
349
563
 
350
- if (prevMaterial !== elem.material) {
351
- if (prevMaterial !== null) {
352
- bufferGeometry.addGroup(index0, numGroupVerts, materials.length - 1);
353
- }
564
+ case FILE_LOCATION_TRY_MODELS:
565
+ subobjectURL = 'models/' + subobjectURL;
566
+ locationState = locationState + 1;
567
+ break;
354
568
 
355
- materials.push(elem.material);
356
- prevMaterial = elem.material;
357
- index0 = iElem * elementSize;
358
- numGroupVerts = elementSize;
359
- } else {
360
- numGroupVerts += elementSize;
361
- }
362
- }
569
+ case FILE_LOCATION_TRY_RELATIVE:
570
+ subobjectURL = fileName.substring(0, fileName.lastIndexOf('/') + 1) + subobjectURL;
571
+ locationState = locationState + 1;
572
+ break;
363
573
 
364
- if (numGroupVerts > 0) {
365
- bufferGeometry.addGroup(index0, Infinity, materials.length - 1);
366
- }
574
+ case FILE_LOCATION_TRY_ABSOLUTE:
575
+ if (triedLowerCase) {
576
+ // Try absolute path
577
+ locationState = FILE_LOCATION_NOT_FOUND;
578
+ } else {
579
+ // Next attempt is lower case
580
+ fileName = fileName.toLowerCase();
581
+ subobjectURL = fileName;
582
+ triedLowerCase = true;
583
+ locationState = FILE_LOCATION_AS_IS;
584
+ }
367
585
 
368
- bufferGeometry.setAttribute('position', new Float32BufferAttribute(positions, 3));
586
+ break;
587
+ }
369
588
 
370
- if (elementSize === 3) {
371
- bufferGeometry.setAttribute('normal', new Float32BufferAttribute(normals, 3));
372
- }
589
+ const loader = this.loader;
590
+ const fileLoader = new FileLoader(loader.manager);
591
+ fileLoader.setPath(loader.partsLibraryPath);
592
+ fileLoader.setRequestHeader(loader.requestHeader);
593
+ fileLoader.setWithCredentials(loader.withCredentials);
373
594
 
374
- let object3d = null;
595
+ try {
596
+ const text = await fileLoader.loadAsync(subobjectURL);
597
+ return text;
598
+ } catch {
599
+ continue;
600
+ }
601
+ }
375
602
 
376
- if (elementSize === 2) {
377
- object3d = new LineSegments(bufferGeometry, materials);
378
- } else if (elementSize === 3) {
379
- object3d = new Mesh(bufferGeometry, materials);
603
+ throw new Error('LDrawLoader: Subobject "' + fileName + '" could not be loaded.');
380
604
  }
381
605
 
382
- if (isConditionalSegments) {
383
- object3d.isConditionalLine = true;
384
- const controlArray0 = new Float32Array(elements.length * 3 * 2);
385
- const controlArray1 = new Float32Array(elements.length * 3 * 2);
386
- const directionArray = new Float32Array(elements.length * 3 * 2);
606
+ parse(text, fileName = null) {
607
+ const loader = this.loader; // final results
387
608
 
388
- for (let i = 0, l = elements.length; i < l; i++) {
389
- const os = elements[i];
390
- const c0 = os.c0;
391
- const c1 = os.c1;
392
- const v0 = os.v0;
393
- const v1 = os.v1;
394
- const index = i * 3 * 2;
395
- controlArray0[index + 0] = c0.x;
396
- controlArray0[index + 1] = c0.y;
397
- controlArray0[index + 2] = c0.z;
398
- controlArray0[index + 3] = c0.x;
399
- controlArray0[index + 4] = c0.y;
400
- controlArray0[index + 5] = c0.z;
401
- controlArray1[index + 0] = c1.x;
402
- controlArray1[index + 1] = c1.y;
403
- controlArray1[index + 2] = c1.z;
404
- controlArray1[index + 3] = c1.x;
405
- controlArray1[index + 4] = c1.y;
406
- controlArray1[index + 5] = c1.z;
407
- directionArray[index + 0] = v1.x - v0.x;
408
- directionArray[index + 1] = v1.y - v0.y;
409
- directionArray[index + 2] = v1.z - v0.z;
410
- directionArray[index + 3] = v1.x - v0.x;
411
- directionArray[index + 4] = v1.y - v0.y;
412
- directionArray[index + 5] = v1.z - v0.z;
413
- }
609
+ const faces = [];
610
+ const lineSegments = [];
611
+ const conditionalSegments = [];
612
+ const subobjects = [];
613
+ const materials = {};
414
614
 
415
- bufferGeometry.setAttribute('control0', new BufferAttribute(controlArray0, 3, false));
416
- bufferGeometry.setAttribute('control1', new BufferAttribute(controlArray1, 3, false));
417
- bufferGeometry.setAttribute('direction', new BufferAttribute(directionArray, 3, false));
418
- }
615
+ const getLocalMaterial = colorCode => {
616
+ return materials[colorCode] || null;
617
+ };
419
618
 
420
- return object3d;
421
- } //
619
+ let type = 'Model';
620
+ let category = null;
621
+ let keywords = null;
622
+ let totalFaces = 0; // split into lines
422
623
 
624
+ if (text.indexOf('\r\n') !== -1) {
625
+ // This is faster than String.split with regex that splits on both
626
+ text = text.replace(/\r\n/g, '\n');
627
+ }
423
628
 
424
- class LDrawLoader extends Loader {
425
- constructor(manager) {
426
- super(manager); // This is a stack of 'parse scopes' with one level per subobject loaded file.
427
- // Each level contains a material lib and also other runtime variables passed between parent and child subobjects
428
- // When searching for a material code, the stack is read from top of the stack to bottom
429
- // Each material library is an object map keyed by colour codes.
629
+ const lines = text.split('\n');
630
+ const numLines = lines.length;
631
+ let parsingEmbeddedFiles = false;
632
+ let currentEmbeddedFileName = null;
633
+ let currentEmbeddedText = null;
634
+ let bfcCertified = false;
635
+ let bfcCCW = true;
636
+ let bfcInverted = false;
637
+ let bfcCull = true;
638
+ let startingConstructionStep = false; // Parse all line commands
430
639
 
431
- this.parseScopesStack = null; // Array of THREE.Material
640
+ for (let lineIndex = 0; lineIndex < numLines; lineIndex++) {
641
+ const line = lines[lineIndex];
642
+ if (line.length === 0) continue;
432
643
 
433
- this.materials = []; // Not using THREE.Cache here because it returns the previous HTML error response instead of calling onError()
434
- // This also allows to handle the embedded text files ("0 FILE" lines)
644
+ if (parsingEmbeddedFiles) {
645
+ if (line.startsWith('0 FILE ')) {
646
+ // Save previous embedded file in the cache
647
+ this.setData(currentEmbeddedFileName, currentEmbeddedText); // New embedded text file
435
648
 
436
- this.subobjectCache = {}; // This object is a map from file names to paths. It agilizes the paths search. If it is not set then files will be searched by trial and error.
649
+ currentEmbeddedFileName = line.substring(7);
650
+ currentEmbeddedText = '';
651
+ } else {
652
+ currentEmbeddedText += line + '\n';
653
+ }
437
654
 
438
- this.fileMap = null; // Add default main triangle and line edge materials (used in piecess that can be coloured with a main color)
655
+ continue;
656
+ }
439
657
 
440
- 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.
441
- // If not (the default), only one object which contains all the merged primitives will be created.
658
+ const lp = new LineParser(line, lineIndex + 1);
659
+ lp.seekNonSpace();
442
660
 
443
- this.separateObjects = false; // If this flag is set to true the vertex normals will be smoothed.
661
+ if (lp.isAtTheEnd()) {
662
+ // Empty line
663
+ continue;
664
+ } // Parse the line type
444
665
 
445
- this.smoothNormals = true;
446
- }
447
666
 
448
- load(url, onLoad, onProgress, onError) {
449
- if (!this.fileMap) {
450
- this.fileMap = {};
451
- }
667
+ const lineType = lp.getToken();
668
+ let material;
669
+ let colorCode;
670
+ let segment;
671
+ let ccw;
672
+ let doubleSided;
673
+ let v0, v1, v2, v3, c0, c1;
452
674
 
453
- const scope = this;
454
- const fileLoader = new FileLoader(this.manager);
455
- fileLoader.setPath(this.path);
456
- fileLoader.setRequestHeader(this.requestHeader);
457
- fileLoader.setWithCredentials(this.withCredentials);
458
- fileLoader.load(url, function (text) {
459
- scope.processObject(text, onLoad, null, url);
460
- }, onProgress, onError);
461
- }
675
+ switch (lineType) {
676
+ // Line type 0: Comment or META
677
+ case '0':
678
+ // Parse meta directive
679
+ const meta = lp.getToken();
462
680
 
463
- parse(text, path, onLoad) {
464
- // Async parse. This function calls onParse with the parsed THREE.Object3D as parameter
465
- this.processObject(text, onLoad, null, path);
466
- }
681
+ if (meta) {
682
+ switch (meta) {
683
+ case '!LDRAW_ORG':
684
+ type = lp.getToken();
685
+ break;
467
686
 
468
- setMaterials(materials) {
469
- // Clears parse scopes stack, adds new scope with material library
470
- this.parseScopesStack = [];
471
- this.newParseScopeLevel(materials);
472
- this.getCurrentParseScope().isFromParse = false;
473
- this.materials = materials;
474
- return this;
475
- }
687
+ case '!COLOUR':
688
+ material = loader.parseColorMetaDirective(lp);
476
689
 
477
- setFileMap(fileMap) {
478
- this.fileMap = fileMap;
479
- return this;
480
- }
690
+ if (material) {
691
+ materials[material.userData.code] = material;
692
+ } else {
693
+ console.warn('LDrawLoader: Error parsing material' + lp.getLineNumberString());
694
+ }
481
695
 
482
- newParseScopeLevel(materials) {
483
- // Adds a new scope level, assign materials to it and returns it
484
- const matLib = {};
696
+ break;
485
697
 
486
- if (materials) {
487
- for (let i = 0, n = materials.length; i < n; i++) {
488
- const material = materials[i];
489
- matLib[material.userData.code] = material;
490
- }
491
- }
698
+ case '!CATEGORY':
699
+ category = lp.getToken();
700
+ break;
492
701
 
493
- const topParseScope = this.getCurrentParseScope();
494
- const newParseScope = {
495
- lib: matLib,
496
- url: null,
497
- // Subobjects
498
- subobjects: null,
499
- numSubobjects: 0,
500
- subobjectIndex: 0,
501
- inverted: false,
502
- category: null,
503
- keywords: null,
504
- // Current subobject
505
- currentFileName: null,
506
- mainColourCode: topParseScope ? topParseScope.mainColourCode : '16',
507
- mainEdgeColourCode: topParseScope ? topParseScope.mainEdgeColourCode : '24',
508
- currentMatrix: new Matrix4(),
509
- matrix: new Matrix4(),
510
- // If false, it is a root material scope previous to parse
511
- isFromParse: true,
512
- triangles: null,
513
- lineSegments: null,
514
- conditionalSegments: null,
515
- // If true, this object is the start of a construction step
516
- startingConstructionStep: false
517
- };
518
- this.parseScopesStack.push(newParseScope);
519
- return newParseScope;
520
- }
702
+ case '!KEYWORDS':
703
+ const newKeywords = lp.getRemainingString().split(',');
521
704
 
522
- removeScopeLevel() {
523
- this.parseScopesStack.pop();
524
- return this;
525
- }
705
+ if (newKeywords.length > 0) {
706
+ if (!keywords) {
707
+ keywords = [];
708
+ }
526
709
 
527
- addMaterial(material) {
528
- // Adds a material to the material library which is on top of the parse scopes stack. And also to the materials array
529
- const matLib = this.getCurrentParseScope().lib;
530
-
531
- if (!matLib[material.userData.code]) {
532
- this.materials.push(material);
533
- }
534
-
535
- matLib[material.userData.code] = material;
536
- return this;
537
- }
710
+ newKeywords.forEach(function (keyword) {
711
+ keywords.push(keyword.trim());
712
+ });
713
+ }
538
714
 
539
- getMaterial(colourCode) {
540
- // Given a colour code search its material in the parse scopes stack
541
- if (colourCode.startsWith('0x2')) {
542
- // Special 'direct' material value (RGB colour)
543
- const colour = colourCode.substring(3);
544
- return this.parseColourMetaDirective(new LineParser('Direct_Color_' + colour + ' CODE -1 VALUE #' + colour + ' EDGE #' + colour + ''));
545
- }
715
+ break;
546
716
 
547
- for (let i = this.parseScopesStack.length - 1; i >= 0; i--) {
548
- const material = this.parseScopesStack[i].lib[colourCode];
717
+ case 'FILE':
718
+ if (lineIndex > 0) {
719
+ // Start embedded text files parsing
720
+ parsingEmbeddedFiles = true;
721
+ currentEmbeddedFileName = lp.getRemainingString();
722
+ currentEmbeddedText = '';
723
+ bfcCertified = false;
724
+ bfcCCW = true;
725
+ }
549
726
 
550
- if (material) {
551
- return material;
552
- }
553
- } // Material was not found
727
+ break;
554
728
 
729
+ case 'BFC':
730
+ // Changes to the backface culling state
731
+ while (!lp.isAtTheEnd()) {
732
+ const token = lp.getToken();
555
733
 
556
- return null;
557
- }
734
+ switch (token) {
735
+ case 'CERTIFY':
736
+ case 'NOCERTIFY':
737
+ bfcCertified = token === 'CERTIFY';
738
+ bfcCCW = true;
739
+ break;
558
740
 
559
- getParentParseScope() {
560
- if (this.parseScopesStack.length > 1) {
561
- return this.parseScopesStack[this.parseScopesStack.length - 2];
562
- }
741
+ case 'CW':
742
+ case 'CCW':
743
+ bfcCCW = token === 'CCW';
744
+ break;
563
745
 
564
- return null;
565
- }
746
+ case 'INVERTNEXT':
747
+ bfcInverted = true;
748
+ break;
566
749
 
567
- getCurrentParseScope() {
568
- if (this.parseScopesStack.length > 0) {
569
- return this.parseScopesStack[this.parseScopesStack.length - 1];
570
- }
750
+ case 'CLIP':
751
+ case 'NOCLIP':
752
+ bfcCull = token === 'CLIP';
753
+ break;
571
754
 
572
- return null;
573
- }
755
+ default:
756
+ console.warn('THREE.LDrawLoader: BFC directive "' + token + '" is unknown.');
757
+ break;
758
+ }
759
+ }
574
760
 
575
- parseColourMetaDirective(lineParser) {
576
- // Parses a colour definition and returns a THREE.Material or null if error
577
- let code = null; // Triangle and line colours
761
+ break;
578
762
 
579
- let colour = 0xff00ff;
580
- let edgeColour = 0xff00ff; // Transparency
763
+ case 'STEP':
764
+ startingConstructionStep = true;
765
+ break;
766
+ }
767
+ }
581
768
 
582
- let alpha = 1;
583
- let isTransparent = false; // Self-illumination:
769
+ break;
770
+ // Line type 1: Sub-object file
584
771
 
585
- let luminance = 0;
586
- let finishType = FINISH_TYPE_DEFAULT;
587
- let canHaveEnvMap = true;
588
- let edgeMaterial = null;
589
- const name = lineParser.getToken();
772
+ case '1':
773
+ colorCode = lp.getToken();
774
+ material = getLocalMaterial(colorCode);
775
+ const posX = parseFloat(lp.getToken());
776
+ const posY = parseFloat(lp.getToken());
777
+ const posZ = parseFloat(lp.getToken());
778
+ const m0 = parseFloat(lp.getToken());
779
+ const m1 = parseFloat(lp.getToken());
780
+ const m2 = parseFloat(lp.getToken());
781
+ const m3 = parseFloat(lp.getToken());
782
+ const m4 = parseFloat(lp.getToken());
783
+ const m5 = parseFloat(lp.getToken());
784
+ const m6 = parseFloat(lp.getToken());
785
+ const m7 = parseFloat(lp.getToken());
786
+ const m8 = parseFloat(lp.getToken());
787
+ const matrix = new Matrix4().set(m0, m1, m2, posX, m3, m4, m5, posY, m6, m7, m8, posZ, 0, 0, 0, 1);
788
+ let fileName = lp.getRemainingString().trim().replace(/\\/g, '/');
590
789
 
591
- if (!name) {
592
- throw 'LDrawLoader: Material name was expected after "!COLOUR tag' + lineParser.getLineNumberString() + '.';
593
- } // Parse tag tokens and their parameters
790
+ if (loader.fileMap[fileName]) {
791
+ // Found the subobject path in the preloaded file path map
792
+ fileName = loader.fileMap[fileName];
793
+ } else {
794
+ // Standardized subfolders
795
+ if (fileName.startsWith('s/')) {
796
+ fileName = 'parts/' + fileName;
797
+ } else if (fileName.startsWith('48/')) {
798
+ fileName = 'p/' + fileName;
799
+ }
800
+ }
594
801
 
802
+ subobjects.push({
803
+ material: material,
804
+ colorCode: colorCode,
805
+ matrix: matrix,
806
+ fileName: fileName,
807
+ inverted: bfcInverted,
808
+ startingConstructionStep: startingConstructionStep
809
+ });
810
+ bfcInverted = false;
811
+ break;
812
+ // Line type 2: Line segment
595
813
 
596
- let token = null;
814
+ case '2':
815
+ colorCode = lp.getToken();
816
+ material = getLocalMaterial(colorCode);
817
+ v0 = lp.getVector();
818
+ v1 = lp.getVector();
819
+ segment = {
820
+ material: material,
821
+ colorCode: colorCode,
822
+ vertices: [v0, v1]
823
+ };
824
+ lineSegments.push(segment);
825
+ break;
826
+ // Line type 5: Conditional Line segment
597
827
 
598
- while (true) {
599
- token = lineParser.getToken();
828
+ case '5':
829
+ colorCode = lp.getToken();
830
+ material = getLocalMaterial(colorCode);
831
+ v0 = lp.getVector();
832
+ v1 = lp.getVector();
833
+ c0 = lp.getVector();
834
+ c1 = lp.getVector();
835
+ segment = {
836
+ material: material,
837
+ colorCode: colorCode,
838
+ vertices: [v0, v1],
839
+ controlPoints: [c0, c1]
840
+ };
841
+ conditionalSegments.push(segment);
842
+ break;
843
+ // Line type 3: Triangle
600
844
 
601
- if (!token) {
602
- break;
603
- }
845
+ case '3':
846
+ colorCode = lp.getToken();
847
+ material = getLocalMaterial(colorCode);
848
+ ccw = bfcCCW;
849
+ doubleSided = !bfcCertified || !bfcCull;
604
850
 
605
- switch (token.toUpperCase()) {
606
- case 'CODE':
607
- code = lineParser.getToken();
608
- break;
851
+ if (ccw === true) {
852
+ v0 = lp.getVector();
853
+ v1 = lp.getVector();
854
+ v2 = lp.getVector();
855
+ } else {
856
+ v2 = lp.getVector();
857
+ v1 = lp.getVector();
858
+ v0 = lp.getVector();
859
+ }
609
860
 
610
- case 'VALUE':
611
- colour = lineParser.getToken();
861
+ faces.push({
862
+ material: material,
863
+ colorCode: colorCode,
864
+ faceNormal: null,
865
+ vertices: [v0, v1, v2],
866
+ normals: [null, null, null]
867
+ });
868
+ totalFaces++;
612
869
 
613
- if (colour.startsWith('0x')) {
614
- colour = '#' + colour.substring(2);
615
- } else if (!colour.startsWith('#')) {
616
- throw 'LDrawLoader: Invalid colour while parsing material' + lineParser.getLineNumberString() + '.';
870
+ if (doubleSided === true) {
871
+ faces.push({
872
+ material: material,
873
+ colorCode: colorCode,
874
+ faceNormal: null,
875
+ vertices: [v2, v1, v0],
876
+ normals: [null, null, null]
877
+ });
878
+ totalFaces++;
617
879
  }
618
880
 
619
881
  break;
882
+ // Line type 4: Quadrilateral
620
883
 
621
- case 'EDGE':
622
- edgeColour = lineParser.getToken();
884
+ case '4':
885
+ colorCode = lp.getToken();
886
+ material = getLocalMaterial(colorCode);
887
+ ccw = bfcCCW;
888
+ doubleSided = !bfcCertified || !bfcCull;
623
889
 
624
- if (edgeColour.startsWith('0x')) {
625
- edgeColour = '#' + edgeColour.substring(2);
626
- } else if (!edgeColour.startsWith('#')) {
627
- // Try to see if edge colour is a colour code
628
- edgeMaterial = this.getMaterial(edgeColour);
890
+ if (ccw === true) {
891
+ v0 = lp.getVector();
892
+ v1 = lp.getVector();
893
+ v2 = lp.getVector();
894
+ v3 = lp.getVector();
895
+ } else {
896
+ v3 = lp.getVector();
897
+ v2 = lp.getVector();
898
+ v1 = lp.getVector();
899
+ v0 = lp.getVector();
900
+ } // specifically place the triangle diagonal in the v0 and v1 slots so we can
901
+ // account for the doubling of vertices later when smoothing normals.
629
902
 
630
- if (!edgeMaterial) {
631
- throw 'LDrawLoader: Invalid edge colour while parsing material' + lineParser.getLineNumberString() + '.';
632
- } // Get the edge material for this triangle material
633
903
 
904
+ faces.push({
905
+ material: material,
906
+ colorCode: colorCode,
907
+ faceNormal: null,
908
+ vertices: [v0, v1, v2, v3],
909
+ normals: [null, null, null, null]
910
+ });
911
+ totalFaces += 2;
634
912
 
635
- edgeMaterial = edgeMaterial.userData.edgeMaterial;
913
+ if (doubleSided === true) {
914
+ faces.push({
915
+ material: material,
916
+ colorCode: colorCode,
917
+ faceNormal: null,
918
+ vertices: [v3, v2, v1, v0],
919
+ normals: [null, null, null, null]
920
+ });
921
+ totalFaces += 2;
636
922
  }
637
923
 
638
924
  break;
639
925
 
640
- case 'ALPHA':
641
- alpha = parseInt(lineParser.getToken());
926
+ default:
927
+ throw new Error('LDrawLoader: Unknown line type "' + lineType + '"' + lp.getLineNumberString() + '.');
928
+ }
929
+ }
642
930
 
643
- if (isNaN(alpha)) {
644
- throw 'LDrawLoader: Invalid alpha value in material definition' + lineParser.getLineNumberString() + '.';
645
- }
931
+ if (parsingEmbeddedFiles) {
932
+ this.setData(currentEmbeddedFileName, currentEmbeddedText);
933
+ }
646
934
 
647
- alpha = Math.max(0, Math.min(1, alpha / 255));
935
+ return {
936
+ faces,
937
+ conditionalSegments,
938
+ lineSegments,
939
+ type,
940
+ category,
941
+ keywords,
942
+ subobjects,
943
+ totalFaces,
944
+ startingConstructionStep,
945
+ materials,
946
+ fileName,
947
+ group: null
948
+ };
949
+ } // returns an (optionally cloned) instance of the data
648
950
 
649
- if (alpha < 1) {
650
- isTransparent = true;
651
- }
652
951
 
653
- break;
952
+ getData(fileName, clone = true) {
953
+ const key = fileName.toLowerCase();
954
+ const result = this._cache[key];
654
955
 
655
- case 'LUMINANCE':
656
- luminance = parseInt(lineParser.getToken());
956
+ if (result === null || result instanceof Promise) {
957
+ return null;
958
+ }
657
959
 
658
- if (isNaN(luminance)) {
659
- throw 'LDrawLoader: Invalid luminance value in material definition' + LineParser.getLineNumberString() + '.';
660
- }
960
+ if (clone) {
961
+ return this.cloneResult(result);
962
+ } else {
963
+ return result;
964
+ }
965
+ } // kicks off a fetch and parse of the requested data if it hasn't already been loaded. Returns when
966
+ // the data is ready to use and can be retrieved synchronously with "getData".
661
967
 
662
- luminance = Math.max(0, Math.min(1, luminance / 255));
663
- break;
664
968
 
665
- case 'CHROME':
666
- finishType = FINISH_TYPE_CHROME;
667
- break;
969
+ async ensureDataLoaded(fileName) {
970
+ const key = fileName.toLowerCase();
668
971
 
669
- case 'PEARLESCENT':
670
- finishType = FINISH_TYPE_PEARLESCENT;
671
- break;
972
+ if (!(key in this._cache)) {
973
+ // replace the promise with a copy of the parsed data for immediate processing
974
+ this._cache[key] = this.fetchData(fileName).then(text => {
975
+ const info = this.parse(text, fileName);
976
+ this._cache[key] = info;
977
+ return info;
978
+ });
979
+ }
672
980
 
673
- case 'RUBBER':
674
- finishType = FINISH_TYPE_RUBBER;
675
- break;
981
+ await this._cache[key];
982
+ } // sets the data in the cache from parsed data
676
983
 
677
- case 'MATTE_METALLIC':
678
- finishType = FINISH_TYPE_MATTE_METALLIC;
679
- break;
680
984
 
681
- case 'METAL':
682
- finishType = FINISH_TYPE_METAL;
683
- break;
985
+ setData(fileName, text) {
986
+ const key = fileName.toLowerCase();
987
+ this._cache[key] = this.parse(text, fileName);
988
+ }
684
989
 
685
- case 'MATERIAL':
686
- // Not implemented
687
- lineParser.setToEnd();
688
- break;
990
+ } // returns the material for an associated color code. If the color code is 16 for a face or 24 for
991
+ // an edge then the passthroughColorCode is used.
689
992
 
690
- default:
691
- throw 'LDrawLoader: Unknown token "' + token + '" while parsing material' + lineParser.getLineNumberString() + '.';
692
- }
693
- }
694
993
 
695
- let material = null;
994
+ function getMaterialFromCode(colorCode, parentColorCode, materialHierarchy, forEdge) {
995
+ const isPassthrough = !forEdge && colorCode === MAIN_COLOUR_CODE || forEdge && colorCode === MAIN_EDGE_COLOUR_CODE;
696
996
 
697
- switch (finishType) {
698
- case FINISH_TYPE_DEFAULT:
699
- material = new MeshStandardMaterial({
700
- color: colour,
701
- roughness: 0.3,
702
- envMapIntensity: 0.3,
703
- metalness: 0
704
- });
705
- break;
997
+ if (isPassthrough) {
998
+ colorCode = parentColorCode;
999
+ }
706
1000
 
707
- case FINISH_TYPE_PEARLESCENT:
708
- // Try to imitate pearlescency by setting the specular to the complementary of the color, and low shininess
709
- const specular = new Color(colour);
710
- const hsl = specular.getHSL({
711
- h: 0,
712
- s: 0,
713
- l: 0
714
- });
715
- hsl.h = (hsl.h + 0.5) % 1;
716
- hsl.l = Math.min(1, hsl.l + (1 - hsl.l) * 0.7);
717
- specular.setHSL(hsl.h, hsl.s, hsl.l);
718
- material = new MeshPhongMaterial({
719
- color: colour,
720
- specular: specular,
721
- shininess: 10,
722
- reflectivity: 0.3
723
- });
724
- break;
1001
+ return materialHierarchy[colorCode] || null;
1002
+ } // Class used to parse and build LDraw parts as three.js objects and cache them if they're a "Part" type.
725
1003
 
726
- case FINISH_TYPE_CHROME:
727
- // Mirror finish surface
728
- material = new MeshStandardMaterial({
729
- color: colour,
730
- roughness: 0,
731
- metalness: 1
732
- });
733
- break;
734
1004
 
735
- case FINISH_TYPE_RUBBER:
736
- // Rubber finish
737
- material = new MeshStandardMaterial({
738
- color: colour,
739
- roughness: 0.9,
740
- metalness: 0
741
- });
742
- canHaveEnvMap = false;
743
- break;
1005
+ class LDrawPartsGeometryCache {
1006
+ constructor(loader) {
1007
+ this.loader = loader;
1008
+ this.parseCache = new LDrawParsedCache(loader);
1009
+ this._cache = {};
1010
+ } // Convert the given file information into a mesh by processing subobjects.
744
1011
 
745
- case FINISH_TYPE_MATTE_METALLIC:
746
- // Brushed metal finish
747
- material = new MeshStandardMaterial({
748
- color: colour,
749
- roughness: 0.8,
750
- metalness: 0.4
751
- });
752
- break;
753
1012
 
754
- case FINISH_TYPE_METAL:
755
- // Average metal finish
756
- material = new MeshStandardMaterial({
757
- color: colour,
758
- roughness: 0.2,
759
- metalness: 0.85
760
- });
761
- break;
762
- }
1013
+ async processIntoMesh(info) {
1014
+ const loader = this.loader;
1015
+ const parseCache = this.parseCache;
1016
+ const faceMaterials = new Set(); // Processes the part subobject information to load child parts and merge geometry onto part
1017
+ // piece object.
763
1018
 
764
- material.transparent = isTransparent;
765
- material.premultipliedAlpha = true;
766
- material.opacity = alpha;
767
- material.depthWrite = !isTransparent;
768
- material.polygonOffset = true;
769
- material.polygonOffsetFactor = 1;
770
- material.userData.canHaveEnvMap = canHaveEnvMap;
1019
+ const processInfoSubobjects = async (info, subobject = null) => {
1020
+ const subobjects = info.subobjects;
1021
+ const promises = []; // Trigger load of all subobjects. If a subobject isn't a primitive then load it as a separate
1022
+ // group which lets instruction steps apply correctly.
771
1023
 
772
- if (luminance !== 0) {
773
- material.emissive.set(material.color).multiplyScalar(luminance);
774
- }
1024
+ for (let i = 0, l = subobjects.length; i < l; i++) {
1025
+ const subobject = subobjects[i];
1026
+ const promise = parseCache.ensureDataLoaded(subobject.fileName).then(() => {
1027
+ const subobjectInfo = parseCache.getData(subobject.fileName, false);
775
1028
 
776
- if (!edgeMaterial) {
777
- // This is the material used for edges
778
- edgeMaterial = new LineBasicMaterial({
779
- color: edgeColour,
780
- transparent: isTransparent,
781
- opacity: alpha,
782
- depthWrite: !isTransparent
783
- });
784
- edgeMaterial.userData.code = code;
785
- edgeMaterial.name = name + ' - Edge';
786
- edgeMaterial.userData.canHaveEnvMap = false; // This is the material used for conditional edges
787
-
788
- edgeMaterial.userData.conditionalEdgeMaterial = new ShaderMaterial({
789
- vertexShader: conditionalLineVertShader,
790
- fragmentShader: conditionalLineFragShader,
791
- uniforms: UniformsUtils.merge([UniformsLib.fog, {
792
- diffuse: {
793
- value: new Color(edgeColour)
794
- },
795
- opacity: {
796
- value: alpha
1029
+ if (!isPrimitiveType(subobjectInfo.type)) {
1030
+ return this.loadModel(subobject.fileName).catch(error => {
1031
+ console.warn(error);
1032
+ return null;
1033
+ });
797
1034
  }
798
- }]),
799
- fog: true,
800
- transparent: isTransparent,
801
- depthWrite: !isTransparent
802
- });
803
- edgeMaterial.userData.conditionalEdgeMaterial.userData.canHaveEnvMap = false;
804
- }
805
1035
 
806
- material.userData.code = code;
807
- material.name = name;
808
- material.userData.edgeMaterial = edgeMaterial;
809
- return material;
810
- } //
1036
+ return processInfoSubobjects(parseCache.getData(subobject.fileName), subobject);
1037
+ });
1038
+ promises.push(promise);
1039
+ }
811
1040
 
1041
+ const group = new Group();
1042
+ group.userData.category = info.category;
1043
+ group.userData.keywords = info.keywords;
1044
+ info.group = group;
1045
+ const subobjectInfos = await Promise.all(promises);
1046
+
1047
+ for (let i = 0, l = subobjectInfos.length; i < l; i++) {
1048
+ const subobject = info.subobjects[i];
1049
+ const subobjectInfo = subobjectInfos[i];
1050
+
1051
+ if (subobjectInfo === null) {
1052
+ // the subobject failed to load
1053
+ continue;
1054
+ } // if the subobject was loaded as a separate group then apply the parent scopes materials
1055
+
1056
+
1057
+ if (subobjectInfo.isGroup) {
1058
+ const subobjectGroup = subobjectInfo;
1059
+ subobject.matrix.decompose(subobjectGroup.position, subobjectGroup.quaternion, subobjectGroup.scale);
1060
+ subobjectGroup.userData.startingConstructionStep = subobject.startingConstructionStep;
1061
+ subobjectGroup.name = subobject.fileName;
1062
+ loader.applyMaterialsToMesh(subobjectGroup, subobject.colorCode, info.materials);
1063
+ group.add(subobjectGroup);
1064
+ continue;
1065
+ } // add the subobject group if it has children in case it has both children and primitives
1066
+
1067
+
1068
+ if (subobjectInfo.group.children.length) {
1069
+ group.add(subobjectInfo.group);
1070
+ } // transform the primitives into the local space of the parent piece and append them to
1071
+ // to the parent primitives list.
1072
+
1073
+
1074
+ const parentLineSegments = info.lineSegments;
1075
+ const parentConditionalSegments = info.conditionalSegments;
1076
+ const parentFaces = info.faces;
1077
+ const lineSegments = subobjectInfo.lineSegments;
1078
+ const conditionalSegments = subobjectInfo.conditionalSegments;
1079
+ const faces = subobjectInfo.faces;
1080
+ const matrix = subobject.matrix;
1081
+ const inverted = subobject.inverted;
1082
+ const matrixScaleInverted = matrix.determinant() < 0;
1083
+ const colorCode = subobject.colorCode;
1084
+ const lineColorCode = colorCode === MAIN_COLOUR_CODE ? MAIN_EDGE_COLOUR_CODE : colorCode;
812
1085
 
813
- objectParse(text) {
814
- // Retrieve data from the parent parse scope
815
- const parentParseScope = this.getParentParseScope(); // Main colour codes passed to this subobject (or default codes 16 and 24 if it is the root object)
1086
+ for (let i = 0, l = lineSegments.length; i < l; i++) {
1087
+ const ls = lineSegments[i];
1088
+ const vertices = ls.vertices;
1089
+ vertices[0].applyMatrix4(matrix);
1090
+ vertices[1].applyMatrix4(matrix);
1091
+ ls.colorCode = ls.colorCode === MAIN_EDGE_COLOUR_CODE ? lineColorCode : ls.colorCode;
1092
+ ls.material = ls.material || getMaterialFromCode(ls.colorCode, ls.colorCode, info.materials, true);
1093
+ parentLineSegments.push(ls);
1094
+ }
816
1095
 
817
- const mainColourCode = parentParseScope.mainColourCode;
818
- const mainEdgeColourCode = parentParseScope.mainEdgeColourCode;
819
- const currentParseScope = this.getCurrentParseScope(); // Parse result variables
1096
+ for (let i = 0, l = conditionalSegments.length; i < l; i++) {
1097
+ const os = conditionalSegments[i];
1098
+ const vertices = os.vertices;
1099
+ const controlPoints = os.controlPoints;
1100
+ vertices[0].applyMatrix4(matrix);
1101
+ vertices[1].applyMatrix4(matrix);
1102
+ controlPoints[0].applyMatrix4(matrix);
1103
+ controlPoints[1].applyMatrix4(matrix);
1104
+ os.colorCode = os.colorCode === MAIN_EDGE_COLOUR_CODE ? lineColorCode : os.colorCode;
1105
+ os.material = os.material || getMaterialFromCode(os.colorCode, os.colorCode, info.materials, true);
1106
+ parentConditionalSegments.push(os);
1107
+ }
820
1108
 
821
- let triangles;
822
- let lineSegments;
823
- let conditionalSegments;
824
- const subobjects = [];
825
- let category = null;
826
- let keywords = null;
1109
+ for (let i = 0, l = faces.length; i < l; i++) {
1110
+ const tri = faces[i];
1111
+ const vertices = tri.vertices;
827
1112
 
828
- if (text.indexOf('\r\n') !== -1) {
829
- // This is faster than String.split with regex that splits on both
830
- text = text.replace(/\r\n/g, '\n');
831
- }
1113
+ for (let i = 0, l = vertices.length; i < l; i++) {
1114
+ vertices[i].applyMatrix4(matrix);
1115
+ }
832
1116
 
833
- const lines = text.split('\n');
834
- const numLines = lines.length;
835
- let parsingEmbeddedFiles = false;
836
- let currentEmbeddedFileName = null;
837
- let currentEmbeddedText = null;
838
- let bfcCertified = false;
839
- let bfcCCW = true;
840
- let bfcInverted = false;
841
- let bfcCull = true;
842
- let type = '';
843
- let startingConstructionStep = false;
844
- const scope = this;
1117
+ tri.colorCode = tri.colorCode === MAIN_COLOUR_CODE ? colorCode : tri.colorCode;
1118
+ tri.material = tri.material || getMaterialFromCode(tri.colorCode, colorCode, info.materials, false);
1119
+ faceMaterials.add(tri.colorCode); // If the scale of the object is negated then the triangle winding order
1120
+ // needs to be flipped.
845
1121
 
846
- function parseColourCode(lineParser, forEdge) {
847
- // Parses next colour code and returns a THREE.Material
848
- let colourCode = lineParser.getToken();
1122
+ if (matrixScaleInverted !== inverted) {
1123
+ vertices.reverse();
1124
+ }
849
1125
 
850
- if (!forEdge && colourCode === '16') {
851
- colourCode = mainColourCode;
852
- }
1126
+ parentFaces.push(tri);
1127
+ }
853
1128
 
854
- if (forEdge && colourCode === '24') {
855
- colourCode = mainEdgeColourCode;
856
- }
1129
+ info.totalFaces += subobjectInfo.totalFaces;
1130
+ } // Apply the parent subobjects pass through material code to this object. This is done several times due
1131
+ // to material scoping.
857
1132
 
858
- const material = scope.getMaterial(colourCode);
859
1133
 
860
- if (!material) {
861
- throw 'LDrawLoader: Unknown colour code "' + colourCode + '" is used' + lineParser.getLineNumberString() + ' but it was not defined previously.';
1134
+ if (subobject) {
1135
+ loader.applyMaterialsToMesh(group, subobject.colorCode, info.materials);
862
1136
  }
863
1137
 
864
- return material;
1138
+ return info;
1139
+ }; // Track material use to see if we need to use the normal smooth slow path for hard edges.
1140
+
1141
+
1142
+ for (let i = 0, l = info.faces; i < l; i++) {
1143
+ faceMaterials.add(info.faces[i].colorCode);
865
1144
  }
866
1145
 
867
- function parseVector(lp) {
868
- const v = new Vector3(parseFloat(lp.getToken()), parseFloat(lp.getToken()), parseFloat(lp.getToken()));
1146
+ await processInfoSubobjects(info);
869
1147
 
870
- if (!scope.separateObjects) {
871
- v.applyMatrix4(currentParseScope.currentMatrix);
872
- }
1148
+ if (loader.smoothNormals) {
1149
+ const checkSubSegments = faceMaterials.size > 1;
1150
+ generateFaceNormals(info.faces);
1151
+ smoothNormals(info.faces, info.lineSegments, checkSubSegments);
1152
+ } // Add the primitive objects and metadata.
873
1153
 
874
- return v;
875
- } // Parse all line commands
876
1154
 
1155
+ const group = info.group;
877
1156
 
878
- for (let lineIndex = 0; lineIndex < numLines; lineIndex++) {
879
- const line = lines[lineIndex];
880
- if (line.length === 0) continue;
1157
+ if (info.faces.length > 0) {
1158
+ group.add(createObject(info.faces, 3, false, info.totalFaces));
1159
+ }
881
1160
 
882
- if (parsingEmbeddedFiles) {
883
- if (line.startsWith('0 FILE ')) {
884
- // Save previous embedded file in the cache
885
- this.subobjectCache[currentEmbeddedFileName.toLowerCase()] = currentEmbeddedText; // New embedded text file
1161
+ if (info.lineSegments.length > 0) {
1162
+ group.add(createObject(info.lineSegments, 2));
1163
+ }
886
1164
 
887
- currentEmbeddedFileName = line.substring(7);
888
- currentEmbeddedText = '';
889
- } else {
890
- currentEmbeddedText += line + '\n';
891
- }
1165
+ if (info.conditionalSegments.length > 0) {
1166
+ group.add(createObject(info.conditionalSegments, 2, true));
1167
+ }
892
1168
 
893
- continue;
894
- }
1169
+ return group;
1170
+ }
895
1171
 
896
- const lp = new LineParser(line, lineIndex + 1);
897
- lp.seekNonSpace();
1172
+ hasCachedModel(fileName) {
1173
+ return fileName !== null && fileName.toLowerCase() in this._cache;
1174
+ }
898
1175
 
899
- if (lp.isAtTheEnd()) {
900
- // Empty line
901
- continue;
902
- } // Parse the line type
1176
+ async getCachedModel(fileName) {
1177
+ if (fileName !== null && this.hasCachedModel(fileName)) {
1178
+ const key = fileName.toLowerCase();
1179
+ const group = await this._cache[key];
1180
+ return group.clone();
1181
+ } else {
1182
+ return null;
1183
+ }
1184
+ } // Loads and parses the model with the given file name. Returns a cached copy if available.
903
1185
 
904
1186
 
905
- const lineType = lp.getToken();
906
- let material;
907
- let segment;
908
- let inverted;
909
- let ccw;
910
- let doubleSided;
911
- let v0, v1, v2, v3, faceNormal;
1187
+ async loadModel(fileName) {
1188
+ const parseCache = this.parseCache;
1189
+ const key = fileName.toLowerCase();
912
1190
 
913
- switch (lineType) {
914
- // Line type 0: Comment or META
915
- case '0':
916
- // Parse meta directive
917
- const meta = lp.getToken();
1191
+ if (this.hasCachedModel(fileName)) {
1192
+ // Return cached model if available.
1193
+ return this.getCachedModel(fileName);
1194
+ } else {
1195
+ // Otherwise parse a new model.
1196
+ // Ensure the file data is loaded and pre parsed.
1197
+ await parseCache.ensureDataLoaded(fileName);
1198
+ const info = parseCache.getData(fileName);
1199
+ const promise = this.processIntoMesh(info); // Now that the file has loaded it's possible that another part parse has been waiting in parallel
1200
+ // so check the cache again to see if it's been added since the last async operation so we don't
1201
+ // do unnecessary work.
918
1202
 
919
- if (meta) {
920
- switch (meta) {
921
- case '!LDRAW_ORG':
922
- type = lp.getToken();
923
- currentParseScope.triangles = [];
924
- currentParseScope.lineSegments = [];
925
- currentParseScope.conditionalSegments = [];
926
- currentParseScope.type = type;
927
- const isRoot = !parentParseScope.isFromParse;
1203
+ if (this.hasCachedModel(fileName)) {
1204
+ return this.getCachedModel(fileName);
1205
+ } // Cache object if it's a part so it can be reused later.
928
1206
 
929
- if (isRoot || scope.separateObjects && !isPrimitiveType(type)) {
930
- currentParseScope.groupObject = new Group();
931
- currentParseScope.groupObject.userData.startingConstructionStep = currentParseScope.startingConstructionStep;
932
- } // If the scale of the object is negated then the triangle winding order
933
- // needs to be flipped.
934
1207
 
1208
+ if (isPartType(info.type)) {
1209
+ this._cache[key] = promise;
1210
+ } // return a copy
935
1211
 
936
- if (currentParseScope.matrix.determinant() < 0 && (scope.separateObjects && isPrimitiveType(type) || !scope.separateObjects)) {
937
- currentParseScope.inverted = !currentParseScope.inverted;
938
- }
939
1212
 
940
- triangles = currentParseScope.triangles;
941
- lineSegments = currentParseScope.lineSegments;
942
- conditionalSegments = currentParseScope.conditionalSegments;
943
- break;
1213
+ const group = await promise;
1214
+ return group.clone();
1215
+ }
1216
+ } // parses the given model text into a renderable object. Returns cached copy if available.
944
1217
 
945
- case '!COLOUR':
946
- material = this.parseColourMetaDirective(lp);
947
1218
 
948
- if (material) {
949
- this.addMaterial(material);
950
- } else {
951
- console.warn('LDrawLoader: Error parsing material' + lp.getLineNumberString());
952
- }
1219
+ async parseModel(text) {
1220
+ const parseCache = this.parseCache;
1221
+ const info = parseCache.parse(text);
953
1222
 
954
- break;
1223
+ if (isPartType(info.type) && this.hasCachedModel(info.fileName)) {
1224
+ return this.getCachedModel(info.fileName);
1225
+ }
955
1226
 
956
- case '!CATEGORY':
957
- category = lp.getToken();
958
- break;
1227
+ return this.processIntoMesh(info);
1228
+ }
959
1229
 
960
- case '!KEYWORDS':
961
- const newKeywords = lp.getRemainingString().split(',');
1230
+ }
962
1231
 
963
- if (newKeywords.length > 0) {
964
- if (!keywords) {
965
- keywords = [];
966
- }
1232
+ function sortByMaterial(a, b) {
1233
+ if (a.colorCode === b.colorCode) {
1234
+ return 0;
1235
+ }
967
1236
 
968
- newKeywords.forEach(function (keyword) {
969
- keywords.push(keyword.trim());
970
- });
971
- }
1237
+ if (a.colorCode < b.colorCode) {
1238
+ return -1;
1239
+ }
972
1240
 
973
- break;
1241
+ return 1;
1242
+ }
974
1243
 
975
- case 'FILE':
976
- if (lineIndex > 0) {
977
- // Start embedded text files parsing
978
- parsingEmbeddedFiles = true;
979
- currentEmbeddedFileName = lp.getRemainingString();
980
- currentEmbeddedText = '';
981
- bfcCertified = false;
982
- bfcCCW = true;
983
- }
1244
+ function createObject(elements, elementSize, isConditionalSegments = false, totalElements = null) {
1245
+ // Creates a LineSegments (elementSize = 2) or a Mesh (elementSize = 3 )
1246
+ // With per face / segment material, implemented with mesh groups and materials array
1247
+ // Sort the faces or line segments by color code to make later the mesh groups
1248
+ elements.sort(sortByMaterial);
984
1249
 
985
- break;
1250
+ if (totalElements === null) {
1251
+ totalElements = elements.length;
1252
+ }
986
1253
 
987
- case 'BFC':
988
- // Changes to the backface culling state
989
- while (!lp.isAtTheEnd()) {
990
- const token = lp.getToken();
1254
+ const positions = new Float32Array(elementSize * totalElements * 3);
1255
+ const normals = elementSize === 3 ? new Float32Array(elementSize * totalElements * 3) : null;
1256
+ const materials = [];
1257
+ const quadArray = new Array(6);
1258
+ const bufferGeometry = new BufferGeometry();
1259
+ let prevMaterial = null;
1260
+ let index0 = 0;
1261
+ let numGroupVerts = 0;
1262
+ let offset = 0;
991
1263
 
992
- switch (token) {
993
- case 'CERTIFY':
994
- case 'NOCERTIFY':
995
- bfcCertified = token === 'CERTIFY';
996
- bfcCCW = true;
997
- break;
1264
+ for (let iElem = 0, nElem = elements.length; iElem < nElem; iElem++) {
1265
+ const elem = elements[iElem];
1266
+ let vertices = elem.vertices;
1267
+
1268
+ if (vertices.length === 4) {
1269
+ quadArray[0] = vertices[0];
1270
+ quadArray[1] = vertices[1];
1271
+ quadArray[2] = vertices[2];
1272
+ quadArray[3] = vertices[0];
1273
+ quadArray[4] = vertices[2];
1274
+ quadArray[5] = vertices[3];
1275
+ vertices = quadArray;
1276
+ }
998
1277
 
999
- case 'CW':
1000
- case 'CCW':
1001
- bfcCCW = token === 'CCW';
1002
- break;
1278
+ for (let j = 0, l = vertices.length; j < l; j++) {
1279
+ const v = vertices[j];
1280
+ const index = offset + j * 3;
1281
+ positions[index + 0] = v.x;
1282
+ positions[index + 1] = v.y;
1283
+ positions[index + 2] = v.z;
1284
+ } // create the normals array if this is a set of faces
1003
1285
 
1004
- case 'INVERTNEXT':
1005
- bfcInverted = true;
1006
- break;
1007
1286
 
1008
- case 'CLIP':
1009
- case 'NOCLIP':
1010
- bfcCull = token === 'CLIP';
1011
- break;
1287
+ if (elementSize === 3) {
1288
+ if (!elem.faceNormal) {
1289
+ const v0 = vertices[0];
1290
+ const v1 = vertices[1];
1291
+ const v2 = vertices[2];
1012
1292
 
1013
- default:
1014
- console.warn('THREE.LDrawLoader: BFC directive "' + token + '" is unknown.');
1015
- break;
1016
- }
1017
- }
1293
+ _tempVec0.subVectors(v1, v0);
1018
1294
 
1019
- break;
1295
+ _tempVec1.subVectors(v2, v1);
1020
1296
 
1021
- case 'STEP':
1022
- startingConstructionStep = true;
1023
- break;
1024
- }
1025
- }
1297
+ elem.faceNormal = new Vector3().crossVectors(_tempVec0, _tempVec1).normalize();
1298
+ }
1026
1299
 
1027
- break;
1028
- // Line type 1: Sub-object file
1300
+ let elemNormals = elem.normals;
1029
1301
 
1030
- case '1':
1031
- material = parseColourCode(lp);
1032
- const posX = parseFloat(lp.getToken());
1033
- const posY = parseFloat(lp.getToken());
1034
- const posZ = parseFloat(lp.getToken());
1035
- const m0 = parseFloat(lp.getToken());
1036
- const m1 = parseFloat(lp.getToken());
1037
- const m2 = parseFloat(lp.getToken());
1038
- const m3 = parseFloat(lp.getToken());
1039
- const m4 = parseFloat(lp.getToken());
1040
- const m5 = parseFloat(lp.getToken());
1041
- const m6 = parseFloat(lp.getToken());
1042
- const m7 = parseFloat(lp.getToken());
1043
- const m8 = parseFloat(lp.getToken());
1044
- const matrix = new Matrix4().set(m0, m1, m2, posX, m3, m4, m5, posY, m6, m7, m8, posZ, 0, 0, 0, 1);
1045
- let fileName = lp.getRemainingString().trim().replace(/\\/g, '/');
1302
+ if (elemNormals.length === 4) {
1303
+ quadArray[0] = elemNormals[0];
1304
+ quadArray[1] = elemNormals[1];
1305
+ quadArray[2] = elemNormals[2];
1306
+ quadArray[3] = elemNormals[0];
1307
+ quadArray[4] = elemNormals[2];
1308
+ quadArray[5] = elemNormals[3];
1309
+ elemNormals = quadArray;
1310
+ }
1046
1311
 
1047
- if (scope.fileMap[fileName]) {
1048
- // Found the subobject path in the preloaded file path map
1049
- fileName = scope.fileMap[fileName];
1050
- } else {
1051
- // Standardized subfolders
1052
- if (fileName.startsWith('s/')) {
1053
- fileName = 'parts/' + fileName;
1054
- } else if (fileName.startsWith('48/')) {
1055
- fileName = 'p/' + fileName;
1312
+ for (let j = 0, l = elemNormals.length; j < l; j++) {
1313
+ // use face normal if a vertex normal is not provided
1314
+ let n = elem.faceNormal;
1315
+
1316
+ if (elemNormals[j]) {
1317
+ n = elemNormals[j].norm;
1318
+ }
1319
+
1320
+ const index = offset + j * 3;
1321
+ normals[index + 0] = n.x;
1322
+ normals[index + 1] = n.y;
1323
+ normals[index + 2] = n.z;
1324
+ }
1325
+ }
1326
+
1327
+ if (prevMaterial !== elem.colorCode) {
1328
+ if (prevMaterial !== null) {
1329
+ bufferGeometry.addGroup(index0, numGroupVerts, materials.length - 1);
1330
+ }
1331
+
1332
+ const material = elem.material;
1333
+
1334
+ if (material !== null) {
1335
+ if (elementSize === 3) {
1336
+ materials.push(material);
1337
+ } else if (elementSize === 2) {
1338
+ if (material !== null) {
1339
+ if (isConditionalSegments) {
1340
+ materials.push(material.userData.edgeMaterial.userData.conditionalEdgeMaterial);
1341
+ } else {
1342
+ materials.push(material.userData.edgeMaterial);
1056
1343
  }
1344
+ } else {
1345
+ materials.push(null);
1057
1346
  }
1347
+ }
1348
+ } else {
1349
+ // If a material has not been made available yet then keep the color code string in the material array
1350
+ // to save the spot for the material once a parent scopes materials are being applied to the object.
1351
+ materials.push(elem.colorCode);
1352
+ }
1058
1353
 
1059
- subobjects.push({
1060
- material: material,
1061
- matrix: matrix,
1062
- fileName: fileName,
1063
- originalFileName: fileName,
1064
- locationState: FILE_LOCATION_AS_IS,
1065
- url: null,
1066
- triedLowerCase: false,
1067
- inverted: bfcInverted !== currentParseScope.inverted,
1068
- startingConstructionStep: startingConstructionStep
1069
- });
1070
- bfcInverted = false;
1071
- break;
1072
- // Line type 2: Line segment
1354
+ prevMaterial = elem.colorCode;
1355
+ index0 = offset / 3;
1356
+ numGroupVerts = vertices.length;
1357
+ } else {
1358
+ numGroupVerts += vertices.length;
1359
+ }
1073
1360
 
1074
- case '2':
1075
- material = parseColourCode(lp, true);
1076
- segment = {
1077
- material: material.userData.edgeMaterial,
1078
- colourCode: material.userData.code,
1079
- v0: parseVector(lp),
1080
- v1: parseVector(lp)
1081
- };
1082
- lineSegments.push(segment);
1083
- break;
1084
- // Line type 5: Conditional Line segment
1361
+ offset += 3 * vertices.length;
1362
+ }
1085
1363
 
1086
- case '5':
1087
- material = parseColourCode(lp, true);
1088
- segment = {
1089
- material: material.userData.edgeMaterial.userData.conditionalEdgeMaterial,
1090
- colourCode: material.userData.code,
1091
- v0: parseVector(lp),
1092
- v1: parseVector(lp),
1093
- c0: parseVector(lp),
1094
- c1: parseVector(lp)
1095
- };
1096
- conditionalSegments.push(segment);
1097
- break;
1098
- // Line type 3: Triangle
1364
+ if (numGroupVerts > 0) {
1365
+ bufferGeometry.addGroup(index0, Infinity, materials.length - 1);
1366
+ }
1099
1367
 
1100
- case '3':
1101
- material = parseColourCode(lp);
1102
- inverted = currentParseScope.inverted;
1103
- ccw = bfcCCW !== inverted;
1104
- doubleSided = !bfcCertified || !bfcCull;
1368
+ bufferGeometry.setAttribute('position', new BufferAttribute(positions, 3));
1105
1369
 
1106
- if (ccw === true) {
1107
- v0 = parseVector(lp);
1108
- v1 = parseVector(lp);
1109
- v2 = parseVector(lp);
1110
- } else {
1111
- v2 = parseVector(lp);
1112
- v1 = parseVector(lp);
1113
- v0 = parseVector(lp);
1114
- }
1370
+ if (normals !== null) {
1371
+ bufferGeometry.setAttribute('normal', new BufferAttribute(normals, 3));
1372
+ }
1115
1373
 
1116
- _tempVec0.subVectors(v1, v0);
1374
+ let object3d = null;
1117
1375
 
1118
- _tempVec1.subVectors(v2, v1);
1376
+ if (elementSize === 2) {
1377
+ if (isConditionalSegments) {
1378
+ object3d = new ConditionalLineSegments(bufferGeometry, materials.length === 1 ? materials[0] : materials);
1379
+ } else {
1380
+ object3d = new LineSegments(bufferGeometry, materials.length === 1 ? materials[0] : materials);
1381
+ }
1382
+ } else if (elementSize === 3) {
1383
+ object3d = new Mesh(bufferGeometry, materials.length === 1 ? materials[0] : materials);
1384
+ }
1119
1385
 
1120
- faceNormal = new Vector3().crossVectors(_tempVec0, _tempVec1).normalize();
1121
- triangles.push({
1122
- material: material,
1123
- colourCode: material.userData.code,
1124
- v0: v0,
1125
- v1: v1,
1126
- v2: v2,
1127
- faceNormal: faceNormal,
1128
- n0: null,
1129
- n1: null,
1130
- n2: null
1131
- });
1386
+ if (isConditionalSegments) {
1387
+ object3d.isConditionalLine = true;
1388
+ const controlArray0 = new Float32Array(elements.length * 3 * 2);
1389
+ const controlArray1 = new Float32Array(elements.length * 3 * 2);
1390
+ const directionArray = new Float32Array(elements.length * 3 * 2);
1132
1391
 
1133
- if (doubleSided === true) {
1134
- triangles.push({
1135
- material: material,
1136
- colourCode: material.userData.code,
1137
- v0: v0,
1138
- v1: v2,
1139
- v2: v1,
1140
- faceNormal: faceNormal,
1141
- n0: null,
1142
- n1: null,
1143
- n2: null
1144
- });
1145
- }
1392
+ for (let i = 0, l = elements.length; i < l; i++) {
1393
+ const os = elements[i];
1394
+ const vertices = os.vertices;
1395
+ const controlPoints = os.controlPoints;
1396
+ const c0 = controlPoints[0];
1397
+ const c1 = controlPoints[1];
1398
+ const v0 = vertices[0];
1399
+ const v1 = vertices[1];
1400
+ const index = i * 3 * 2;
1401
+ controlArray0[index + 0] = c0.x;
1402
+ controlArray0[index + 1] = c0.y;
1403
+ controlArray0[index + 2] = c0.z;
1404
+ controlArray0[index + 3] = c0.x;
1405
+ controlArray0[index + 4] = c0.y;
1406
+ controlArray0[index + 5] = c0.z;
1407
+ controlArray1[index + 0] = c1.x;
1408
+ controlArray1[index + 1] = c1.y;
1409
+ controlArray1[index + 2] = c1.z;
1410
+ controlArray1[index + 3] = c1.x;
1411
+ controlArray1[index + 4] = c1.y;
1412
+ controlArray1[index + 5] = c1.z;
1413
+ directionArray[index + 0] = v1.x - v0.x;
1414
+ directionArray[index + 1] = v1.y - v0.y;
1415
+ directionArray[index + 2] = v1.z - v0.z;
1416
+ directionArray[index + 3] = v1.x - v0.x;
1417
+ directionArray[index + 4] = v1.y - v0.y;
1418
+ directionArray[index + 5] = v1.z - v0.z;
1419
+ }
1420
+
1421
+ bufferGeometry.setAttribute('control0', new BufferAttribute(controlArray0, 3, false));
1422
+ bufferGeometry.setAttribute('control1', new BufferAttribute(controlArray1, 3, false));
1423
+ bufferGeometry.setAttribute('direction', new BufferAttribute(directionArray, 3, false));
1424
+ }
1425
+
1426
+ return object3d;
1427
+ } //
1428
+
1429
+
1430
+ class LDrawLoader extends Loader {
1431
+ constructor(manager) {
1432
+ super(manager); // Array of THREE.Material
1433
+
1434
+ this.materials = [];
1435
+ this.materialLibrary = {}; // This also allows to handle the embedded text files ("0 FILE" lines)
1436
+
1437
+ this.partsCache = new LDrawPartsGeometryCache(this); // This object is a map from file names to paths. It agilizes the paths search. If it is not set then files will be searched by trial and error.
1438
+
1439
+ this.fileMap = {}; // Initializes the materials library with default materials
1440
+
1441
+ this.setMaterials([]); // If this flag is set to true the vertex normals will be smoothed.
1442
+
1443
+ this.smoothNormals = true; // The path to load parts from the LDraw parts library from.
1444
+
1445
+ this.partsLibraryPath = '';
1446
+ }
1447
+
1448
+ setPartsLibraryPath(path) {
1449
+ this.partsLibraryPath = path;
1450
+ return this;
1451
+ }
1452
+
1453
+ async preloadMaterials(url) {
1454
+ const fileLoader = new FileLoader(this.manager);
1455
+ fileLoader.setPath(this.path);
1456
+ fileLoader.setRequestHeader(this.requestHeader);
1457
+ fileLoader.setWithCredentials(this.withCredentials);
1458
+ const text = await fileLoader.loadAsync(url);
1459
+ const colorLineRegex = /^0 !COLOUR/;
1460
+ const lines = text.split(/[\n\r]/g);
1461
+ const materials = [];
1462
+
1463
+ for (let i = 0, l = lines.length; i < l; i++) {
1464
+ const line = lines[i];
1465
+
1466
+ if (colorLineRegex.test(line)) {
1467
+ const directive = line.replace(colorLineRegex, '');
1468
+ const material = this.parseColorMetaDirective(new LineParser(directive));
1469
+ materials.push(material);
1470
+ }
1471
+ }
1146
1472
 
1147
- break;
1148
- // Line type 4: Quadrilateral
1473
+ this.setMaterials(materials);
1474
+ }
1149
1475
 
1150
- case '4':
1151
- material = parseColourCode(lp);
1152
- inverted = currentParseScope.inverted;
1153
- ccw = bfcCCW !== inverted;
1154
- doubleSided = !bfcCertified || !bfcCull;
1476
+ load(url, onLoad, onProgress, onError) {
1477
+ const fileLoader = new FileLoader(this.manager);
1478
+ fileLoader.setPath(this.path);
1479
+ fileLoader.setRequestHeader(this.requestHeader);
1480
+ fileLoader.setWithCredentials(this.withCredentials);
1481
+ fileLoader.load(url, text => {
1482
+ this.partsCache.parseModel(text, this.materialLibrary).then(group => {
1483
+ this.applyMaterialsToMesh(group, MAIN_COLOUR_CODE, this.materialLibrary, true);
1484
+ this.computeConstructionSteps(group);
1485
+ onLoad(group);
1486
+ }).catch(onError);
1487
+ }, onProgress, onError);
1488
+ }
1155
1489
 
1156
- if (ccw === true) {
1157
- v0 = parseVector(lp);
1158
- v1 = parseVector(lp);
1159
- v2 = parseVector(lp);
1160
- v3 = parseVector(lp);
1161
- } else {
1162
- v3 = parseVector(lp);
1163
- v2 = parseVector(lp);
1164
- v1 = parseVector(lp);
1165
- v0 = parseVector(lp);
1166
- }
1490
+ parse(text, onLoad) {
1491
+ this.partsCache.parseModel(text, this.materialLibrary).then(group => {
1492
+ this.computeConstructionSteps(group);
1493
+ onLoad(group);
1494
+ });
1495
+ }
1167
1496
 
1168
- _tempVec0.subVectors(v1, v0);
1497
+ setMaterials(materials) {
1498
+ this.materialLibrary = {};
1499
+ this.materials = [];
1169
1500
 
1170
- _tempVec1.subVectors(v2, v1);
1501
+ for (let i = 0, l = materials.length; i < l; i++) {
1502
+ this.addMaterial(materials[i]);
1503
+ } // Add default main triangle and line edge materials (used in pieces that can be colored with a main color)
1171
1504
 
1172
- faceNormal = new Vector3().crossVectors(_tempVec0, _tempVec1).normalize();
1173
- triangles.push({
1174
- material: material,
1175
- colourCode: material.userData.code,
1176
- v0: v0,
1177
- v1: v1,
1178
- v2: v2,
1179
- faceNormal: faceNormal,
1180
- n0: null,
1181
- n1: null,
1182
- n2: null
1183
- });
1184
- triangles.push({
1185
- material: material,
1186
- colourCode: material.userData.code,
1187
- v0: v0,
1188
- v1: v2,
1189
- v2: v3,
1190
- faceNormal: faceNormal,
1191
- n0: null,
1192
- n1: null,
1193
- n2: null
1194
- });
1195
1505
 
1196
- if (doubleSided === true) {
1197
- triangles.push({
1198
- material: material,
1199
- colourCode: material.userData.code,
1200
- v0: v0,
1201
- v1: v2,
1202
- v2: v1,
1203
- faceNormal: faceNormal,
1204
- n0: null,
1205
- n1: null,
1206
- n2: null
1207
- });
1208
- triangles.push({
1209
- material: material,
1210
- colourCode: material.userData.code,
1211
- v0: v0,
1212
- v1: v3,
1213
- v2: v2,
1214
- faceNormal: faceNormal,
1215
- n0: null,
1216
- n1: null,
1217
- n2: null
1218
- });
1219
- }
1506
+ this.addMaterial(this.parseColorMetaDirective(new LineParser('Main_Colour CODE 16 VALUE #FF8080 EDGE #333333')));
1507
+ this.addMaterial(this.parseColorMetaDirective(new LineParser('Edge_Colour CODE 24 VALUE #A0A0A0 EDGE #333333')));
1508
+ return this;
1509
+ }
1220
1510
 
1221
- break;
1511
+ setFileMap(fileMap) {
1512
+ this.fileMap = fileMap;
1513
+ return this;
1514
+ }
1222
1515
 
1223
- default:
1224
- throw 'LDrawLoader: Unknown line type "' + lineType + '"' + lp.getLineNumberString() + '.';
1225
- }
1226
- }
1516
+ addMaterial(material) {
1517
+ // Adds a material to the material library which is on top of the parse scopes stack. And also to the materials array
1518
+ const matLib = this.materialLibrary;
1227
1519
 
1228
- if (parsingEmbeddedFiles) {
1229
- this.subobjectCache[currentEmbeddedFileName.toLowerCase()] = currentEmbeddedText;
1520
+ if (!matLib[material.userData.code]) {
1521
+ this.materials.push(material);
1522
+ matLib[material.userData.code] = material;
1230
1523
  }
1231
1524
 
1232
- currentParseScope.category = category;
1233
- currentParseScope.keywords = keywords;
1234
- currentParseScope.subobjects = subobjects;
1235
- currentParseScope.numSubobjects = subobjects.length;
1236
- currentParseScope.subobjectIndex = 0;
1525
+ return this;
1237
1526
  }
1238
1527
 
1239
- computeConstructionSteps(model) {
1240
- // Sets userdata.constructionStep number in Group objects and userData.numConstructionSteps number in the root Group object.
1241
- let stepNumber = 0;
1242
- model.traverse(c => {
1243
- if (c.isGroup) {
1244
- if (c.userData.startingConstructionStep) {
1245
- stepNumber++;
1246
- }
1528
+ getMaterial(colorCode) {
1529
+ if (colorCode.startsWith('0x2')) {
1530
+ // Special 'direct' material value (RGB color)
1531
+ const color = colorCode.substring(3);
1532
+ return this.parseColorMetaDirective(new LineParser('Direct_Color_' + color + ' CODE -1 VALUE #' + color + ' EDGE #' + color + ''));
1533
+ }
1247
1534
 
1248
- c.userData.constructionStep = stepNumber;
1535
+ return this.materialLibrary[colorCode] || null;
1536
+ } // Applies the appropriate materials to a prebuilt hierarchy of geometry. Assumes that color codes are present
1537
+ // in the material array if they need to be filled in.
1538
+
1539
+
1540
+ applyMaterialsToMesh(group, parentColorCode, materialHierarchy, finalMaterialPass = false) {
1541
+ // find any missing materials as indicated by a color code string and replace it with a material from the current material lib
1542
+ const loader = this;
1543
+ const parentIsPassthrough = parentColorCode === MAIN_COLOUR_CODE;
1544
+ group.traverse(c => {
1545
+ if (c.isMesh || c.isLineSegments) {
1546
+ if (Array.isArray(c.material)) {
1547
+ for (let i = 0, l = c.material.length; i < l; i++) {
1548
+ if (!c.material[i].isMaterial) {
1549
+ c.material[i] = getMaterial(c, c.material[i]);
1550
+ }
1551
+ }
1552
+ } else if (!c.material.isMaterial) {
1553
+ c.material = getMaterial(c, c.material);
1554
+ }
1555
+ }
1556
+ }); // Returns the appropriate material for the object (line or face) given color code. If the code is "pass through"
1557
+ // (24 for lines, 16 for edges) then the pass through color code is used. If that is also pass through then it's
1558
+ // simply returned for the subsequent material application.
1559
+
1560
+ function getMaterial(c, colorCode) {
1561
+ // if our parent is a passthrough color code and we don't have the current material color available then
1562
+ // return early.
1563
+ if (parentIsPassthrough && !(colorCode in materialHierarchy) && !finalMaterialPass) {
1564
+ return colorCode;
1249
1565
  }
1250
- });
1251
- model.userData.numConstructionSteps = stepNumber + 1;
1252
- }
1253
1566
 
1254
- processObject(text, onProcessed, subobject, url) {
1255
- const scope = this;
1256
- const parseScope = scope.newParseScopeLevel();
1257
- parseScope.url = url;
1258
- const parentParseScope = scope.getParentParseScope(); // Set current matrix
1567
+ const forEdge = c.isLineSegments || c.isConditionalLine;
1568
+ const isPassthrough = !forEdge && colorCode === MAIN_COLOUR_CODE || forEdge && colorCode === MAIN_EDGE_COLOUR_CODE;
1259
1569
 
1260
- if (subobject) {
1261
- parseScope.currentMatrix.multiplyMatrices(parentParseScope.currentMatrix, subobject.matrix);
1262
- parseScope.matrix.copy(subobject.matrix);
1263
- parseScope.inverted = subobject.inverted;
1264
- parseScope.startingConstructionStep = subobject.startingConstructionStep;
1265
- } // Add to cache
1570
+ if (isPassthrough) {
1571
+ colorCode = parentColorCode;
1572
+ }
1266
1573
 
1574
+ let material = null;
1267
1575
 
1268
- let currentFileName = parentParseScope.currentFileName;
1576
+ if (colorCode in materialHierarchy) {
1577
+ material = materialHierarchy[colorCode];
1578
+ } else if (finalMaterialPass) {
1579
+ // see if we can get the final material from from the "getMaterial" function which will attempt to
1580
+ // parse the "direct" colors
1581
+ material = loader.getMaterial(colorCode);
1269
1582
 
1270
- if (currentFileName !== null) {
1271
- currentFileName = parentParseScope.currentFileName.toLowerCase();
1272
- }
1583
+ if (material === null) {
1584
+ // otherwise throw an error if this is final opportunity to set the material
1585
+ throw new Error(`LDrawLoader: Material properties for code ${colorCode} not available.`);
1586
+ }
1587
+ } else {
1588
+ return colorCode;
1589
+ }
1273
1590
 
1274
- if (scope.subobjectCache[currentFileName] === undefined) {
1275
- scope.subobjectCache[currentFileName] = text;
1276
- } // Parse the object (returns a Group)
1591
+ if (c.isLineSegments) {
1592
+ material = material.userData.edgeMaterial;
1277
1593
 
1594
+ if (c.isConditionalLine) {
1595
+ material = material.userData.conditionalEdgeMaterial;
1596
+ }
1597
+ }
1278
1598
 
1279
- scope.objectParse(text);
1280
- let finishedCount = 0;
1281
- onSubobjectFinish();
1599
+ return material;
1600
+ }
1601
+ }
1282
1602
 
1283
- function onSubobjectFinish() {
1284
- finishedCount++;
1603
+ getMainMaterial() {
1604
+ return this.getMaterial(MAIN_COLOUR_CODE);
1605
+ }
1285
1606
 
1286
- if (finishedCount === parseScope.subobjects.length + 1) {
1287
- finalizeObject();
1288
- } else {
1289
- // Once the previous subobject has finished we can start processing the next one in the list.
1290
- // The subobject processing shares scope in processing so it's important that they be loaded serially
1291
- // to avoid race conditions.
1292
- // Promise.resolve is used as an approach to asynchronously schedule a task _before_ this frame ends to
1293
- // avoid stack overflow exceptions when loading many subobjects from the cache. RequestAnimationFrame
1294
- // will work but causes the load to happen after the next frame which causes the load to take significantly longer.
1295
- const subobject = parseScope.subobjects[parseScope.subobjectIndex];
1296
- Promise.resolve().then(function () {
1297
- loadSubobject(subobject);
1298
- });
1299
- parseScope.subobjectIndex++;
1300
- }
1301
- }
1607
+ getMainEdgeMaterial() {
1608
+ return this.getMaterial(MAIN_EDGE_COLOUR_CODE);
1609
+ }
1302
1610
 
1303
- function finalizeObject() {
1304
- if (scope.smoothNormals && parseScope.type === 'Part') {
1305
- smoothNormals(parseScope.triangles, parseScope.lineSegments);
1306
- }
1611
+ parseColorMetaDirective(lineParser) {
1612
+ // Parses a color definition and returns a THREE.Material
1613
+ let code = null; // Triangle and line colors
1307
1614
 
1308
- const isRoot = !parentParseScope.isFromParse;
1615
+ let color = 0xff00ff;
1616
+ let edgeColor = 0xff00ff; // Transparency
1309
1617
 
1310
- if (scope.separateObjects && !isPrimitiveType(parseScope.type) || isRoot) {
1311
- const objGroup = parseScope.groupObject;
1618
+ let alpha = 1;
1619
+ let isTransparent = false; // Self-illumination:
1312
1620
 
1313
- if (parseScope.triangles.length > 0) {
1314
- objGroup.add(createObject(parseScope.triangles, 3));
1315
- }
1621
+ let luminance = 0;
1622
+ let finishType = FINISH_TYPE_DEFAULT;
1623
+ let edgeMaterial = null;
1624
+ const name = lineParser.getToken();
1316
1625
 
1317
- if (parseScope.lineSegments.length > 0) {
1318
- objGroup.add(createObject(parseScope.lineSegments, 2));
1319
- }
1626
+ if (!name) {
1627
+ throw new Error('LDrawLoader: Material name was expected after "!COLOUR tag' + lineParser.getLineNumberString() + '.');
1628
+ } // Parse tag tokens and their parameters
1320
1629
 
1321
- if (parseScope.conditionalSegments.length > 0) {
1322
- objGroup.add(createObject(parseScope.conditionalSegments, 2, true));
1323
- }
1324
1630
 
1325
- if (parentParseScope.groupObject) {
1326
- objGroup.name = parseScope.fileName;
1327
- objGroup.userData.category = parseScope.category;
1328
- objGroup.userData.keywords = parseScope.keywords;
1329
- parseScope.matrix.decompose(objGroup.position, objGroup.quaternion, objGroup.scale);
1330
- parentParseScope.groupObject.add(objGroup);
1331
- }
1332
- } else {
1333
- const separateObjects = scope.separateObjects;
1334
- const parentLineSegments = parentParseScope.lineSegments;
1335
- const parentConditionalSegments = parentParseScope.conditionalSegments;
1336
- const parentTriangles = parentParseScope.triangles;
1337
- const lineSegments = parseScope.lineSegments;
1338
- const conditionalSegments = parseScope.conditionalSegments;
1339
- const triangles = parseScope.triangles;
1631
+ let token = null;
1340
1632
 
1341
- for (let i = 0, l = lineSegments.length; i < l; i++) {
1342
- const ls = lineSegments[i];
1633
+ while (true) {
1634
+ token = lineParser.getToken();
1343
1635
 
1344
- if (separateObjects) {
1345
- ls.v0.applyMatrix4(parseScope.matrix);
1346
- ls.v1.applyMatrix4(parseScope.matrix);
1347
- }
1636
+ if (!token) {
1637
+ break;
1638
+ }
1348
1639
 
1349
- parentLineSegments.push(ls);
1350
- }
1640
+ switch (token.toUpperCase()) {
1641
+ case 'CODE':
1642
+ code = lineParser.getToken();
1643
+ break;
1351
1644
 
1352
- for (let i = 0, l = conditionalSegments.length; i < l; i++) {
1353
- const os = conditionalSegments[i];
1645
+ case 'VALUE':
1646
+ color = lineParser.getToken();
1354
1647
 
1355
- if (separateObjects) {
1356
- os.v0.applyMatrix4(parseScope.matrix);
1357
- os.v1.applyMatrix4(parseScope.matrix);
1358
- os.c0.applyMatrix4(parseScope.matrix);
1359
- os.c1.applyMatrix4(parseScope.matrix);
1648
+ if (color.startsWith('0x')) {
1649
+ color = '#' + color.substring(2);
1650
+ } else if (!color.startsWith('#')) {
1651
+ throw new Error('LDrawLoader: Invalid color while parsing material' + lineParser.getLineNumberString() + '.');
1360
1652
  }
1361
1653
 
1362
- parentConditionalSegments.push(os);
1363
- }
1654
+ break;
1364
1655
 
1365
- for (let i = 0, l = triangles.length; i < l; i++) {
1366
- const tri = triangles[i];
1656
+ case 'EDGE':
1657
+ edgeColor = lineParser.getToken();
1367
1658
 
1368
- if (separateObjects) {
1369
- tri.v0 = tri.v0.clone().applyMatrix4(parseScope.matrix);
1370
- tri.v1 = tri.v1.clone().applyMatrix4(parseScope.matrix);
1371
- tri.v2 = tri.v2.clone().applyMatrix4(parseScope.matrix);
1659
+ if (edgeColor.startsWith('0x')) {
1660
+ edgeColor = '#' + edgeColor.substring(2);
1661
+ } else if (!edgeColor.startsWith('#')) {
1662
+ // Try to see if edge color is a color code
1663
+ edgeMaterial = this.getMaterial(edgeColor);
1372
1664
 
1373
- _tempVec0.subVectors(tri.v1, tri.v0);
1665
+ if (!edgeMaterial) {
1666
+ throw new Error('LDrawLoader: Invalid edge color while parsing material' + lineParser.getLineNumberString() + '.');
1667
+ } // Get the edge material for this triangle material
1374
1668
 
1375
- _tempVec1.subVectors(tri.v2, tri.v1);
1376
1669
 
1377
- tri.faceNormal.crossVectors(_tempVec0, _tempVec1).normalize();
1670
+ edgeMaterial = edgeMaterial.userData.edgeMaterial;
1378
1671
  }
1379
1672
 
1380
- parentTriangles.push(tri);
1381
- }
1382
- }
1383
-
1384
- scope.removeScopeLevel(); // If it is root object, compute construction steps
1673
+ break;
1385
1674
 
1386
- if (!parentParseScope.isFromParse) {
1387
- scope.computeConstructionSteps(parseScope.groupObject);
1388
- }
1675
+ case 'ALPHA':
1676
+ alpha = parseInt(lineParser.getToken());
1389
1677
 
1390
- if (onProcessed) {
1391
- onProcessed(parseScope.groupObject);
1392
- }
1393
- }
1678
+ if (isNaN(alpha)) {
1679
+ throw new Error('LDrawLoader: Invalid alpha value in material definition' + lineParser.getLineNumberString() + '.');
1680
+ }
1394
1681
 
1395
- function loadSubobject(subobject) {
1396
- parseScope.mainColourCode = subobject.material.userData.code;
1397
- parseScope.mainEdgeColourCode = subobject.material.userData.edgeMaterial.userData.code;
1398
- parseScope.currentFileName = subobject.originalFileName; // If subobject was cached previously, use the cached one
1682
+ alpha = Math.max(0, Math.min(1, alpha / 255));
1399
1683
 
1400
- const cached = scope.subobjectCache[subobject.originalFileName.toLowerCase()];
1684
+ if (alpha < 1) {
1685
+ isTransparent = true;
1686
+ }
1401
1687
 
1402
- if (cached) {
1403
- scope.processObject(cached, function (subobjectGroup) {
1404
- onSubobjectLoaded(subobjectGroup, subobject);
1405
- onSubobjectFinish();
1406
- }, subobject, url);
1407
- return;
1408
- } // Adjust file name to locate the subobject file path in standard locations (always under directory scope.path)
1409
- // Update also subobject.locationState for the next try if this load fails.
1688
+ break;
1410
1689
 
1690
+ case 'LUMINANCE':
1691
+ luminance = parseInt(lineParser.getToken());
1411
1692
 
1412
- let subobjectURL = subobject.fileName;
1413
- let newLocationState = FILE_LOCATION_NOT_FOUND;
1693
+ if (isNaN(luminance)) {
1694
+ throw new Error('LDrawLoader: Invalid luminance value in material definition' + LineParser.getLineNumberString() + '.');
1695
+ }
1414
1696
 
1415
- switch (subobject.locationState) {
1416
- case FILE_LOCATION_AS_IS:
1417
- newLocationState = subobject.locationState + 1;
1697
+ luminance = Math.max(0, Math.min(1, luminance / 255));
1418
1698
  break;
1419
1699
 
1420
- case FILE_LOCATION_TRY_PARTS:
1421
- subobjectURL = 'parts/' + subobjectURL;
1422
- newLocationState = subobject.locationState + 1;
1700
+ case 'CHROME':
1701
+ finishType = FINISH_TYPE_CHROME;
1423
1702
  break;
1424
1703
 
1425
- case FILE_LOCATION_TRY_P:
1426
- subobjectURL = 'p/' + subobjectURL;
1427
- newLocationState = subobject.locationState + 1;
1704
+ case 'PEARLESCENT':
1705
+ finishType = FINISH_TYPE_PEARLESCENT;
1428
1706
  break;
1429
1707
 
1430
- case FILE_LOCATION_TRY_MODELS:
1431
- subobjectURL = 'models/' + subobjectURL;
1432
- newLocationState = subobject.locationState + 1;
1708
+ case 'RUBBER':
1709
+ finishType = FINISH_TYPE_RUBBER;
1433
1710
  break;
1434
1711
 
1435
- case FILE_LOCATION_TRY_RELATIVE:
1436
- subobjectURL = url.substring(0, url.lastIndexOf('/') + 1) + subobjectURL;
1437
- newLocationState = subobject.locationState + 1;
1712
+ case 'MATTE_METALLIC':
1713
+ finishType = FINISH_TYPE_MATTE_METALLIC;
1438
1714
  break;
1439
1715
 
1440
- case FILE_LOCATION_TRY_ABSOLUTE:
1441
- if (subobject.triedLowerCase) {
1442
- // Try absolute path
1443
- newLocationState = FILE_LOCATION_NOT_FOUND;
1444
- } else {
1445
- // Next attempt is lower case
1446
- subobject.fileName = subobject.fileName.toLowerCase();
1447
- subobjectURL = subobject.fileName;
1448
- subobject.triedLowerCase = true;
1449
- newLocationState = FILE_LOCATION_AS_IS;
1450
- }
1716
+ case 'METAL':
1717
+ finishType = FINISH_TYPE_METAL;
1718
+ break;
1451
1719
 
1720
+ case 'MATERIAL':
1721
+ // Not implemented
1722
+ lineParser.setToEnd();
1452
1723
  break;
1453
1724
 
1454
- case FILE_LOCATION_NOT_FOUND:
1455
- // All location possibilities have been tried, give up loading this object
1456
- console.warn('LDrawLoader: Subobject "' + subobject.originalFileName + '" could not be found.');
1457
- return;
1725
+ default:
1726
+ throw new Error('LDrawLoader: Unknown token "' + token + '" while parsing material' + lineParser.getLineNumberString() + '.');
1458
1727
  }
1728
+ }
1729
+
1730
+ let material = null;
1731
+
1732
+ switch (finishType) {
1733
+ case FINISH_TYPE_DEFAULT:
1734
+ material = new MeshStandardMaterial({
1735
+ color: color,
1736
+ roughness: 0.3,
1737
+ metalness: 0
1738
+ });
1739
+ break;
1740
+
1741
+ case FINISH_TYPE_PEARLESCENT:
1742
+ // Try to imitate pearlescency by making the surface glossy
1743
+ material = new MeshStandardMaterial({
1744
+ color: color,
1745
+ roughness: 0.3,
1746
+ metalness: 0.25
1747
+ });
1748
+ break;
1749
+
1750
+ case FINISH_TYPE_CHROME:
1751
+ // Mirror finish surface
1752
+ material = new MeshStandardMaterial({
1753
+ color: color,
1754
+ roughness: 0,
1755
+ metalness: 1
1756
+ });
1757
+ break;
1758
+
1759
+ case FINISH_TYPE_RUBBER:
1760
+ // Rubber finish
1761
+ material = new MeshStandardMaterial({
1762
+ color: color,
1763
+ roughness: 0.9,
1764
+ metalness: 0
1765
+ });
1766
+ break;
1767
+
1768
+ case FINISH_TYPE_MATTE_METALLIC:
1769
+ // Brushed metal finish
1770
+ material = new MeshStandardMaterial({
1771
+ color: color,
1772
+ roughness: 0.8,
1773
+ metalness: 0.4
1774
+ });
1775
+ break;
1459
1776
 
1460
- subobject.locationState = newLocationState;
1461
- subobject.url = subobjectURL; // Load the subobject
1462
- // Use another file loader here so we can keep track of the subobject information
1463
- // and use it when processing the next model.
1464
-
1465
- const fileLoader = new FileLoader(scope.manager);
1466
- fileLoader.setPath(scope.path);
1467
- fileLoader.setRequestHeader(scope.requestHeader);
1468
- fileLoader.setWithCredentials(scope.withCredentials);
1469
- fileLoader.load(subobjectURL, function (text) {
1470
- scope.processObject(text, function (subobjectGroup) {
1471
- onSubobjectLoaded(subobjectGroup, subobject);
1472
- onSubobjectFinish();
1473
- }, subobject, url);
1474
- }, undefined, function (err) {
1475
- onSubobjectError(err, subobject);
1476
- }, subobject);
1777
+ case FINISH_TYPE_METAL:
1778
+ // Average metal finish
1779
+ material = new MeshStandardMaterial({
1780
+ color: color,
1781
+ roughness: 0.2,
1782
+ metalness: 0.85
1783
+ });
1784
+ break;
1477
1785
  }
1478
1786
 
1479
- function onSubobjectLoaded(subobjectGroup, subobject) {
1480
- if (subobjectGroup === null) {
1481
- // Try to reload
1482
- loadSubobject(subobject);
1483
- return;
1484
- }
1787
+ material.transparent = isTransparent;
1788
+ material.premultipliedAlpha = true;
1789
+ material.opacity = alpha;
1790
+ material.depthWrite = !isTransparent;
1791
+ material.polygonOffset = true;
1792
+ material.polygonOffsetFactor = 1;
1485
1793
 
1486
- scope.fileMap[subobject.originalFileName] = subobject.url;
1794
+ if (luminance !== 0) {
1795
+ material.emissive.set(material.color).multiplyScalar(luminance);
1487
1796
  }
1488
1797
 
1489
- function onSubobjectError(err, subobject) {
1490
- // Retry download from a different default possible location
1491
- loadSubobject(subobject);
1798
+ if (!edgeMaterial) {
1799
+ // This is the material used for edges
1800
+ edgeMaterial = new LineBasicMaterial({
1801
+ color: edgeColor,
1802
+ transparent: isTransparent,
1803
+ opacity: alpha,
1804
+ depthWrite: !isTransparent
1805
+ });
1806
+ edgeMaterial.userData.code = code;
1807
+ edgeMaterial.name = name + ' - Edge'; // This is the material used for conditional edges
1808
+
1809
+ edgeMaterial.userData.conditionalEdgeMaterial = new LDrawConditionalLineMaterial({
1810
+ fog: true,
1811
+ transparent: isTransparent,
1812
+ depthWrite: !isTransparent,
1813
+ color: edgeColor,
1814
+ opacity: alpha
1815
+ });
1492
1816
  }
1817
+
1818
+ material.userData.code = code;
1819
+ material.name = name;
1820
+ material.userData.edgeMaterial = edgeMaterial;
1821
+ this.addMaterial(material);
1822
+ return material;
1823
+ }
1824
+
1825
+ computeConstructionSteps(model) {
1826
+ // Sets userdata.constructionStep number in Group objects and userData.numConstructionSteps number in the root Group object.
1827
+ let stepNumber = 0;
1828
+ model.traverse(c => {
1829
+ if (c.isGroup) {
1830
+ if (c.userData.startingConstructionStep) {
1831
+ stepNumber++;
1832
+ }
1833
+
1834
+ c.userData.constructionStep = stepNumber;
1835
+ }
1836
+ });
1837
+ model.userData.numConstructionSteps = stepNumber + 1;
1493
1838
  }
1494
1839
 
1495
1840
  }