1421 lines
48 KiB
JavaScript
1421 lines
48 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
const THREE = require("three");
|
|
const constants = require("../_polyfill/constants.cjs");
|
|
const FINISH_TYPE_DEFAULT = 0;
|
|
const FINISH_TYPE_CHROME = 1;
|
|
const FINISH_TYPE_PEARLESCENT = 2;
|
|
const FINISH_TYPE_RUBBER = 3;
|
|
const FINISH_TYPE_MATTE_METALLIC = 4;
|
|
const FINISH_TYPE_METAL = 5;
|
|
const FILE_LOCATION_AS_IS = 0;
|
|
const FILE_LOCATION_TRY_PARTS = 1;
|
|
const FILE_LOCATION_TRY_P = 2;
|
|
const FILE_LOCATION_TRY_MODELS = 3;
|
|
const FILE_LOCATION_TRY_RELATIVE = 4;
|
|
const FILE_LOCATION_TRY_ABSOLUTE = 5;
|
|
const FILE_LOCATION_NOT_FOUND = 6;
|
|
const MAIN_COLOUR_CODE = "16";
|
|
const MAIN_EDGE_COLOUR_CODE = "24";
|
|
const _tempVec0 = /* @__PURE__ */ new THREE.Vector3();
|
|
const _tempVec1 = /* @__PURE__ */ new THREE.Vector3();
|
|
class LDrawConditionalLineMaterial extends THREE.ShaderMaterial {
|
|
constructor(parameters) {
|
|
super({
|
|
uniforms: THREE.UniformsUtils.merge([
|
|
THREE.UniformsLib.fog,
|
|
{
|
|
diffuse: {
|
|
value: new THREE.Color()
|
|
},
|
|
opacity: {
|
|
value: 1
|
|
}
|
|
}
|
|
]),
|
|
vertexShader: (
|
|
/* glsl */
|
|
`
|
|
attribute vec3 control0;
|
|
attribute vec3 control1;
|
|
attribute vec3 direction;
|
|
varying float discardFlag;
|
|
|
|
#include <common>
|
|
#include <color_pars_vertex>
|
|
#include <fog_pars_vertex>
|
|
#include <logdepthbuf_pars_vertex>
|
|
#include <clipping_planes_pars_vertex>
|
|
|
|
void main() {
|
|
#include <color_vertex>
|
|
|
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
gl_Position = projectionMatrix * mvPosition;
|
|
|
|
// Transform the line segment ends and control points into camera clip space
|
|
vec4 c0 = projectionMatrix * modelViewMatrix * vec4(control0, 1.0);
|
|
vec4 c1 = projectionMatrix * modelViewMatrix * vec4(control1, 1.0);
|
|
vec4 p0 = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
vec4 p1 = projectionMatrix * modelViewMatrix * vec4(position + direction, 1.0);
|
|
|
|
c0.xy /= c0.w;
|
|
c1.xy /= c1.w;
|
|
p0.xy /= p0.w;
|
|
p1.xy /= p1.w;
|
|
|
|
// Get the direction of the segment and an orthogonal vector
|
|
vec2 dir = p1.xy - p0.xy;
|
|
vec2 norm = vec2(-dir.y, dir.x);
|
|
|
|
// Get control point directions from the line
|
|
vec2 c0dir = c0.xy - p1.xy;
|
|
vec2 c1dir = c1.xy - p1.xy;
|
|
|
|
// If the vectors to the controls points are pointed in different directions away
|
|
// from the line segment then the line should not be drawn.
|
|
float d0 = dot(normalize(norm), normalize(c0dir));
|
|
float d1 = dot(normalize(norm), normalize(c1dir));
|
|
discardFlag = float(sign(d0) != sign(d1));
|
|
|
|
#include <logdepthbuf_vertex>
|
|
#include <clipping_planes_vertex>
|
|
#include <fog_vertex>
|
|
}
|
|
`
|
|
),
|
|
fragmentShader: (
|
|
/* glsl */
|
|
`
|
|
uniform vec3 diffuse;
|
|
uniform float opacity;
|
|
varying float discardFlag;
|
|
|
|
#include <common>
|
|
#include <color_pars_fragment>
|
|
#include <fog_pars_fragment>
|
|
#include <logdepthbuf_pars_fragment>
|
|
#include <clipping_planes_pars_fragment>
|
|
|
|
void main() {
|
|
if (discardFlag > 0.5) discard;
|
|
|
|
#include <clipping_planes_fragment>
|
|
vec3 outgoingLight = vec3(0.0);
|
|
vec4 diffuseColor = vec4(diffuse, opacity);
|
|
#include <logdepthbuf_fragment>
|
|
#include <color_fragment>
|
|
outgoingLight = diffuseColor.rgb; // simple shader
|
|
gl_FragColor = vec4(outgoingLight, diffuseColor.a);
|
|
#include <tonemapping_fragment>
|
|
#include <${constants.version >= 154 ? "colorspace_fragment" : "encodings_fragment"}>
|
|
#include <fog_fragment>
|
|
#include <premultiplied_alpha_fragment>
|
|
}
|
|
`
|
|
)
|
|
});
|
|
Object.defineProperties(this, {
|
|
opacity: {
|
|
get: function() {
|
|
return this.uniforms.opacity.value;
|
|
},
|
|
set: function(value) {
|
|
this.uniforms.opacity.value = value;
|
|
}
|
|
},
|
|
color: {
|
|
get: function() {
|
|
return this.uniforms.diffuse.value;
|
|
}
|
|
}
|
|
});
|
|
this.setValues(parameters);
|
|
this.isLDrawConditionalLineMaterial = true;
|
|
}
|
|
}
|
|
class ConditionalLineSegments extends THREE.LineSegments {
|
|
constructor(geometry, material) {
|
|
super(geometry, material);
|
|
this.isConditionalLine = true;
|
|
}
|
|
}
|
|
function generateFaceNormals(faces) {
|
|
for (let i = 0, l = faces.length; i < l; i++) {
|
|
const face = faces[i];
|
|
const vertices = face.vertices;
|
|
const v0 = vertices[0];
|
|
const v1 = vertices[1];
|
|
const v2 = vertices[2];
|
|
_tempVec0.subVectors(v1, v0);
|
|
_tempVec1.subVectors(v2, v1);
|
|
face.faceNormal = new THREE.Vector3().crossVectors(_tempVec0, _tempVec1).normalize();
|
|
}
|
|
}
|
|
const _ray = /* @__PURE__ */ new THREE.Ray();
|
|
function smoothNormals(faces, lineSegments, checkSubSegments = false) {
|
|
const hashMultiplier = (1 + 1e-10) * 100;
|
|
function hashVertex(v) {
|
|
const x = ~~(v.x * hashMultiplier);
|
|
const y = ~~(v.y * hashMultiplier);
|
|
const z = ~~(v.z * hashMultiplier);
|
|
return `${x},${y},${z}`;
|
|
}
|
|
function hashEdge(v0, v1) {
|
|
return `${hashVertex(v0)}_${hashVertex(v1)}`;
|
|
}
|
|
function toNormalizedRay(v0, v1, targetRay) {
|
|
targetRay.direction.subVectors(v1, v0).normalize();
|
|
const scalar = v0.dot(targetRay.direction);
|
|
targetRay.origin.copy(v0).addScaledVector(targetRay.direction, -scalar);
|
|
return targetRay;
|
|
}
|
|
function hashRay(ray) {
|
|
return hashEdge(ray.origin, ray.direction);
|
|
}
|
|
const hardEdges = /* @__PURE__ */ new Set();
|
|
const hardEdgeRays = /* @__PURE__ */ new Map();
|
|
const halfEdgeList = {};
|
|
const normals = [];
|
|
for (let i = 0, l = lineSegments.length; i < l; i++) {
|
|
const ls = lineSegments[i];
|
|
const vertices = ls.vertices;
|
|
const v0 = vertices[0];
|
|
const v1 = vertices[1];
|
|
hardEdges.add(hashEdge(v0, v1));
|
|
hardEdges.add(hashEdge(v1, v0));
|
|
if (checkSubSegments) {
|
|
const ray = toNormalizedRay(v0, v1, new THREE.Ray());
|
|
const rh1 = hashRay(ray);
|
|
if (!hardEdgeRays.has(rh1)) {
|
|
toNormalizedRay(v1, v0, ray);
|
|
const rh2 = hashRay(ray);
|
|
const info2 = {
|
|
ray,
|
|
distances: []
|
|
};
|
|
hardEdgeRays.set(rh1, info2);
|
|
hardEdgeRays.set(rh2, info2);
|
|
}
|
|
const info = hardEdgeRays.get(rh1);
|
|
let d0 = info.ray.direction.dot(v0);
|
|
let d1 = info.ray.direction.dot(v1);
|
|
if (d0 > d1) {
|
|
[d0, d1] = [d1, d0];
|
|
}
|
|
info.distances.push(d0, d1);
|
|
}
|
|
}
|
|
for (let i = 0, l = faces.length; i < l; i++) {
|
|
const tri = faces[i];
|
|
const vertices = tri.vertices;
|
|
const vertCount = vertices.length;
|
|
for (let i2 = 0; i2 < vertCount; i2++) {
|
|
const index = i2;
|
|
const next = (i2 + 1) % vertCount;
|
|
const v0 = vertices[index];
|
|
const v1 = vertices[next];
|
|
const hash = hashEdge(v0, v1);
|
|
if (hardEdges.has(hash)) {
|
|
continue;
|
|
}
|
|
if (checkSubSegments) {
|
|
toNormalizedRay(v0, v1, _ray);
|
|
const rayHash = hashRay(_ray);
|
|
if (hardEdgeRays.has(rayHash)) {
|
|
const info2 = hardEdgeRays.get(rayHash);
|
|
const { ray, distances } = info2;
|
|
let d0 = ray.direction.dot(v0);
|
|
let d1 = ray.direction.dot(v1);
|
|
if (d0 > d1) {
|
|
[d0, d1] = [d1, d0];
|
|
}
|
|
let found = false;
|
|
for (let i3 = 0, l2 = distances.length; i3 < l2; i3 += 2) {
|
|
if (d0 >= distances[i3] && d1 <= distances[i3 + 1]) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (found) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
const info = {
|
|
index,
|
|
tri
|
|
};
|
|
halfEdgeList[hash] = info;
|
|
}
|
|
}
|
|
while (true) {
|
|
let halfEdge = null;
|
|
for (const key in halfEdgeList) {
|
|
halfEdge = halfEdgeList[key];
|
|
break;
|
|
}
|
|
if (halfEdge === null) {
|
|
break;
|
|
}
|
|
const queue = [halfEdge];
|
|
while (queue.length > 0) {
|
|
const tri = queue.pop().tri;
|
|
const vertices = tri.vertices;
|
|
const vertNormals = tri.normals;
|
|
const faceNormal = tri.faceNormal;
|
|
const vertCount = vertices.length;
|
|
for (let i2 = 0; i2 < vertCount; i2++) {
|
|
const index = i2;
|
|
const next = (i2 + 1) % vertCount;
|
|
const v0 = vertices[index];
|
|
const v1 = vertices[next];
|
|
const hash = hashEdge(v0, v1);
|
|
delete halfEdgeList[hash];
|
|
const reverseHash = hashEdge(v1, v0);
|
|
const otherInfo = halfEdgeList[reverseHash];
|
|
if (otherInfo) {
|
|
const otherTri = otherInfo.tri;
|
|
const otherIndex = otherInfo.index;
|
|
const otherNormals = otherTri.normals;
|
|
const otherVertCount = otherNormals.length;
|
|
const otherFaceNormal = otherTri.faceNormal;
|
|
if (Math.abs(otherTri.faceNormal.dot(tri.faceNormal)) < 0.25) {
|
|
continue;
|
|
}
|
|
if (reverseHash in halfEdgeList) {
|
|
queue.push(otherInfo);
|
|
delete halfEdgeList[reverseHash];
|
|
}
|
|
const otherNext = (otherIndex + 1) % otherVertCount;
|
|
if (vertNormals[index] && otherNormals[otherNext] && vertNormals[index] !== otherNormals[otherNext]) {
|
|
otherNormals[otherNext].norm.add(vertNormals[index].norm);
|
|
vertNormals[index].norm = otherNormals[otherNext].norm;
|
|
}
|
|
let sharedNormal1 = vertNormals[index] || otherNormals[otherNext];
|
|
if (sharedNormal1 === null) {
|
|
sharedNormal1 = { norm: new THREE.Vector3() };
|
|
normals.push(sharedNormal1.norm);
|
|
}
|
|
if (vertNormals[index] === null) {
|
|
vertNormals[index] = sharedNormal1;
|
|
sharedNormal1.norm.add(faceNormal);
|
|
}
|
|
if (otherNormals[otherNext] === null) {
|
|
otherNormals[otherNext] = sharedNormal1;
|
|
sharedNormal1.norm.add(otherFaceNormal);
|
|
}
|
|
if (vertNormals[next] && otherNormals[otherIndex] && vertNormals[next] !== otherNormals[otherIndex]) {
|
|
otherNormals[otherIndex].norm.add(vertNormals[next].norm);
|
|
vertNormals[next].norm = otherNormals[otherIndex].norm;
|
|
}
|
|
let sharedNormal2 = vertNormals[next] || otherNormals[otherIndex];
|
|
if (sharedNormal2 === null) {
|
|
sharedNormal2 = { norm: new THREE.Vector3() };
|
|
normals.push(sharedNormal2.norm);
|
|
}
|
|
if (vertNormals[next] === null) {
|
|
vertNormals[next] = sharedNormal2;
|
|
sharedNormal2.norm.add(faceNormal);
|
|
}
|
|
if (otherNormals[otherIndex] === null) {
|
|
otherNormals[otherIndex] = sharedNormal2;
|
|
sharedNormal2.norm.add(otherFaceNormal);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (let i = 0, l = normals.length; i < l; i++) {
|
|
normals[i].normalize();
|
|
}
|
|
}
|
|
function isPartType(type) {
|
|
return type === "Part" || type === "Unofficial_Part";
|
|
}
|
|
function isPrimitiveType(type) {
|
|
return /primitive/i.test(type) || type === "Subpart";
|
|
}
|
|
class LineParser {
|
|
constructor(line, lineNumber) {
|
|
this.line = line;
|
|
this.lineLength = line.length;
|
|
this.currentCharIndex = 0;
|
|
this.currentChar = " ";
|
|
this.lineNumber = lineNumber;
|
|
}
|
|
seekNonSpace() {
|
|
while (this.currentCharIndex < this.lineLength) {
|
|
this.currentChar = this.line.charAt(this.currentCharIndex);
|
|
if (this.currentChar !== " " && this.currentChar !== " ") {
|
|
return;
|
|
}
|
|
this.currentCharIndex++;
|
|
}
|
|
}
|
|
getToken() {
|
|
const pos0 = this.currentCharIndex++;
|
|
while (this.currentCharIndex < this.lineLength) {
|
|
this.currentChar = this.line.charAt(this.currentCharIndex);
|
|
if (this.currentChar === " " || this.currentChar === " ") {
|
|
break;
|
|
}
|
|
this.currentCharIndex++;
|
|
}
|
|
const pos1 = this.currentCharIndex;
|
|
this.seekNonSpace();
|
|
return this.line.substring(pos0, pos1);
|
|
}
|
|
getVector() {
|
|
return new THREE.Vector3(parseFloat(this.getToken()), parseFloat(this.getToken()), parseFloat(this.getToken()));
|
|
}
|
|
getRemainingString() {
|
|
return this.line.substring(this.currentCharIndex, this.lineLength);
|
|
}
|
|
isAtTheEnd() {
|
|
return this.currentCharIndex >= this.lineLength;
|
|
}
|
|
setToEnd() {
|
|
this.currentCharIndex = this.lineLength;
|
|
}
|
|
getLineNumberString() {
|
|
return this.lineNumber >= 0 ? " at line " + this.lineNumber : "";
|
|
}
|
|
}
|
|
class LDrawParsedCache {
|
|
constructor(loader) {
|
|
this.loader = loader;
|
|
this._cache = {};
|
|
}
|
|
cloneResult(original) {
|
|
const result = {};
|
|
result.faces = original.faces.map((face) => {
|
|
return {
|
|
colorCode: face.colorCode,
|
|
material: face.material,
|
|
vertices: face.vertices.map((v) => v.clone()),
|
|
normals: face.normals.map(() => null),
|
|
faceNormal: null
|
|
};
|
|
});
|
|
result.conditionalSegments = original.conditionalSegments.map((face) => {
|
|
return {
|
|
colorCode: face.colorCode,
|
|
material: face.material,
|
|
vertices: face.vertices.map((v) => v.clone()),
|
|
controlPoints: face.controlPoints.map((v) => v.clone())
|
|
};
|
|
});
|
|
result.lineSegments = original.lineSegments.map((face) => {
|
|
return {
|
|
colorCode: face.colorCode,
|
|
material: face.material,
|
|
vertices: face.vertices.map((v) => v.clone())
|
|
};
|
|
});
|
|
result.type = original.type;
|
|
result.category = original.category;
|
|
result.keywords = original.keywords;
|
|
result.subobjects = original.subobjects;
|
|
result.totalFaces = original.totalFaces;
|
|
result.startingConstructionStep = original.startingConstructionStep;
|
|
result.materials = original.materials;
|
|
result.group = null;
|
|
return result;
|
|
}
|
|
async fetchData(fileName) {
|
|
let triedLowerCase = false;
|
|
let locationState = FILE_LOCATION_AS_IS;
|
|
while (locationState !== FILE_LOCATION_NOT_FOUND) {
|
|
let subobjectURL = fileName;
|
|
switch (locationState) {
|
|
case FILE_LOCATION_AS_IS:
|
|
locationState = locationState + 1;
|
|
break;
|
|
case FILE_LOCATION_TRY_PARTS:
|
|
subobjectURL = "parts/" + subobjectURL;
|
|
locationState = locationState + 1;
|
|
break;
|
|
case FILE_LOCATION_TRY_P:
|
|
subobjectURL = "p/" + subobjectURL;
|
|
locationState = locationState + 1;
|
|
break;
|
|
case FILE_LOCATION_TRY_MODELS:
|
|
subobjectURL = "models/" + subobjectURL;
|
|
locationState = locationState + 1;
|
|
break;
|
|
case FILE_LOCATION_TRY_RELATIVE:
|
|
subobjectURL = fileName.substring(0, fileName.lastIndexOf("/") + 1) + subobjectURL;
|
|
locationState = locationState + 1;
|
|
break;
|
|
case FILE_LOCATION_TRY_ABSOLUTE:
|
|
if (triedLowerCase) {
|
|
locationState = FILE_LOCATION_NOT_FOUND;
|
|
} else {
|
|
fileName = fileName.toLowerCase();
|
|
subobjectURL = fileName;
|
|
triedLowerCase = true;
|
|
locationState = FILE_LOCATION_AS_IS;
|
|
}
|
|
break;
|
|
}
|
|
const loader = this.loader;
|
|
const fileLoader = new THREE.FileLoader(loader.manager);
|
|
fileLoader.setPath(loader.partsLibraryPath);
|
|
fileLoader.setRequestHeader(loader.requestHeader);
|
|
fileLoader.setWithCredentials(loader.withCredentials);
|
|
try {
|
|
const text = await fileLoader.loadAsync(subobjectURL);
|
|
return text;
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
}
|
|
throw new Error('LDrawLoader: Subobject "' + fileName + '" could not be loaded.');
|
|
}
|
|
parse(text, fileName = null) {
|
|
const loader = this.loader;
|
|
const faces = [];
|
|
const lineSegments = [];
|
|
const conditionalSegments = [];
|
|
const subobjects = [];
|
|
const materials = {};
|
|
const getLocalMaterial = (colorCode) => {
|
|
return materials[colorCode] || null;
|
|
};
|
|
let type = "Model";
|
|
let category = null;
|
|
let keywords = null;
|
|
let totalFaces = 0;
|
|
if (text.indexOf("\r\n") !== -1) {
|
|
text = text.replace(/\r\n/g, "\n");
|
|
}
|
|
const lines = text.split("\n");
|
|
const numLines = lines.length;
|
|
let parsingEmbeddedFiles = false;
|
|
let currentEmbeddedFileName = null;
|
|
let currentEmbeddedText = null;
|
|
let bfcCertified = false;
|
|
let bfcCCW = true;
|
|
let bfcInverted = false;
|
|
let bfcCull = true;
|
|
let startingConstructionStep = false;
|
|
for (let lineIndex = 0; lineIndex < numLines; lineIndex++) {
|
|
const line = lines[lineIndex];
|
|
if (line.length === 0)
|
|
continue;
|
|
if (parsingEmbeddedFiles) {
|
|
if (line.startsWith("0 FILE ")) {
|
|
this.setData(currentEmbeddedFileName, currentEmbeddedText);
|
|
currentEmbeddedFileName = line.substring(7);
|
|
currentEmbeddedText = "";
|
|
} else {
|
|
currentEmbeddedText += line + "\n";
|
|
}
|
|
continue;
|
|
}
|
|
const lp = new LineParser(line, lineIndex + 1);
|
|
lp.seekNonSpace();
|
|
if (lp.isAtTheEnd()) {
|
|
continue;
|
|
}
|
|
const lineType = lp.getToken();
|
|
let material;
|
|
let colorCode;
|
|
let segment;
|
|
let ccw;
|
|
let doubleSided;
|
|
let v0, v1, v2, v3, c0, c1;
|
|
switch (lineType) {
|
|
case "0":
|
|
const meta = lp.getToken();
|
|
if (meta) {
|
|
switch (meta) {
|
|
case "!LDRAW_ORG":
|
|
type = lp.getToken();
|
|
break;
|
|
case "!COLOUR":
|
|
material = loader.parseColorMetaDirective(lp);
|
|
if (material) {
|
|
materials[material.userData.code] = material;
|
|
} else {
|
|
console.warn("LDrawLoader: Error parsing material" + lp.getLineNumberString());
|
|
}
|
|
break;
|
|
case "!CATEGORY":
|
|
category = lp.getToken();
|
|
break;
|
|
case "!KEYWORDS":
|
|
const newKeywords = lp.getRemainingString().split(",");
|
|
if (newKeywords.length > 0) {
|
|
if (!keywords) {
|
|
keywords = [];
|
|
}
|
|
newKeywords.forEach(function(keyword) {
|
|
keywords.push(keyword.trim());
|
|
});
|
|
}
|
|
break;
|
|
case "FILE":
|
|
if (lineIndex > 0) {
|
|
parsingEmbeddedFiles = true;
|
|
currentEmbeddedFileName = lp.getRemainingString();
|
|
currentEmbeddedText = "";
|
|
bfcCertified = false;
|
|
bfcCCW = true;
|
|
}
|
|
break;
|
|
case "BFC":
|
|
while (!lp.isAtTheEnd()) {
|
|
const token = lp.getToken();
|
|
switch (token) {
|
|
case "CERTIFY":
|
|
case "NOCERTIFY":
|
|
bfcCertified = token === "CERTIFY";
|
|
bfcCCW = true;
|
|
break;
|
|
case "CW":
|
|
case "CCW":
|
|
bfcCCW = token === "CCW";
|
|
break;
|
|
case "INVERTNEXT":
|
|
bfcInverted = true;
|
|
break;
|
|
case "CLIP":
|
|
case "NOCLIP":
|
|
bfcCull = token === "CLIP";
|
|
break;
|
|
default:
|
|
console.warn('THREE.LDrawLoader: BFC directive "' + token + '" is unknown.');
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case "STEP":
|
|
startingConstructionStep = true;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case "1":
|
|
colorCode = lp.getToken();
|
|
material = getLocalMaterial(colorCode);
|
|
const posX = parseFloat(lp.getToken());
|
|
const posY = parseFloat(lp.getToken());
|
|
const posZ = parseFloat(lp.getToken());
|
|
const m0 = parseFloat(lp.getToken());
|
|
const m1 = parseFloat(lp.getToken());
|
|
const m2 = parseFloat(lp.getToken());
|
|
const m3 = parseFloat(lp.getToken());
|
|
const m4 = parseFloat(lp.getToken());
|
|
const m5 = parseFloat(lp.getToken());
|
|
const m6 = parseFloat(lp.getToken());
|
|
const m7 = parseFloat(lp.getToken());
|
|
const m8 = parseFloat(lp.getToken());
|
|
const matrix = new THREE.Matrix4().set(m0, m1, m2, posX, m3, m4, m5, posY, m6, m7, m8, posZ, 0, 0, 0, 1);
|
|
let fileName2 = lp.getRemainingString().trim().replace(/\\/g, "/");
|
|
if (loader.fileMap[fileName2]) {
|
|
fileName2 = loader.fileMap[fileName2];
|
|
} else {
|
|
if (fileName2.startsWith("s/")) {
|
|
fileName2 = "parts/" + fileName2;
|
|
} else if (fileName2.startsWith("48/")) {
|
|
fileName2 = "p/" + fileName2;
|
|
}
|
|
}
|
|
subobjects.push({
|
|
material,
|
|
colorCode,
|
|
matrix,
|
|
fileName: fileName2,
|
|
inverted: bfcInverted,
|
|
startingConstructionStep
|
|
});
|
|
bfcInverted = false;
|
|
break;
|
|
case "2":
|
|
colorCode = lp.getToken();
|
|
material = getLocalMaterial(colorCode);
|
|
v0 = lp.getVector();
|
|
v1 = lp.getVector();
|
|
segment = {
|
|
material,
|
|
colorCode,
|
|
vertices: [v0, v1]
|
|
};
|
|
lineSegments.push(segment);
|
|
break;
|
|
case "5":
|
|
colorCode = lp.getToken();
|
|
material = getLocalMaterial(colorCode);
|
|
v0 = lp.getVector();
|
|
v1 = lp.getVector();
|
|
c0 = lp.getVector();
|
|
c1 = lp.getVector();
|
|
segment = {
|
|
material,
|
|
colorCode,
|
|
vertices: [v0, v1],
|
|
controlPoints: [c0, c1]
|
|
};
|
|
conditionalSegments.push(segment);
|
|
break;
|
|
case "3":
|
|
colorCode = lp.getToken();
|
|
material = getLocalMaterial(colorCode);
|
|
ccw = bfcCCW;
|
|
doubleSided = !bfcCertified || !bfcCull;
|
|
if (ccw === true) {
|
|
v0 = lp.getVector();
|
|
v1 = lp.getVector();
|
|
v2 = lp.getVector();
|
|
} else {
|
|
v2 = lp.getVector();
|
|
v1 = lp.getVector();
|
|
v0 = lp.getVector();
|
|
}
|
|
faces.push({
|
|
material,
|
|
colorCode,
|
|
faceNormal: null,
|
|
vertices: [v0, v1, v2],
|
|
normals: [null, null, null]
|
|
});
|
|
totalFaces++;
|
|
if (doubleSided === true) {
|
|
faces.push({
|
|
material,
|
|
colorCode,
|
|
faceNormal: null,
|
|
vertices: [v2, v1, v0],
|
|
normals: [null, null, null]
|
|
});
|
|
totalFaces++;
|
|
}
|
|
break;
|
|
case "4":
|
|
colorCode = lp.getToken();
|
|
material = getLocalMaterial(colorCode);
|
|
ccw = bfcCCW;
|
|
doubleSided = !bfcCertified || !bfcCull;
|
|
if (ccw === true) {
|
|
v0 = lp.getVector();
|
|
v1 = lp.getVector();
|
|
v2 = lp.getVector();
|
|
v3 = lp.getVector();
|
|
} else {
|
|
v3 = lp.getVector();
|
|
v2 = lp.getVector();
|
|
v1 = lp.getVector();
|
|
v0 = lp.getVector();
|
|
}
|
|
faces.push({
|
|
material,
|
|
colorCode,
|
|
faceNormal: null,
|
|
vertices: [v0, v1, v2, v3],
|
|
normals: [null, null, null, null]
|
|
});
|
|
totalFaces += 2;
|
|
if (doubleSided === true) {
|
|
faces.push({
|
|
material,
|
|
colorCode,
|
|
faceNormal: null,
|
|
vertices: [v3, v2, v1, v0],
|
|
normals: [null, null, null, null]
|
|
});
|
|
totalFaces += 2;
|
|
}
|
|
break;
|
|
default:
|
|
throw new Error('LDrawLoader: Unknown line type "' + lineType + '"' + lp.getLineNumberString() + ".");
|
|
}
|
|
}
|
|
if (parsingEmbeddedFiles) {
|
|
this.setData(currentEmbeddedFileName, currentEmbeddedText);
|
|
}
|
|
return {
|
|
faces,
|
|
conditionalSegments,
|
|
lineSegments,
|
|
type,
|
|
category,
|
|
keywords,
|
|
subobjects,
|
|
totalFaces,
|
|
startingConstructionStep,
|
|
materials,
|
|
fileName,
|
|
group: null
|
|
};
|
|
}
|
|
// returns an (optionally cloned) instance of the data
|
|
getData(fileName, clone = true) {
|
|
const key = fileName.toLowerCase();
|
|
const result = this._cache[key];
|
|
if (result === null || result instanceof Promise) {
|
|
return null;
|
|
}
|
|
if (clone) {
|
|
return this.cloneResult(result);
|
|
} else {
|
|
return result;
|
|
}
|
|
}
|
|
// kicks off a fetch and parse of the requested data if it hasn't already been loaded. Returns when
|
|
// the data is ready to use and can be retrieved synchronously with "getData".
|
|
async ensureDataLoaded(fileName) {
|
|
const key = fileName.toLowerCase();
|
|
if (!(key in this._cache)) {
|
|
this._cache[key] = this.fetchData(fileName).then((text) => {
|
|
const info = this.parse(text, fileName);
|
|
this._cache[key] = info;
|
|
return info;
|
|
});
|
|
}
|
|
await this._cache[key];
|
|
}
|
|
// sets the data in the cache from parsed data
|
|
setData(fileName, text) {
|
|
const key = fileName.toLowerCase();
|
|
this._cache[key] = this.parse(text, fileName);
|
|
}
|
|
}
|
|
function getMaterialFromCode(colorCode, parentColorCode, materialHierarchy, forEdge) {
|
|
const isPassthrough = !forEdge && colorCode === MAIN_COLOUR_CODE || forEdge && colorCode === MAIN_EDGE_COLOUR_CODE;
|
|
if (isPassthrough) {
|
|
colorCode = parentColorCode;
|
|
}
|
|
return materialHierarchy[colorCode] || null;
|
|
}
|
|
class LDrawPartsGeometryCache {
|
|
constructor(loader) {
|
|
this.loader = loader;
|
|
this.parseCache = new LDrawParsedCache(loader);
|
|
this._cache = {};
|
|
}
|
|
// Convert the given file information into a mesh by processing subobjects.
|
|
async processIntoMesh(info) {
|
|
const loader = this.loader;
|
|
const parseCache = this.parseCache;
|
|
const faceMaterials = /* @__PURE__ */ new Set();
|
|
const processInfoSubobjects = async (info2, subobject = null) => {
|
|
const subobjects = info2.subobjects;
|
|
const promises = [];
|
|
for (let i = 0, l = subobjects.length; i < l; i++) {
|
|
const subobject2 = subobjects[i];
|
|
const promise = parseCache.ensureDataLoaded(subobject2.fileName).then(() => {
|
|
const subobjectInfo = parseCache.getData(subobject2.fileName, false);
|
|
if (!isPrimitiveType(subobjectInfo.type)) {
|
|
return this.loadModel(subobject2.fileName).catch((error) => {
|
|
console.warn(error);
|
|
return null;
|
|
});
|
|
}
|
|
return processInfoSubobjects(parseCache.getData(subobject2.fileName), subobject2);
|
|
});
|
|
promises.push(promise);
|
|
}
|
|
const group2 = new THREE.Group();
|
|
group2.userData.category = info2.category;
|
|
group2.userData.keywords = info2.keywords;
|
|
info2.group = group2;
|
|
const subobjectInfos = await Promise.all(promises);
|
|
for (let i = 0, l = subobjectInfos.length; i < l; i++) {
|
|
const subobject2 = info2.subobjects[i];
|
|
const subobjectInfo = subobjectInfos[i];
|
|
if (subobjectInfo === null) {
|
|
continue;
|
|
}
|
|
if (subobjectInfo.isGroup) {
|
|
const subobjectGroup = subobjectInfo;
|
|
subobject2.matrix.decompose(subobjectGroup.position, subobjectGroup.quaternion, subobjectGroup.scale);
|
|
subobjectGroup.userData.startingConstructionStep = subobject2.startingConstructionStep;
|
|
subobjectGroup.name = subobject2.fileName;
|
|
loader.applyMaterialsToMesh(subobjectGroup, subobject2.colorCode, info2.materials);
|
|
group2.add(subobjectGroup);
|
|
continue;
|
|
}
|
|
if (subobjectInfo.group.children.length) {
|
|
group2.add(subobjectInfo.group);
|
|
}
|
|
const parentLineSegments = info2.lineSegments;
|
|
const parentConditionalSegments = info2.conditionalSegments;
|
|
const parentFaces = info2.faces;
|
|
const lineSegments = subobjectInfo.lineSegments;
|
|
const conditionalSegments = subobjectInfo.conditionalSegments;
|
|
const faces = subobjectInfo.faces;
|
|
const matrix = subobject2.matrix;
|
|
const inverted = subobject2.inverted;
|
|
const matrixScaleInverted = matrix.determinant() < 0;
|
|
const colorCode = subobject2.colorCode;
|
|
const lineColorCode = colorCode === MAIN_COLOUR_CODE ? MAIN_EDGE_COLOUR_CODE : colorCode;
|
|
for (let i2 = 0, l2 = lineSegments.length; i2 < l2; i2++) {
|
|
const ls = lineSegments[i2];
|
|
const vertices = ls.vertices;
|
|
vertices[0].applyMatrix4(matrix);
|
|
vertices[1].applyMatrix4(matrix);
|
|
ls.colorCode = ls.colorCode === MAIN_EDGE_COLOUR_CODE ? lineColorCode : ls.colorCode;
|
|
ls.material = ls.material || getMaterialFromCode(ls.colorCode, ls.colorCode, info2.materials, true);
|
|
parentLineSegments.push(ls);
|
|
}
|
|
for (let i2 = 0, l2 = conditionalSegments.length; i2 < l2; i2++) {
|
|
const os = conditionalSegments[i2];
|
|
const vertices = os.vertices;
|
|
const controlPoints = os.controlPoints;
|
|
vertices[0].applyMatrix4(matrix);
|
|
vertices[1].applyMatrix4(matrix);
|
|
controlPoints[0].applyMatrix4(matrix);
|
|
controlPoints[1].applyMatrix4(matrix);
|
|
os.colorCode = os.colorCode === MAIN_EDGE_COLOUR_CODE ? lineColorCode : os.colorCode;
|
|
os.material = os.material || getMaterialFromCode(os.colorCode, os.colorCode, info2.materials, true);
|
|
parentConditionalSegments.push(os);
|
|
}
|
|
for (let i2 = 0, l2 = faces.length; i2 < l2; i2++) {
|
|
const tri = faces[i2];
|
|
const vertices = tri.vertices;
|
|
for (let i3 = 0, l3 = vertices.length; i3 < l3; i3++) {
|
|
vertices[i3].applyMatrix4(matrix);
|
|
}
|
|
tri.colorCode = tri.colorCode === MAIN_COLOUR_CODE ? colorCode : tri.colorCode;
|
|
tri.material = tri.material || getMaterialFromCode(tri.colorCode, colorCode, info2.materials, false);
|
|
faceMaterials.add(tri.colorCode);
|
|
if (matrixScaleInverted !== inverted) {
|
|
vertices.reverse();
|
|
}
|
|
parentFaces.push(tri);
|
|
}
|
|
info2.totalFaces += subobjectInfo.totalFaces;
|
|
}
|
|
if (subobject) {
|
|
loader.applyMaterialsToMesh(group2, subobject.colorCode, info2.materials);
|
|
}
|
|
return info2;
|
|
};
|
|
for (let i = 0, l = info.faces; i < l; i++) {
|
|
faceMaterials.add(info.faces[i].colorCode);
|
|
}
|
|
await processInfoSubobjects(info);
|
|
if (loader.smoothNormals) {
|
|
const checkSubSegments = faceMaterials.size > 1;
|
|
generateFaceNormals(info.faces);
|
|
smoothNormals(info.faces, info.lineSegments, checkSubSegments);
|
|
}
|
|
const group = info.group;
|
|
if (info.faces.length > 0) {
|
|
group.add(createObject(info.faces, 3, false, info.totalFaces));
|
|
}
|
|
if (info.lineSegments.length > 0) {
|
|
group.add(createObject(info.lineSegments, 2));
|
|
}
|
|
if (info.conditionalSegments.length > 0) {
|
|
group.add(createObject(info.conditionalSegments, 2, true));
|
|
}
|
|
return group;
|
|
}
|
|
hasCachedModel(fileName) {
|
|
return fileName !== null && fileName.toLowerCase() in this._cache;
|
|
}
|
|
async getCachedModel(fileName) {
|
|
if (fileName !== null && this.hasCachedModel(fileName)) {
|
|
const key = fileName.toLowerCase();
|
|
const group = await this._cache[key];
|
|
return group.clone();
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
// Loads and parses the model with the given file name. Returns a cached copy if available.
|
|
async loadModel(fileName) {
|
|
const parseCache = this.parseCache;
|
|
const key = fileName.toLowerCase();
|
|
if (this.hasCachedModel(fileName)) {
|
|
return this.getCachedModel(fileName);
|
|
} else {
|
|
await parseCache.ensureDataLoaded(fileName);
|
|
const info = parseCache.getData(fileName);
|
|
const promise = this.processIntoMesh(info);
|
|
if (this.hasCachedModel(fileName)) {
|
|
return this.getCachedModel(fileName);
|
|
}
|
|
if (isPartType(info.type)) {
|
|
this._cache[key] = promise;
|
|
}
|
|
const group = await promise;
|
|
return group.clone();
|
|
}
|
|
}
|
|
// parses the given model text into a renderable object. Returns cached copy if available.
|
|
async parseModel(text) {
|
|
const parseCache = this.parseCache;
|
|
const info = parseCache.parse(text);
|
|
if (isPartType(info.type) && this.hasCachedModel(info.fileName)) {
|
|
return this.getCachedModel(info.fileName);
|
|
}
|
|
return this.processIntoMesh(info);
|
|
}
|
|
}
|
|
function sortByMaterial(a, b) {
|
|
if (a.colorCode === b.colorCode) {
|
|
return 0;
|
|
}
|
|
if (a.colorCode < b.colorCode) {
|
|
return -1;
|
|
}
|
|
return 1;
|
|
}
|
|
function createObject(elements, elementSize, isConditionalSegments = false, totalElements = null) {
|
|
elements.sort(sortByMaterial);
|
|
if (totalElements === null) {
|
|
totalElements = elements.length;
|
|
}
|
|
const positions = new Float32Array(elementSize * totalElements * 3);
|
|
const normals = elementSize === 3 ? new Float32Array(elementSize * totalElements * 3) : null;
|
|
const materials = [];
|
|
const quadArray = new Array(6);
|
|
const bufferGeometry = new THREE.BufferGeometry();
|
|
let prevMaterial = null;
|
|
let index0 = 0;
|
|
let numGroupVerts = 0;
|
|
let offset = 0;
|
|
for (let iElem = 0, nElem = elements.length; iElem < nElem; iElem++) {
|
|
const elem = elements[iElem];
|
|
let vertices = elem.vertices;
|
|
if (vertices.length === 4) {
|
|
quadArray[0] = vertices[0];
|
|
quadArray[1] = vertices[1];
|
|
quadArray[2] = vertices[2];
|
|
quadArray[3] = vertices[0];
|
|
quadArray[4] = vertices[2];
|
|
quadArray[5] = vertices[3];
|
|
vertices = quadArray;
|
|
}
|
|
for (let j = 0, l = vertices.length; j < l; j++) {
|
|
const v = vertices[j];
|
|
const index = offset + j * 3;
|
|
positions[index + 0] = v.x;
|
|
positions[index + 1] = v.y;
|
|
positions[index + 2] = v.z;
|
|
}
|
|
if (elementSize === 3) {
|
|
if (!elem.faceNormal) {
|
|
const v0 = vertices[0];
|
|
const v1 = vertices[1];
|
|
const v2 = vertices[2];
|
|
_tempVec0.subVectors(v1, v0);
|
|
_tempVec1.subVectors(v2, v1);
|
|
elem.faceNormal = new THREE.Vector3().crossVectors(_tempVec0, _tempVec1).normalize();
|
|
}
|
|
let elemNormals = elem.normals;
|
|
if (elemNormals.length === 4) {
|
|
quadArray[0] = elemNormals[0];
|
|
quadArray[1] = elemNormals[1];
|
|
quadArray[2] = elemNormals[2];
|
|
quadArray[3] = elemNormals[0];
|
|
quadArray[4] = elemNormals[2];
|
|
quadArray[5] = elemNormals[3];
|
|
elemNormals = quadArray;
|
|
}
|
|
for (let j = 0, l = elemNormals.length; j < l; j++) {
|
|
let n = elem.faceNormal;
|
|
if (elemNormals[j]) {
|
|
n = elemNormals[j].norm;
|
|
}
|
|
const index = offset + j * 3;
|
|
normals[index + 0] = n.x;
|
|
normals[index + 1] = n.y;
|
|
normals[index + 2] = n.z;
|
|
}
|
|
}
|
|
if (prevMaterial !== elem.colorCode) {
|
|
if (prevMaterial !== null) {
|
|
bufferGeometry.addGroup(index0, numGroupVerts, materials.length - 1);
|
|
}
|
|
const material = elem.material;
|
|
if (material !== null) {
|
|
if (elementSize === 3) {
|
|
materials.push(material);
|
|
} else if (elementSize === 2) {
|
|
if (material !== null) {
|
|
if (isConditionalSegments) {
|
|
materials.push(material.userData.edgeMaterial.userData.conditionalEdgeMaterial);
|
|
} else {
|
|
materials.push(material.userData.edgeMaterial);
|
|
}
|
|
} else {
|
|
materials.push(null);
|
|
}
|
|
}
|
|
} else {
|
|
materials.push(elem.colorCode);
|
|
}
|
|
prevMaterial = elem.colorCode;
|
|
index0 = offset / 3;
|
|
numGroupVerts = vertices.length;
|
|
} else {
|
|
numGroupVerts += vertices.length;
|
|
}
|
|
offset += 3 * vertices.length;
|
|
}
|
|
if (numGroupVerts > 0) {
|
|
bufferGeometry.addGroup(index0, Infinity, materials.length - 1);
|
|
}
|
|
bufferGeometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
if (normals !== null) {
|
|
bufferGeometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
|
|
}
|
|
let object3d = null;
|
|
if (elementSize === 2) {
|
|
if (isConditionalSegments) {
|
|
object3d = new ConditionalLineSegments(bufferGeometry, materials.length === 1 ? materials[0] : materials);
|
|
} else {
|
|
object3d = new THREE.LineSegments(bufferGeometry, materials.length === 1 ? materials[0] : materials);
|
|
}
|
|
} else if (elementSize === 3) {
|
|
object3d = new THREE.Mesh(bufferGeometry, materials.length === 1 ? materials[0] : materials);
|
|
}
|
|
if (isConditionalSegments) {
|
|
object3d.isConditionalLine = true;
|
|
const controlArray0 = new Float32Array(elements.length * 3 * 2);
|
|
const controlArray1 = new Float32Array(elements.length * 3 * 2);
|
|
const directionArray = new Float32Array(elements.length * 3 * 2);
|
|
for (let i = 0, l = elements.length; i < l; i++) {
|
|
const os = elements[i];
|
|
const vertices = os.vertices;
|
|
const controlPoints = os.controlPoints;
|
|
const c0 = controlPoints[0];
|
|
const c1 = controlPoints[1];
|
|
const v0 = vertices[0];
|
|
const v1 = vertices[1];
|
|
const index = i * 3 * 2;
|
|
controlArray0[index + 0] = c0.x;
|
|
controlArray0[index + 1] = c0.y;
|
|
controlArray0[index + 2] = c0.z;
|
|
controlArray0[index + 3] = c0.x;
|
|
controlArray0[index + 4] = c0.y;
|
|
controlArray0[index + 5] = c0.z;
|
|
controlArray1[index + 0] = c1.x;
|
|
controlArray1[index + 1] = c1.y;
|
|
controlArray1[index + 2] = c1.z;
|
|
controlArray1[index + 3] = c1.x;
|
|
controlArray1[index + 4] = c1.y;
|
|
controlArray1[index + 5] = c1.z;
|
|
directionArray[index + 0] = v1.x - v0.x;
|
|
directionArray[index + 1] = v1.y - v0.y;
|
|
directionArray[index + 2] = v1.z - v0.z;
|
|
directionArray[index + 3] = v1.x - v0.x;
|
|
directionArray[index + 4] = v1.y - v0.y;
|
|
directionArray[index + 5] = v1.z - v0.z;
|
|
}
|
|
bufferGeometry.setAttribute("control0", new THREE.BufferAttribute(controlArray0, 3, false));
|
|
bufferGeometry.setAttribute("control1", new THREE.BufferAttribute(controlArray1, 3, false));
|
|
bufferGeometry.setAttribute("direction", new THREE.BufferAttribute(directionArray, 3, false));
|
|
}
|
|
return object3d;
|
|
}
|
|
class LDrawLoader extends THREE.Loader {
|
|
constructor(manager) {
|
|
super(manager);
|
|
this.materials = [];
|
|
this.materialLibrary = {};
|
|
this.partsCache = new LDrawPartsGeometryCache(this);
|
|
this.fileMap = {};
|
|
this.setMaterials([]);
|
|
this.smoothNormals = true;
|
|
this.partsLibraryPath = "";
|
|
}
|
|
setPartsLibraryPath(path) {
|
|
this.partsLibraryPath = path;
|
|
return this;
|
|
}
|
|
async preloadMaterials(url) {
|
|
const fileLoader = new THREE.FileLoader(this.manager);
|
|
fileLoader.setPath(this.path);
|
|
fileLoader.setRequestHeader(this.requestHeader);
|
|
fileLoader.setWithCredentials(this.withCredentials);
|
|
const text = await fileLoader.loadAsync(url);
|
|
const colorLineRegex = /^0 !COLOUR/;
|
|
const lines = text.split(/[\n\r]/g);
|
|
const materials = [];
|
|
for (let i = 0, l = lines.length; i < l; i++) {
|
|
const line = lines[i];
|
|
if (colorLineRegex.test(line)) {
|
|
const directive = line.replace(colorLineRegex, "");
|
|
const material = this.parseColorMetaDirective(new LineParser(directive));
|
|
materials.push(material);
|
|
}
|
|
}
|
|
this.setMaterials(materials);
|
|
}
|
|
load(url, onLoad, onProgress, onError) {
|
|
const fileLoader = new THREE.FileLoader(this.manager);
|
|
fileLoader.setPath(this.path);
|
|
fileLoader.setRequestHeader(this.requestHeader);
|
|
fileLoader.setWithCredentials(this.withCredentials);
|
|
fileLoader.load(
|
|
url,
|
|
(text) => {
|
|
this.partsCache.parseModel(text, this.materialLibrary).then((group) => {
|
|
this.applyMaterialsToMesh(group, MAIN_COLOUR_CODE, this.materialLibrary, true);
|
|
this.computeConstructionSteps(group);
|
|
onLoad(group);
|
|
}).catch(onError);
|
|
},
|
|
onProgress,
|
|
onError
|
|
);
|
|
}
|
|
parse(text, onLoad) {
|
|
this.partsCache.parseModel(text, this.materialLibrary).then((group) => {
|
|
this.computeConstructionSteps(group);
|
|
onLoad(group);
|
|
});
|
|
}
|
|
setMaterials(materials) {
|
|
this.materialLibrary = {};
|
|
this.materials = [];
|
|
for (let i = 0, l = materials.length; i < l; i++) {
|
|
this.addMaterial(materials[i]);
|
|
}
|
|
this.addMaterial(this.parseColorMetaDirective(new LineParser("Main_Colour CODE 16 VALUE #FF8080 EDGE #333333")));
|
|
this.addMaterial(this.parseColorMetaDirective(new LineParser("Edge_Colour CODE 24 VALUE #A0A0A0 EDGE #333333")));
|
|
return this;
|
|
}
|
|
setFileMap(fileMap) {
|
|
this.fileMap = fileMap;
|
|
return this;
|
|
}
|
|
addMaterial(material) {
|
|
const matLib = this.materialLibrary;
|
|
if (!matLib[material.userData.code]) {
|
|
this.materials.push(material);
|
|
matLib[material.userData.code] = material;
|
|
}
|
|
return this;
|
|
}
|
|
getMaterial(colorCode) {
|
|
if (colorCode.startsWith("0x2")) {
|
|
const color = colorCode.substring(3);
|
|
return this.parseColorMetaDirective(
|
|
new LineParser("Direct_Color_" + color + " CODE -1 VALUE #" + color + " EDGE #" + color)
|
|
);
|
|
}
|
|
return this.materialLibrary[colorCode] || null;
|
|
}
|
|
// Applies the appropriate materials to a prebuilt hierarchy of geometry. Assumes that color codes are present
|
|
// in the material array if they need to be filled in.
|
|
applyMaterialsToMesh(group, parentColorCode, materialHierarchy, finalMaterialPass = false) {
|
|
const loader = this;
|
|
const parentIsPassthrough = parentColorCode === MAIN_COLOUR_CODE;
|
|
group.traverse((c) => {
|
|
if (c.isMesh || c.isLineSegments) {
|
|
if (Array.isArray(c.material)) {
|
|
for (let i = 0, l = c.material.length; i < l; i++) {
|
|
if (!c.material[i].isMaterial) {
|
|
c.material[i] = getMaterial(c, c.material[i]);
|
|
}
|
|
}
|
|
} else if (!c.material.isMaterial) {
|
|
c.material = getMaterial(c, c.material);
|
|
}
|
|
}
|
|
});
|
|
function getMaterial(c, colorCode) {
|
|
if (parentIsPassthrough && !(colorCode in materialHierarchy) && !finalMaterialPass) {
|
|
return colorCode;
|
|
}
|
|
const forEdge = c.isLineSegments || c.isConditionalLine;
|
|
const isPassthrough = !forEdge && colorCode === MAIN_COLOUR_CODE || forEdge && colorCode === MAIN_EDGE_COLOUR_CODE;
|
|
if (isPassthrough) {
|
|
colorCode = parentColorCode;
|
|
}
|
|
let material = null;
|
|
if (colorCode in materialHierarchy) {
|
|
material = materialHierarchy[colorCode];
|
|
} else if (finalMaterialPass) {
|
|
material = loader.getMaterial(colorCode);
|
|
if (material === null) {
|
|
throw new Error(`LDrawLoader: Material properties for code ${colorCode} not available.`);
|
|
}
|
|
} else {
|
|
return colorCode;
|
|
}
|
|
if (c.isLineSegments) {
|
|
material = material.userData.edgeMaterial;
|
|
if (c.isConditionalLine) {
|
|
material = material.userData.conditionalEdgeMaterial;
|
|
}
|
|
}
|
|
return material;
|
|
}
|
|
}
|
|
getMainMaterial() {
|
|
return this.getMaterial(MAIN_COLOUR_CODE);
|
|
}
|
|
getMainEdgeMaterial() {
|
|
return this.getMaterial(MAIN_EDGE_COLOUR_CODE);
|
|
}
|
|
parseColorMetaDirective(lineParser) {
|
|
let code = null;
|
|
let color = 16711935;
|
|
let edgeColor = 16711935;
|
|
let alpha = 1;
|
|
let isTransparent = false;
|
|
let luminance = 0;
|
|
let finishType = FINISH_TYPE_DEFAULT;
|
|
let edgeMaterial = null;
|
|
const name = lineParser.getToken();
|
|
if (!name) {
|
|
throw new Error(
|
|
'LDrawLoader: Material name was expected after "!COLOUR tag' + lineParser.getLineNumberString() + "."
|
|
);
|
|
}
|
|
let token = null;
|
|
while (true) {
|
|
token = lineParser.getToken();
|
|
if (!token) {
|
|
break;
|
|
}
|
|
switch (token.toUpperCase()) {
|
|
case "CODE":
|
|
code = lineParser.getToken();
|
|
break;
|
|
case "VALUE":
|
|
color = lineParser.getToken();
|
|
if (color.startsWith("0x")) {
|
|
color = "#" + color.substring(2);
|
|
} else if (!color.startsWith("#")) {
|
|
throw new Error(
|
|
"LDrawLoader: Invalid color while parsing material" + lineParser.getLineNumberString() + "."
|
|
);
|
|
}
|
|
break;
|
|
case "EDGE":
|
|
edgeColor = lineParser.getToken();
|
|
if (edgeColor.startsWith("0x")) {
|
|
edgeColor = "#" + edgeColor.substring(2);
|
|
} else if (!edgeColor.startsWith("#")) {
|
|
edgeMaterial = this.getMaterial(edgeColor);
|
|
if (!edgeMaterial) {
|
|
throw new Error(
|
|
"LDrawLoader: Invalid edge color while parsing material" + lineParser.getLineNumberString() + "."
|
|
);
|
|
}
|
|
edgeMaterial = edgeMaterial.userData.edgeMaterial;
|
|
}
|
|
break;
|
|
case "ALPHA":
|
|
alpha = parseInt(lineParser.getToken());
|
|
if (isNaN(alpha)) {
|
|
throw new Error(
|
|
"LDrawLoader: Invalid alpha value in material definition" + lineParser.getLineNumberString() + "."
|
|
);
|
|
}
|
|
alpha = Math.max(0, Math.min(1, alpha / 255));
|
|
if (alpha < 1) {
|
|
isTransparent = true;
|
|
}
|
|
break;
|
|
case "LUMINANCE":
|
|
luminance = parseInt(lineParser.getToken());
|
|
if (isNaN(luminance)) {
|
|
throw new Error(
|
|
"LDrawLoader: Invalid luminance value in material definition" + LineParser.getLineNumberString() + "."
|
|
);
|
|
}
|
|
luminance = Math.max(0, Math.min(1, luminance / 255));
|
|
break;
|
|
case "CHROME":
|
|
finishType = FINISH_TYPE_CHROME;
|
|
break;
|
|
case "PEARLESCENT":
|
|
finishType = FINISH_TYPE_PEARLESCENT;
|
|
break;
|
|
case "RUBBER":
|
|
finishType = FINISH_TYPE_RUBBER;
|
|
break;
|
|
case "MATTE_METALLIC":
|
|
finishType = FINISH_TYPE_MATTE_METALLIC;
|
|
break;
|
|
case "METAL":
|
|
finishType = FINISH_TYPE_METAL;
|
|
break;
|
|
case "MATERIAL":
|
|
lineParser.setToEnd();
|
|
break;
|
|
default:
|
|
throw new Error(
|
|
'LDrawLoader: Unknown token "' + token + '" while parsing material' + lineParser.getLineNumberString() + "."
|
|
);
|
|
}
|
|
}
|
|
let material = null;
|
|
switch (finishType) {
|
|
case FINISH_TYPE_DEFAULT:
|
|
material = new THREE.MeshStandardMaterial({ color, roughness: 0.3, metalness: 0 });
|
|
break;
|
|
case FINISH_TYPE_PEARLESCENT:
|
|
material = new THREE.MeshStandardMaterial({ color, roughness: 0.3, metalness: 0.25 });
|
|
break;
|
|
case FINISH_TYPE_CHROME:
|
|
material = new THREE.MeshStandardMaterial({ color, roughness: 0, metalness: 1 });
|
|
break;
|
|
case FINISH_TYPE_RUBBER:
|
|
material = new THREE.MeshStandardMaterial({ color, roughness: 0.9, metalness: 0 });
|
|
break;
|
|
case FINISH_TYPE_MATTE_METALLIC:
|
|
material = new THREE.MeshStandardMaterial({ color, roughness: 0.8, metalness: 0.4 });
|
|
break;
|
|
case FINISH_TYPE_METAL:
|
|
material = new THREE.MeshStandardMaterial({ color, roughness: 0.2, metalness: 0.85 });
|
|
break;
|
|
}
|
|
material.transparent = isTransparent;
|
|
material.premultipliedAlpha = true;
|
|
material.opacity = alpha;
|
|
material.depthWrite = !isTransparent;
|
|
material.polygonOffset = true;
|
|
material.polygonOffsetFactor = 1;
|
|
if (luminance !== 0) {
|
|
material.emissive.set(material.color).multiplyScalar(luminance);
|
|
}
|
|
if (!edgeMaterial) {
|
|
edgeMaterial = new THREE.LineBasicMaterial({
|
|
color: edgeColor,
|
|
transparent: isTransparent,
|
|
opacity: alpha,
|
|
depthWrite: !isTransparent
|
|
});
|
|
edgeMaterial.userData.code = code;
|
|
edgeMaterial.name = name + " - Edge";
|
|
edgeMaterial.userData.conditionalEdgeMaterial = new LDrawConditionalLineMaterial({
|
|
fog: true,
|
|
transparent: isTransparent,
|
|
depthWrite: !isTransparent,
|
|
color: edgeColor,
|
|
opacity: alpha
|
|
});
|
|
}
|
|
material.userData.code = code;
|
|
material.name = name;
|
|
material.userData.edgeMaterial = edgeMaterial;
|
|
this.addMaterial(material);
|
|
return material;
|
|
}
|
|
computeConstructionSteps(model) {
|
|
let stepNumber = 0;
|
|
model.traverse((c) => {
|
|
if (c.isGroup) {
|
|
if (c.userData.startingConstructionStep) {
|
|
stepNumber++;
|
|
}
|
|
c.userData.constructionStep = stepNumber;
|
|
}
|
|
});
|
|
model.userData.numConstructionSteps = stepNumber + 1;
|
|
}
|
|
}
|
|
exports.LDrawLoader = LDrawLoader;
|
|
//# sourceMappingURL=LDrawLoader.cjs.map
|