three-stdlib 2.8.0 → 2.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. package/cameras/CinematicCamera.cjs.js +1 -1
  2. package/cameras/CinematicCamera.js +3 -8
  3. package/exporters/GLTFExporter.cjs.js +1 -1
  4. package/exporters/GLTFExporter.js +9 -18
  5. package/index.cjs.js +1 -1
  6. package/loaders/EXRLoader.cjs.js +1 -1
  7. package/loaders/EXRLoader.js +21 -10
  8. package/loaders/GLTFLoader.cjs.js +1 -1
  9. package/loaders/GLTFLoader.js +4 -3
  10. package/loaders/HDRCubeTextureLoader.cjs.js +1 -1
  11. package/loaders/HDRCubeTextureLoader.js +1 -3
  12. package/loaders/LDrawLoader.cjs.js +1 -1
  13. package/loaders/LDrawLoader.js +1450 -1105
  14. package/loaders/LUT3dlLoader.cjs.js +1 -1
  15. package/loaders/LUT3dlLoader.js +18 -11
  16. package/loaders/LUTCubeLoader.cjs.js +1 -1
  17. package/loaders/LUTCubeLoader.js +4 -5
  18. package/loaders/PCDLoader.cjs.js +1 -1
  19. package/loaders/PCDLoader.js +2 -2
  20. package/loaders/RGBELoader.cjs.js +1 -1
  21. package/loaders/RGBELoader.js +6 -6
  22. package/loaders/VRMLLoader.cjs.js +1 -1
  23. package/loaders/VRMLLoader.js +10 -18
  24. package/modifiers/CurveModifier.cjs.js +1 -1
  25. package/modifiers/CurveModifier.js +9 -8
  26. package/objects/Lensflare.cjs.js +1 -1
  27. package/objects/Lensflare.js +3 -11
  28. package/objects/Reflector.cjs.js +1 -1
  29. package/objects/Reflector.js +16 -12
  30. package/objects/ReflectorForSSRPass.cjs.js +1 -1
  31. package/objects/ReflectorForSSRPass.js +1 -9
  32. package/objects/Refractor.cjs.js +1 -1
  33. package/objects/Refractor.js +7 -12
  34. package/objects/Water.cjs.js +1 -1
  35. package/objects/Water.js +5 -16
  36. package/package.json +1 -1
  37. package/postprocessing/GlitchPass.cjs.js +1 -1
  38. package/postprocessing/GlitchPass.js +36 -33
  39. package/postprocessing/SMAAPass.cjs.js +1 -1
  40. package/postprocessing/SMAAPass.js +93 -96
  41. package/postprocessing/SSAOPass.cjs.js +1 -1
  42. package/postprocessing/SSAOPass.js +151 -152
  43. package/postprocessing/SavePass.cjs.js +1 -1
  44. package/postprocessing/SavePass.js +27 -28
  45. package/renderers/webgpu/WebGPUTextures.cjs.js +1 -1
  46. package/renderers/webgpu/WebGPUTextures.js +1 -2
  47. package/utils/LDrawUtils.cjs.js +1 -0
  48. package/utils/LDrawUtils.js +144 -0
@@ -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
  }