UNPKG

three-stdlib

Version:

stand-alone library of threejs examples

1 lines 90.4 kB
{"version":3,"file":"LDrawLoader.cjs","sources":["../../src/loaders/LDrawLoader.js"],"sourcesContent":["import {\n BufferAttribute,\n BufferGeometry,\n Color,\n FileLoader,\n Group,\n LineBasicMaterial,\n LineSegments,\n Loader,\n Matrix4,\n Mesh,\n MeshStandardMaterial,\n ShaderMaterial,\n UniformsLib,\n UniformsUtils,\n Vector3,\n Ray,\n} from 'three'\nimport { version } from '../_polyfill/constants'\n\n// Special surface finish tag types.\n// Note: \"MATERIAL\" tag (e.g. GLITTER, SPECKLE) is not implemented\nconst FINISH_TYPE_DEFAULT = 0\nconst FINISH_TYPE_CHROME = 1\nconst FINISH_TYPE_PEARLESCENT = 2\nconst FINISH_TYPE_RUBBER = 3\nconst FINISH_TYPE_MATTE_METALLIC = 4\nconst FINISH_TYPE_METAL = 5\n\n// State machine to search a subobject path.\n// The LDraw standard establishes these various possible subfolders.\nconst FILE_LOCATION_AS_IS = 0\nconst FILE_LOCATION_TRY_PARTS = 1\nconst FILE_LOCATION_TRY_P = 2\nconst FILE_LOCATION_TRY_MODELS = 3\nconst FILE_LOCATION_TRY_RELATIVE = 4\nconst FILE_LOCATION_TRY_ABSOLUTE = 5\nconst FILE_LOCATION_NOT_FOUND = 6\n\nconst MAIN_COLOUR_CODE = '16'\nconst MAIN_EDGE_COLOUR_CODE = '24'\n\nconst _tempVec0 = /* @__PURE__ */ new Vector3()\nconst _tempVec1 = /* @__PURE__ */ new Vector3()\n\nclass LDrawConditionalLineMaterial extends ShaderMaterial {\n constructor(parameters) {\n super({\n uniforms: UniformsUtils.merge([\n UniformsLib.fog,\n {\n diffuse: {\n value: new Color(),\n },\n opacity: {\n value: 1.0,\n },\n },\n ]),\n\n vertexShader: /* glsl */ `\n attribute vec3 control0;\n attribute vec3 control1;\n attribute vec3 direction;\n varying float discardFlag;\n\n #include <common>\n #include <color_pars_vertex>\n #include <fog_pars_vertex>\n #include <logdepthbuf_pars_vertex>\n #include <clipping_planes_pars_vertex>\n\n void main() {\n #include <color_vertex>\n\n vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);\n gl_Position = projectionMatrix * mvPosition;\n\n // Transform the line segment ends and control points into camera clip space\n vec4 c0 = projectionMatrix * modelViewMatrix * vec4(control0, 1.0);\n vec4 c1 = projectionMatrix * modelViewMatrix * vec4(control1, 1.0);\n vec4 p0 = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n vec4 p1 = projectionMatrix * modelViewMatrix * vec4(position + direction, 1.0);\n\n c0.xy /= c0.w;\n c1.xy /= c1.w;\n p0.xy /= p0.w;\n p1.xy /= p1.w;\n\n // Get the direction of the segment and an orthogonal vector\n vec2 dir = p1.xy - p0.xy;\n vec2 norm = vec2(-dir.y, dir.x);\n\n // Get control point directions from the line\n vec2 c0dir = c0.xy - p1.xy;\n vec2 c1dir = c1.xy - p1.xy;\n\n // If the vectors to the controls points are pointed in different directions away\n // from the line segment then the line should not be drawn.\n float d0 = dot(normalize(norm), normalize(c0dir));\n float d1 = dot(normalize(norm), normalize(c1dir));\n discardFlag = float(sign(d0) != sign(d1));\n\n #include <logdepthbuf_vertex>\n #include <clipping_planes_vertex>\n #include <fog_vertex>\n }\n `,\n\n fragmentShader: /* glsl */ `\n uniform vec3 diffuse;\n uniform float opacity;\n varying float discardFlag;\n\n #include <common>\n #include <color_pars_fragment>\n #include <fog_pars_fragment>\n #include <logdepthbuf_pars_fragment>\n #include <clipping_planes_pars_fragment>\n\n void main() {\n if (discardFlag > 0.5) discard;\n\n #include <clipping_planes_fragment>\n vec3 outgoingLight = vec3(0.0);\n vec4 diffuseColor = vec4(diffuse, opacity);\n #include <logdepthbuf_fragment>\n #include <color_fragment>\n outgoingLight = diffuseColor.rgb; // simple shader\n gl_FragColor = vec4(outgoingLight, diffuseColor.a);\n #include <tonemapping_fragment>\n #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}>\n #include <fog_fragment>\n #include <premultiplied_alpha_fragment>\n }\n `,\n })\n\n Object.defineProperties(this, {\n opacity: {\n get: function () {\n return this.uniforms.opacity.value\n },\n\n set: function (value) {\n this.uniforms.opacity.value = value\n },\n },\n\n color: {\n get: function () {\n return this.uniforms.diffuse.value\n },\n },\n })\n\n this.setValues(parameters)\n this.isLDrawConditionalLineMaterial = true\n }\n}\n\nclass ConditionalLineSegments extends LineSegments {\n constructor(geometry, material) {\n super(geometry, material)\n this.isConditionalLine = true\n }\n}\n\nfunction generateFaceNormals(faces) {\n for (let i = 0, l = faces.length; i < l; i++) {\n const face = faces[i]\n const vertices = face.vertices\n const v0 = vertices[0]\n const v1 = vertices[1]\n const v2 = vertices[2]\n\n _tempVec0.subVectors(v1, v0)\n _tempVec1.subVectors(v2, v1)\n face.faceNormal = new Vector3().crossVectors(_tempVec0, _tempVec1).normalize()\n }\n}\n\nconst _ray = /* @__PURE__ */ new Ray()\nfunction smoothNormals(faces, lineSegments, checkSubSegments = false) {\n // NOTE: 1e2 is pretty coarse but was chosen to quantize the resulting value because\n // it allows edges to be smoothed as expected (see minifig arms).\n // --\n // And the vector values are initialize multiplied by 1 + 1e-10 to account for floating\n // point errors on vertices along quantization boundaries. Ie after matrix multiplication\n // vertices that should be merged might be set to \"1.7\" and \"1.6999...\" meaning they won't\n // get merged. This added epsilon attempts to push these error values to the same quantized\n // value for the sake of hashing. See \"AT-ST mini\" dishes. See mrdoob/three#23169.\n\n const hashMultiplier = (1 + 1e-10) * 1e2\n function hashVertex(v) {\n const x = ~~(v.x * hashMultiplier)\n const y = ~~(v.y * hashMultiplier)\n const z = ~~(v.z * hashMultiplier)\n\n return `${x},${y},${z}`\n }\n\n function hashEdge(v0, v1) {\n return `${hashVertex(v0)}_${hashVertex(v1)}`\n }\n\n // converts the two vertices to a ray with a normalized direction and origin of 0, 0, 0 projected\n // onto the original line.\n function toNormalizedRay(v0, v1, targetRay) {\n targetRay.direction.subVectors(v1, v0).normalize()\n\n const scalar = v0.dot(targetRay.direction)\n targetRay.origin.copy(v0).addScaledVector(targetRay.direction, -scalar)\n\n return targetRay\n }\n\n function hashRay(ray) {\n return hashEdge(ray.origin, ray.direction)\n }\n\n const hardEdges = new Set()\n const hardEdgeRays = new Map()\n const halfEdgeList = {}\n const normals = []\n\n // Save the list of hard edges by hash\n for (let i = 0, l = lineSegments.length; i < l; i++) {\n const ls = lineSegments[i]\n const vertices = ls.vertices\n const v0 = vertices[0]\n const v1 = vertices[1]\n hardEdges.add(hashEdge(v0, v1))\n hardEdges.add(hashEdge(v1, v0))\n\n // only generate the hard edge ray map if we're checking subsegments because it's more expensive to check\n // and requires more memory.\n if (checkSubSegments) {\n // add both ray directions to the map\n const ray = toNormalizedRay(v0, v1, new Ray())\n const rh1 = hashRay(ray)\n if (!hardEdgeRays.has(rh1)) {\n toNormalizedRay(v1, v0, ray)\n const rh2 = hashRay(ray)\n\n const info = {\n ray,\n distances: [],\n }\n\n hardEdgeRays.set(rh1, info)\n hardEdgeRays.set(rh2, info)\n }\n\n // store both segments ends in min, max order in the distances array to check if a face edge is a\n // subsegment later.\n const info = hardEdgeRays.get(rh1)\n let d0 = info.ray.direction.dot(v0)\n let d1 = info.ray.direction.dot(v1)\n if (d0 > d1) {\n ;[d0, d1] = [d1, d0]\n }\n\n info.distances.push(d0, d1)\n }\n }\n\n // track the half edges associated with each triangle\n for (let i = 0, l = faces.length; i < l; i++) {\n const tri = faces[i]\n const vertices = tri.vertices\n const vertCount = vertices.length\n for (let i2 = 0; i2 < vertCount; i2++) {\n const index = i2\n const next = (i2 + 1) % vertCount\n const v0 = vertices[index]\n const v1 = vertices[next]\n const hash = hashEdge(v0, v1)\n\n // don't add the triangle if the edge is supposed to be hard\n if (hardEdges.has(hash)) {\n continue\n }\n\n // if checking subsegments then check to see if this edge lies on a hard edge ray and whether its within any ray bounds\n if (checkSubSegments) {\n toNormalizedRay(v0, v1, _ray)\n\n const rayHash = hashRay(_ray)\n if (hardEdgeRays.has(rayHash)) {\n const info = hardEdgeRays.get(rayHash)\n const { ray, distances } = info\n let d0 = ray.direction.dot(v0)\n let d1 = ray.direction.dot(v1)\n\n if (d0 > d1) {\n ;[d0, d1] = [d1, d0]\n }\n\n // return early if the face edge is found to be a subsegment of a line edge meaning the edge will have \"hard\" normals\n let found = false\n for (let i = 0, l = distances.length; i < l; i += 2) {\n if (d0 >= distances[i] && d1 <= distances[i + 1]) {\n found = true\n break\n }\n }\n\n if (found) {\n continue\n }\n }\n }\n\n const info = {\n index: index,\n tri: tri,\n }\n halfEdgeList[hash] = info\n }\n }\n\n // Iterate until we've tried to connect all faces to share normals\n while (true) {\n // Stop if there are no more faces left\n let halfEdge = null\n for (const key in halfEdgeList) {\n halfEdge = halfEdgeList[key]\n break\n }\n\n if (halfEdge === null) {\n break\n }\n\n // Exhaustively find all connected faces\n const queue = [halfEdge]\n while (queue.length > 0) {\n // initialize all vertex normals in this triangle\n const tri = queue.pop().tri\n const vertices = tri.vertices\n const vertNormals = tri.normals\n const faceNormal = tri.faceNormal\n\n // Check if any edge is connected to another triangle edge\n const vertCount = vertices.length\n for (let i2 = 0; i2 < vertCount; i2++) {\n const index = i2\n const next = (i2 + 1) % vertCount\n const v0 = vertices[index]\n const v1 = vertices[next]\n\n // delete this triangle from the list so it won't be found again\n const hash = hashEdge(v0, v1)\n delete halfEdgeList[hash]\n\n const reverseHash = hashEdge(v1, v0)\n const otherInfo = halfEdgeList[reverseHash]\n if (otherInfo) {\n const otherTri = otherInfo.tri\n const otherIndex = otherInfo.index\n const otherNormals = otherTri.normals\n const otherVertCount = otherNormals.length\n const otherFaceNormal = otherTri.faceNormal\n\n // NOTE: If the angle between faces is > 67.5 degrees then assume it's\n // hard edge. There are some cases where the line segments do not line up exactly\n // with or span multiple triangle edges (see Lunar Vehicle wheels).\n if (Math.abs(otherTri.faceNormal.dot(tri.faceNormal)) < 0.25) {\n continue\n }\n\n // if this triangle has already been traversed then it won't be in\n // the halfEdgeList. If it has not then add it to the queue and delete\n // it so it won't be found again.\n if (reverseHash in halfEdgeList) {\n queue.push(otherInfo)\n delete halfEdgeList[reverseHash]\n }\n\n // share the first normal\n const otherNext = (otherIndex + 1) % otherVertCount\n if (vertNormals[index] && otherNormals[otherNext] && vertNormals[index] !== otherNormals[otherNext]) {\n otherNormals[otherNext].norm.add(vertNormals[index].norm)\n vertNormals[index].norm = otherNormals[otherNext].norm\n }\n\n let sharedNormal1 = vertNormals[index] || otherNormals[otherNext]\n if (sharedNormal1 === null) {\n // it's possible to encounter an edge of a triangle that has already been traversed meaning\n // both edges already have different normals defined and shared. To work around this we create\n // a wrapper object so when those edges are merged the normals can be updated everywhere.\n sharedNormal1 = { norm: new Vector3() }\n normals.push(sharedNormal1.norm)\n }\n\n if (vertNormals[index] === null) {\n vertNormals[index] = sharedNormal1\n sharedNormal1.norm.add(faceNormal)\n }\n\n if (otherNormals[otherNext] === null) {\n otherNormals[otherNext] = sharedNormal1\n sharedNormal1.norm.add(otherFaceNormal)\n }\n\n // share the second normal\n if (vertNormals[next] && otherNormals[otherIndex] && vertNormals[next] !== otherNormals[otherIndex]) {\n otherNormals[otherIndex].norm.add(vertNormals[next].norm)\n vertNormals[next].norm = otherNormals[otherIndex].norm\n }\n\n let sharedNormal2 = vertNormals[next] || otherNormals[otherIndex]\n if (sharedNormal2 === null) {\n sharedNormal2 = { norm: new Vector3() }\n normals.push(sharedNormal2.norm)\n }\n\n if (vertNormals[next] === null) {\n vertNormals[next] = sharedNormal2\n sharedNormal2.norm.add(faceNormal)\n }\n\n if (otherNormals[otherIndex] === null) {\n otherNormals[otherIndex] = sharedNormal2\n sharedNormal2.norm.add(otherFaceNormal)\n }\n }\n }\n }\n }\n\n // The normals of each face have been added up so now we average them by normalizing the vector.\n for (let i = 0, l = normals.length; i < l; i++) {\n normals[i].normalize()\n }\n}\n\nfunction isPartType(type) {\n return type === 'Part' || type === 'Unofficial_Part'\n}\n\nfunction isPrimitiveType(type) {\n return /primitive/i.test(type) || type === 'Subpart'\n}\n\nclass LineParser {\n constructor(line, lineNumber) {\n this.line = line\n this.lineLength = line.length\n this.currentCharIndex = 0\n this.currentChar = ' '\n this.lineNumber = lineNumber\n }\n\n seekNonSpace() {\n while (this.currentCharIndex < this.lineLength) {\n this.currentChar = this.line.charAt(this.currentCharIndex)\n\n if (this.currentChar !== ' ' && this.currentChar !== '\\t') {\n return\n }\n\n this.currentCharIndex++\n }\n }\n\n getToken() {\n const pos0 = this.currentCharIndex++\n\n // Seek space\n while (this.currentCharIndex < this.lineLength) {\n this.currentChar = this.line.charAt(this.currentCharIndex)\n\n if (this.currentChar === ' ' || this.currentChar === '\\t') {\n break\n }\n\n this.currentCharIndex++\n }\n\n const pos1 = this.currentCharIndex\n\n this.seekNonSpace()\n\n return this.line.substring(pos0, pos1)\n }\n\n getVector() {\n return new Vector3(parseFloat(this.getToken()), parseFloat(this.getToken()), parseFloat(this.getToken()))\n }\n\n getRemainingString() {\n return this.line.substring(this.currentCharIndex, this.lineLength)\n }\n\n isAtTheEnd() {\n return this.currentCharIndex >= this.lineLength\n }\n\n setToEnd() {\n this.currentCharIndex = this.lineLength\n }\n\n getLineNumberString() {\n return this.lineNumber >= 0 ? ' at line ' + this.lineNumber : ''\n }\n}\n\n// Fetches and parses an intermediate representation of LDraw parts files.\nclass LDrawParsedCache {\n constructor(loader) {\n this.loader = loader\n this._cache = {}\n }\n\n cloneResult(original) {\n const result = {}\n\n // vertices are transformed and normals computed before being converted to geometry\n // so these pieces must be cloned.\n result.faces = original.faces.map((face) => {\n return {\n colorCode: face.colorCode,\n material: face.material,\n vertices: face.vertices.map((v) => v.clone()),\n normals: face.normals.map(() => null),\n faceNormal: null,\n }\n })\n\n result.conditionalSegments = original.conditionalSegments.map((face) => {\n return {\n colorCode: face.colorCode,\n material: face.material,\n vertices: face.vertices.map((v) => v.clone()),\n controlPoints: face.controlPoints.map((v) => v.clone()),\n }\n })\n\n result.lineSegments = original.lineSegments.map((face) => {\n return {\n colorCode: face.colorCode,\n material: face.material,\n vertices: face.vertices.map((v) => v.clone()),\n }\n })\n\n // none if this is subsequently modified\n result.type = original.type\n result.category = original.category\n result.keywords = original.keywords\n result.subobjects = original.subobjects\n result.totalFaces = original.totalFaces\n result.startingConstructionStep = original.startingConstructionStep\n result.materials = original.materials\n result.group = null\n return result\n }\n\n async fetchData(fileName) {\n let triedLowerCase = false\n let locationState = FILE_LOCATION_AS_IS\n while (locationState !== FILE_LOCATION_NOT_FOUND) {\n let subobjectURL = fileName\n switch (locationState) {\n case FILE_LOCATION_AS_IS:\n locationState = locationState + 1\n break\n\n case FILE_LOCATION_TRY_PARTS:\n subobjectURL = 'parts/' + subobjectURL\n locationState = locationState + 1\n break\n\n case FILE_LOCATION_TRY_P:\n subobjectURL = 'p/' + subobjectURL\n locationState = locationState + 1\n break\n\n case FILE_LOCATION_TRY_MODELS:\n subobjectURL = 'models/' + subobjectURL\n locationState = locationState + 1\n break\n\n case FILE_LOCATION_TRY_RELATIVE:\n subobjectURL = fileName.substring(0, fileName.lastIndexOf('/') + 1) + subobjectURL\n locationState = locationState + 1\n break\n\n case FILE_LOCATION_TRY_ABSOLUTE:\n if (triedLowerCase) {\n // Try absolute path\n locationState = FILE_LOCATION_NOT_FOUND\n } else {\n // Next attempt is lower case\n fileName = fileName.toLowerCase()\n subobjectURL = fileName\n triedLowerCase = true\n locationState = FILE_LOCATION_AS_IS\n }\n\n break\n }\n\n const loader = this.loader\n const fileLoader = new FileLoader(loader.manager)\n fileLoader.setPath(loader.partsLibraryPath)\n fileLoader.setRequestHeader(loader.requestHeader)\n fileLoader.setWithCredentials(loader.withCredentials)\n\n try {\n const text = await fileLoader.loadAsync(subobjectURL)\n return text\n } catch {\n continue\n }\n }\n\n throw new Error('LDrawLoader: Subobject \"' + fileName + '\" could not be loaded.')\n }\n\n parse(text, fileName = null) {\n const loader = this.loader\n\n // final results\n const faces = []\n const lineSegments = []\n const conditionalSegments = []\n const subobjects = []\n const materials = {}\n\n const getLocalMaterial = (colorCode) => {\n return materials[colorCode] || null\n }\n\n let type = 'Model'\n let category = null\n let keywords = null\n let totalFaces = 0\n\n // split into lines\n if (text.indexOf('\\r\\n') !== -1) {\n // This is faster than String.split with regex that splits on both\n text = text.replace(/\\r\\n/g, '\\n')\n }\n\n const lines = text.split('\\n')\n const numLines = lines.length\n\n let parsingEmbeddedFiles = false\n let currentEmbeddedFileName = null\n let currentEmbeddedText = null\n\n let bfcCertified = false\n let bfcCCW = true\n let bfcInverted = false\n let bfcCull = true\n\n let startingConstructionStep = false\n\n // Parse all line commands\n for (let lineIndex = 0; lineIndex < numLines; lineIndex++) {\n const line = lines[lineIndex]\n\n if (line.length === 0) continue\n\n if (parsingEmbeddedFiles) {\n if (line.startsWith('0 FILE ')) {\n // Save previous embedded file in the cache\n this.setData(currentEmbeddedFileName, currentEmbeddedText)\n\n // New embedded text file\n currentEmbeddedFileName = line.substring(7)\n currentEmbeddedText = ''\n } else {\n currentEmbeddedText += line + '\\n'\n }\n\n continue\n }\n\n const lp = new LineParser(line, lineIndex + 1)\n lp.seekNonSpace()\n\n if (lp.isAtTheEnd()) {\n // Empty line\n continue\n }\n\n // Parse the line type\n const lineType = lp.getToken()\n\n let material\n let colorCode\n let segment\n let ccw\n let doubleSided\n let v0, v1, v2, v3, c0, c1\n\n switch (lineType) {\n // Line type 0: Comment or META\n case '0':\n // Parse meta directive\n const meta = lp.getToken()\n\n if (meta) {\n switch (meta) {\n case '!LDRAW_ORG':\n type = lp.getToken()\n break\n\n case '!COLOUR':\n material = loader.parseColorMetaDirective(lp)\n if (material) {\n materials[material.userData.code] = material\n } else {\n console.warn('LDrawLoader: Error parsing material' + lp.getLineNumberString())\n }\n\n break\n\n case '!CATEGORY':\n category = lp.getToken()\n break\n\n case '!KEYWORDS':\n const newKeywords = lp.getRemainingString().split(',')\n if (newKeywords.length > 0) {\n if (!keywords) {\n keywords = []\n }\n\n newKeywords.forEach(function (keyword) {\n keywords.push(keyword.trim())\n })\n }\n\n break\n\n case 'FILE':\n if (lineIndex > 0) {\n // Start embedded text files parsing\n parsingEmbeddedFiles = true\n currentEmbeddedFileName = lp.getRemainingString()\n currentEmbeddedText = ''\n\n bfcCertified = false\n bfcCCW = true\n }\n\n break\n\n case 'BFC':\n // Changes to the backface culling state\n while (!lp.isAtTheEnd()) {\n const token = lp.getToken()\n\n switch (token) {\n case 'CERTIFY':\n case 'NOCERTIFY':\n bfcCertified = token === 'CERTIFY'\n bfcCCW = true\n\n break\n\n case 'CW':\n case 'CCW':\n bfcCCW = token === 'CCW'\n\n break\n\n case 'INVERTNEXT':\n bfcInverted = true\n\n break\n\n case 'CLIP':\n case 'NOCLIP':\n bfcCull = token === 'CLIP'\n\n break\n\n default:\n console.warn('THREE.LDrawLoader: BFC directive \"' + token + '\" is unknown.')\n\n break\n }\n }\n\n break\n\n case 'STEP':\n startingConstructionStep = true\n\n break\n\n default:\n // Other meta directives are not implemented\n break\n }\n }\n\n break\n\n // Line type 1: Sub-object file\n case '1':\n colorCode = lp.getToken()\n material = getLocalMaterial(colorCode)\n\n const posX = parseFloat(lp.getToken())\n const posY = parseFloat(lp.getToken())\n const posZ = parseFloat(lp.getToken())\n const m0 = parseFloat(lp.getToken())\n const m1 = parseFloat(lp.getToken())\n const m2 = parseFloat(lp.getToken())\n const m3 = parseFloat(lp.getToken())\n const m4 = parseFloat(lp.getToken())\n const m5 = parseFloat(lp.getToken())\n const m6 = parseFloat(lp.getToken())\n const m7 = parseFloat(lp.getToken())\n const m8 = parseFloat(lp.getToken())\n\n const matrix = new Matrix4().set(m0, m1, m2, posX, m3, m4, m5, posY, m6, m7, m8, posZ, 0, 0, 0, 1)\n\n let fileName = lp.getRemainingString().trim().replace(/\\\\/g, '/')\n\n if (loader.fileMap[fileName]) {\n // Found the subobject path in the preloaded file path map\n fileName = loader.fileMap[fileName]\n } else {\n // Standardized subfolders\n if (fileName.startsWith('s/')) {\n fileName = 'parts/' + fileName\n } else if (fileName.startsWith('48/')) {\n fileName = 'p/' + fileName\n }\n }\n\n subobjects.push({\n material: material,\n colorCode: colorCode,\n matrix: matrix,\n fileName: fileName,\n inverted: bfcInverted,\n startingConstructionStep: startingConstructionStep,\n })\n\n bfcInverted = false\n\n break\n\n // Line type 2: Line segment\n case '2':\n colorCode = lp.getToken()\n material = getLocalMaterial(colorCode)\n v0 = lp.getVector()\n v1 = lp.getVector()\n\n segment = {\n material: material,\n colorCode: colorCode,\n vertices: [v0, v1],\n }\n\n lineSegments.push(segment)\n\n break\n\n // Line type 5: Conditional Line segment\n case '5':\n colorCode = lp.getToken()\n material = getLocalMaterial(colorCode)\n v0 = lp.getVector()\n v1 = lp.getVector()\n c0 = lp.getVector()\n c1 = lp.getVector()\n\n segment = {\n material: material,\n colorCode: colorCode,\n vertices: [v0, v1],\n controlPoints: [c0, c1],\n }\n\n conditionalSegments.push(segment)\n\n break\n\n // Line type 3: Triangle\n case '3':\n colorCode = lp.getToken()\n material = getLocalMaterial(colorCode)\n ccw = bfcCCW\n doubleSided = !bfcCertified || !bfcCull\n\n if (ccw === true) {\n v0 = lp.getVector()\n v1 = lp.getVector()\n v2 = lp.getVector()\n } else {\n v2 = lp.getVector()\n v1 = lp.getVector()\n v0 = lp.getVector()\n }\n\n faces.push({\n material: material,\n colorCode: colorCode,\n faceNormal: null,\n vertices: [v0, v1, v2],\n normals: [null, null, null],\n })\n totalFaces++\n\n if (doubleSided === true) {\n faces.push({\n material: material,\n colorCode: colorCode,\n faceNormal: null,\n vertices: [v2, v1, v0],\n normals: [null, null, null],\n })\n totalFaces++\n }\n\n break\n\n // Line type 4: Quadrilateral\n case '4':\n colorCode = lp.getToken()\n material = getLocalMaterial(colorCode)\n ccw = bfcCCW\n doubleSided = !bfcCertified || !bfcCull\n\n if (ccw === true) {\n v0 = lp.getVector()\n v1 = lp.getVector()\n v2 = lp.getVector()\n v3 = lp.getVector()\n } else {\n v3 = lp.getVector()\n v2 = lp.getVector()\n v1 = lp.getVector()\n v0 = lp.getVector()\n }\n\n // specifically place the triangle diagonal in the v0 and v1 slots so we can\n // account for the doubling of vertices later when smoothing normals.\n faces.push({\n material: material,\n colorCode: colorCode,\n faceNormal: null,\n vertices: [v0, v1, v2, v3],\n normals: [null, null, null, null],\n })\n totalFaces += 2\n\n if (doubleSided === true) {\n faces.push({\n material: material,\n colorCode: colorCode,\n faceNormal: null,\n vertices: [v3, v2, v1, v0],\n normals: [null, null, null, null],\n })\n totalFaces += 2\n }\n\n break\n\n default:\n throw new Error('LDrawLoader: Unknown line type \"' + lineType + '\"' + lp.getLineNumberString() + '.')\n }\n }\n\n if (parsingEmbeddedFiles) {\n this.setData(currentEmbeddedFileName, currentEmbeddedText)\n }\n\n return {\n faces,\n conditionalSegments,\n lineSegments,\n type,\n category,\n keywords,\n subobjects,\n totalFaces,\n startingConstructionStep,\n materials,\n fileName,\n group: null,\n }\n }\n\n // returns an (optionally cloned) instance of the data\n getData(fileName, clone = true) {\n const key = fileName.toLowerCase()\n const result = this._cache[key]\n if (result === null || result instanceof Promise) {\n return null\n }\n\n if (clone) {\n return this.cloneResult(result)\n } else {\n return result\n }\n }\n\n // kicks off a fetch and parse of the requested data if it hasn't already been loaded. Returns when\n // the data is ready to use and can be retrieved synchronously with \"getData\".\n async ensureDataLoaded(fileName) {\n const key = fileName.toLowerCase()\n if (!(key in this._cache)) {\n // replace the promise with a copy of the parsed data for immediate processing\n this._cache[key] = this.fetchData(fileName).then((text) => {\n const info = this.parse(text, fileName)\n this._cache[key] = info\n return info\n })\n }\n\n await this._cache[key]\n }\n\n // sets the data in the cache from parsed data\n setData(fileName, text) {\n const key = fileName.toLowerCase()\n this._cache[key] = this.parse(text, fileName)\n }\n}\n\n// returns the material for an associated color code. If the color code is 16 for a face or 24 for\n// an edge then the passthroughColorCode is used.\nfunction getMaterialFromCode(colorCode, parentColorCode, materialHierarchy, forEdge) {\n const isPassthrough = (!forEdge && colorCode === MAIN_COLOUR_CODE) || (forEdge && colorCode === MAIN_EDGE_COLOUR_CODE)\n if (isPassthrough) {\n colorCode = parentColorCode\n }\n\n return materialHierarchy[colorCode] || null\n}\n\n// Class used to parse and build LDraw parts as three.js objects and cache them if they're a \"Part\" type.\nclass LDrawPartsGeometryCache {\n constructor(loader) {\n this.loader = loader\n this.parseCache = new LDrawParsedCache(loader)\n this._cache = {}\n }\n\n // Convert the given file information into a mesh by processing subobjects.\n async processIntoMesh(info) {\n const loader = this.loader\n const parseCache = this.parseCache\n const faceMaterials = new Set()\n\n // Processes the part subobject information to load child parts and merge geometry onto part\n // piece object.\n const processInfoSubobjects = async (info, subobject = null) => {\n const subobjects = info.subobjects\n const promises = []\n\n // Trigger load of all subobjects. If a subobject isn't a primitive then load it as a separate\n // group which lets instruction steps apply correctly.\n for (let i = 0, l = subobjects.length; i < l; i++) {\n const subobject = subobjects[i]\n const promise = parseCache.ensureDataLoaded(subobject.fileName).then(() => {\n const subobjectInfo = parseCache.getData(subobject.fileName, false)\n if (!isPrimitiveType(subobjectInfo.type)) {\n return this.loadModel(subobject.fileName).catch((error) => {\n console.warn(error)\n return null\n })\n }\n\n return processInfoSubobjects(parseCache.getData(subobject.fileName), subobject)\n })\n\n promises.push(promise)\n }\n\n const group = new Group()\n group.userData.category = info.category\n group.userData.keywords = info.keywords\n info.group = group\n\n const subobjectInfos = await Promise.all(promises)\n for (let i = 0, l = subobjectInfos.length; i < l; i++) {\n const subobject = info.subobjects[i]\n const subobjectInfo = subobjectInfos[i]\n\n if (subobjectInfo === null) {\n // the subobject failed to load\n continue\n }\n\n // if the subobject was loaded as a separate group then apply the parent scopes materials\n if (subobjectInfo.isGroup) {\n const subobjectGroup = subobjectInfo\n subobject.matrix.decompose(subobjectGroup.position, subobjectGroup.quaternion, subobjectGroup.scale)\n subobjectGroup.userData.startingConstructionStep = subobject.startingConstructionStep\n subobjectGroup.name = subobject.fileName\n\n loader.applyMaterialsToMesh(subobjectGroup, subobject.colorCode, info.materials)\n\n group.add(subobjectGroup)\n continue\n }\n\n // add the subobject group if it has children in case it has both children and primitives\n if (subobjectInfo.group.children.length) {\n group.add(subobjectInfo.group)\n }\n\n // transform the primitives into the local space of the parent piece and append them to\n // to the parent primitives list.\n const parentLineSegments = info.lineSegments\n const parentConditionalSegments = info.conditionalSegments\n const parentFaces = info.faces\n\n const lineSegments = subobjectInfo.lineSegments\n const conditionalSegments = subobjectInfo.conditionalSegments\n\n const faces = subobjectInfo.faces\n const matrix = subobject.matrix\n const inverted = subobject.inverted\n const matrixScaleInverted = matrix.determinant() < 0\n const colorCode = subobject.colorCode\n\n const lineColorCode = colorCode === MAIN_COLOUR_CODE ? MAIN_EDGE_COLOUR_CODE : colorCode\n for (let i = 0, l = lineSegments.length; i < l; i++) {\n const ls = lineSegments[i]\n const vertices = ls.vertices\n vertices[0].applyMatrix4(matrix)\n vertices[1].applyMatrix4(matrix)\n ls.colorCode = ls.colorCode === MAIN_EDGE_COLOUR_CODE ? lineColorCode : ls.colorCode\n ls.material = ls.material || getMaterialFromCode(ls.colorCode, ls.colorCode, info.materials, true)\n\n parentLineSegments.push(ls)\n }\n\n for (let i = 0, l = conditionalSegments.length; i < l; i++) {\n const os = conditionalSegments[i]\n const vertices = os.vertices\n const controlPoints = os.controlPoints\n vertices[0].applyMatrix4(matrix)\n vertices[1].applyMatrix4(matrix)\n controlPoints[0].applyMatrix4(matrix)\n controlPoints[1].applyMatrix4(matrix)\n os.colorCode = os.colorCode === MAIN_EDGE_COLOUR_CODE ? lineColorCode : os.colorCode\n os.material = os.material || getMaterialFromCode(os.colorCode, os.colorCode, info.materials, true)\n\n parentConditionalSegments.push(os)\n }\n\n for (let i = 0, l = faces.length; i < l; i++) {\n const tri = faces[i]\n const vertices = tri.vertices\n for (let i = 0, l = vertices.length; i < l; i++) {\n vertices[i].applyMatrix4(matrix)\n }\n\n tri.colorCode = tri.colorCode === MAIN_COLOUR_CODE ? colorCode : tri.colorCode\n tri.material = tri.material || getMaterialFromCode(tri.colorCode, colorCode, info.materials, false)\n faceMaterials.add(tri.colorCode)\n\n // If the scale of the object is negated then the triangle winding order\n // needs to be flipped.\n if (matrixScaleInverted !== inverted) {\n vertices.reverse()\n }\n\n parentFaces.push(tri)\n }\n\n info.totalFaces += subobjectInfo.totalFaces\n }\n\n // Apply the parent subobjects pass through material code to this object. This is done several times due\n // to material scoping.\n if (subobject) {\n loader.applyMaterialsToMesh(group, subobject.colorCode, info.materials)\n }\n\n return info\n }\n\n // Track material use to see if we need to use the normal smooth slow path for hard edges.\n for (let i = 0, l = info.faces; i < l; i++) {\n faceMaterials.add(info.faces[i].colorCode)\n }\n\n await processInfoSubobjects(info)\n\n if (loader.smoothNormals) {\n const checkSubSegments = faceMaterials.size > 1\n generateFaceNormals(info.faces)\n smoothNormals(info.faces, info.lineSegments, checkSubSegments)\n }\n\n // Add the primitive objects and metadata.\n const group = info.group\n if (info.faces.length > 0) {\n group.add(createObject(info.faces, 3, false, info.totalFaces))\n }\n\n if (info.lineSegments.length > 0) {\n group.add(createObject(info.lineSegments, 2))\n }\n\n if (info.conditionalSegments.length > 0) {\n group.add(createObject(info.conditionalSegments, 2, true))\n }\n\n return group\n }\n\n hasCachedModel(fileName) {\n return fileName !== null && fileName.toLowerCase() in this._cache\n }\n\n async getCachedModel(fileName) {\n if (fileName !== null && this.hasCachedModel(fileName)) {\n const key = fileName.toLowerCase()\n const group = await this._cache[key]\n return group.clone()\n } else {\n return null\n }\n }\n\n // Loads and parses the model with the given file name. Returns a cached copy if available.\n async loadModel(fileName) {\n const parseCache = this.parseCache\n const key = fileName.toLowerCase()\n if (this.hasCachedModel(fileName)) {\n // Return cached model if available.\n return this.getCachedModel(fileName)\n } else {\n // Otherwise parse a new model.\n // Ensure the file data is loaded and pre parsed.\n await parseCache.ensureDataLoaded(fileName)\n\n const info = parseCache.getData(fileName)\n const promise = this.processIntoMesh(info)\n\n // Now that the file has loaded it's possible that another part parse has been waiting in parallel\n // so check the cache again to see if it's been added since the last async operation so we don't\n // do unnecessary work.\n if (this.hasCachedModel(fileName)) {\n return this.getCachedModel(fileName)\n }\n\n // Cache object if it's a part so it can be reused later.\n if (isPartType(info.type)) {\n this._cache[key] = promise\n }\n\n // return a copy\n const group = await promise\n return group.clone()\n }\n }\n\n // parses the given model text into a renderable object. Returns cached copy if available.\n async parseModel(text) {\n const parseCache = this.parseCache\n const info = parseCache.parse(text)\n if (isPartType(info.type) && this.hasCachedModel(info.fileName)) {\n return this.getCachedModel(info.fileName)\n }\n\n return this.processIntoMesh(info)\n }\n}\n\nfunction sortByMaterial(a, b) {\n if (a.colorCode === b.colorCode) {\n return 0\n }\n\n if (a.colorCode < b.colorCode) {\n return -1\n }\n\n return 1\n}\n\nfunction createObject(elements, elementSize, isConditionalSegments = false, totalElements = null) {\n // Creates a LineSegments (elementSize = 2) or a Mesh (elementSize = 3 )\n // With per face / segment material, implemented with mesh groups and materials array\n\n // Sort the faces or line segments by color code to make later the mesh groups\n elements.sort(sortByMaterial)\n\n if (totalElements === null) {\n totalElements = elements.length\n }\n\n const positions = new Float32Array(elementSize * totalElements * 3)\n const normals = elementSize === 3 ? new Float32Array(elementSize * totalElements * 3) : null\n const materials = []\n\n const quadArray = new Array(6)\n const bufferGeometry = new BufferGeometry()\n let prevMaterial = null\n let index0 = 0\n let numGroupVerts = 0\n let offset = 0\n\n for (let iElem = 0, nElem = elements.length; iElem < nElem; iElem++) {\n const elem = elements[iElem]\n let vertices = elem.vertices\n if (vertices.length === 4) {\n quadArray[0] = vertices[0]\n quadArray[1] = vertices[1]\n quadArray[2] = vertices[2]\n quadArray[3] = vertices[0]\n quadArray[4] = vertices[2]\n quadArray[5] = vertices[3]\n vertices = quadArray\n }\n\n for (let j = 0, l = vertices.length; j < l; j++) {\n const v = vertices[j]\n const index = offset + j * 3\n positions[index + 0] = v.x\n positions[index + 1] = v.y\n positions[index + 2] = v.z\n }\n\n // create the normals array if this is a set of faces\n if (elementSize === 3) {\n if (!elem.faceNormal) {\n const v0 = vertices[0]\n const v1 = vertices[1]\n const v2 = vertices[2]\n _tempVec0.subVectors(v1, v0)\n _tempVec1.subVectors(v2, v1)\n elem.faceNormal = new Vector3().crossVectors(_tempVec0, _tempVec1).normalize()\n }\n\n let elemNormals = elem.normals\n if (elemNormals.length === 4) {\n quadArray[0] = elemNormals[0]\n quadArray[1] = elemNormals[1]\n quadArray[2] = elemNormals[2]\n quadArray[3] = elemNormals[0]\n quadArray[4] = elemNormals[2]\n quadArray[5] = elemNormals[3]\n elemNormals = quadArray\n }\n\n for (let j = 0, l = elemNormals.length; j < l; j++) {\n // use face normal if a vertex normal is not provided\n let n = elem.faceNormal\n if (elemNormals[j]) {\n n = elemNormals[j].norm\n }\n\n const index = offset + j * 3\n normals[index + 0] = n.x\n normals[index + 1] = n.y\n normals[index + 2] = n.z\n }\n }\n\n if (prevMaterial !== elem.colorCode) {\n if (prevMaterial !== null) {\n bufferGeometry.addGroup(index0, numGroupVerts, materials.length - 1)\n }\n\n const material = elem.material\n if (material !== null) {\n if (elementSize === 3) {\n materials.push(material)\n } else if (elementSize === 2) {\n if (material !== null) {\n if (isConditionalSegments) {\n materials.push(material.userData.edgeMaterial.userData.conditionalEdgeMaterial)\n } else {\n materials.push(material.userData.edgeMaterial)\n }\n } else {\n materials.push(null)\n }\n }\n } else {\n // If a material has not been made available yet then keep the color code string in the material array\n // to save the spot for the material once a parent scopes materials are being applied to the object.\n materials.push(elem.colorCode)\n }\n\n prevMaterial = elem.colorCode\n index0 = offset / 3\n numGroupVerts = vertices.length\n } else {\n numGroupVerts += vertices.length\n }\n\n offset += 3 * vertices.length\n }\n\n if (numGroupVerts > 0) {\n bufferGeometry.addGroup(index0, Infinity, materials.length - 1)\n }\n\n bufferGeometry.setAttribute('position', new BufferAttribute(positions, 3))\n\n if (normals !== null) {\n bufferGeometry.setAttribute('normal', new BufferAttribute(normals, 3))\n }\n\n let object3d = null\n\n if (elementSize === 2) {\n if (isConditionalSegments) {\n object3d = new ConditionalLineSegments(bufferGeometry, materials.length === 1 ? materials[0] : materials)\n } else {\n object3d = new LineSegments(bufferGeometry, materials.length === 1 ? materials[0] : materials)\n }\n } else if (elementSize === 3) {\n object3d = new Mesh(bufferGeometry, materials.length === 1 ? materials[0] : materials)\n }\n\n if (isConditionalSegments) {\n object3d.isConditionalLine = true\n\n const controlArray0 = new Float32Array(elements.length * 3 * 2)\n const controlArray1 = new Float32Array(elements.length * 3 * 2)\n const directionArray = new Float32Array(elements.length * 3 * 2)\n for (let i = 0, l = elements.length; i < l; i++) {\n const os = elements[i]\n const vertices = os.vertices\n const controlPoints = os.controlPoints\n const c0 = controlPoints[0]\n const c1 = controlPoints[1]\n const v0 = vertices[0]\n const v1 = vertices[1]\n const index = i * 3 * 2\n controlArray0[index + 0] = c0.x\n controlArray0[index + 1] = c0.y\n controlArray0[index + 2] = c0.z\n controlArray0[index + 3] = c0.x\n controlArray0[index + 4] = c0.y\n controlArray0[index + 5] = c0.z\n\n controlArray1[index + 0] = c1.x\n controlArray1[index + 1] = c1.y\n controlArray1[index + 2] = c1.z\n controlArray1[index + 3] = c1.x\n controlArray1[index + 4] = c1.y\n controlArray1[index + 5] = c1.z\n\n directionArray[index + 0] = v1.x - v0.x\n directionArray[index + 1] = v1.y - v0.y\n directionArray[index + 2] = v1.z - v0.z\n directionArray[index + 3] = v1.x - v0.x\n directionArray[index + 4] = v1.y - v0.y\n directionArray[index + 5] = v1.z - v0.z\n }\n\n bufferGeometry.setAttribute('control0', new BufferAttribute(controlArray0, 3, false))\n bufferGeometry.setAttribute('control1', new BufferAttribute(controlArray1, 3, false))\n bufferGeometry.setAttribute('direction', new BufferAttribute(directionArray, 3, false))\n }\n\n return object3d\n}\n\n//\n\nclass LDrawLoader extends Loader {\n constructor(manager) {\n super(manager)\n\n // Array of THREE.Material\n this.materials = []\n this.materialLibrary = {}\n\n // This also allows to handle the embedded text files (\"0 FILE\" lines)\n this.partsCache = new LDrawPartsGeometryCache(this)\n\n // 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.\n this.fileMap = {}\n\n // Initializes the materials library with default materials\n this.setMaterials([])\n\n // If this flag is set to true the vertex normals will be smoothed.\n this.smoothNormals = true\n\n // The path to load parts from the LDraw parts library from.\n this.partsLibraryPath = ''\n }\n\n setPartsLibraryPath(path) {\n this.partsLibraryPath = path\n return this\n }\n\n async preloadMaterials(url) {\n const fileLoader = new FileLoader(this.manager)\n fileLoader.setPath(this.path)\n fileLoader.setRequestHeader(this.requestHeader)\n fileLoader.setWithCredentials(this.withCredentials)\n\n const text = await fileLoader.loadAsync(url)\n const colorLineRegex = /^0 !COLOUR/\n const lines = text.split(/[\\n\\r]/g)\n const materials = []\n for (let i = 0, l = lines.length; i < l; i++) {\n const line = lines[i]\n if (colorLineRegex.test(line)) {\n const directive = line.replace(colorLineRegex, '')\n const material = this.parseColorMetaDirective(new LineParser(directive))\n materials.push(material)\n }\n }\n\n this.setMaterials(materials)\n }\n\n load(url, onLoad, onProgress, onError) {\n const fileLoader = new FileLoader(this.manager)\n fileLoader.setPath(this.path)\n fileLoader.setRequestHeader(this.requestHeader)\n fileLoader.setWithCredentials(this.withCredentials)\n fileLoader.load(\n url,\n (text) => {\n this.partsCache\n .parseModel(text, this.materialLibrary)\n .then((group) => {\n this.applyMaterialsToMesh(group, MAIN_COLOUR_CODE, this.materialLibrary, true)\n this.computeConstructionSteps(group)\n onLoad(group)\n })\n .catch(onError)\n },\n onProgress,\n onError,\n )\n }\n\n parse(text, onLoad) {\n this.partsCache.parseModel(text, this.materialLibrary).then((group) => {\n this.computeConstructionSteps(group)\n onLoad(group)\n })\n }\n\n setMaterials(materials) {\n this.materialLibrary = {}\n this.materials = []\n for (let i = 0, l = materials.length; i < l; i++) {\n this.addMaterial(materials[i])\n }\n\n // Add default main triangle and line edge materials (used in pieces that can be colored with a main color)\n this.addMaterial(this.parseColorMetaDirective(new LineParser('Main_Colour CODE 16 VALUE #FF8080 EDGE #333333')))\n this.addMaterial(this.parseColorMetaDirective(new LineParser('Edge_Colour CODE 24 VALUE #A0A0A0 EDGE #333333')))\n\n return this\n }\n\n setFileMap(fileMap) {\n this.fileMap = fileMap\n\n return this