obja/js/obja.js

412 lines
13 KiB
JavaScript

function fetchDataLength(path, callback) {
let xhr = new XMLHttpRequest();
xhr.open('HEAD', path, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(xhr.getResponseHeader('Content-Length'));
}
}
};
xhr.send();
}
function fetchData(path, start, end, callback) {
let xhr = new XMLHttpRequest();
xhr.open('GET', path, true);
xhr.setRequestHeader('Range', 'bytes=' + start + "-" + (end - 1));
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 206) {
callback(xhr.responseText);
}
}
};
xhr.send();
}
function parseLine(line, number) {
let element = {};
let split = line.split('#')[0].split(/[ \t]+/);
if (split.length === 0) {
return;
}
switch (split[0]) {
case "v":
element.type = Element.AddVertex;
element.value = new THREE.Vector3(
parseFloat(split[1]),
parseFloat(split[2]),
parseFloat(split[3]),
);
return element;
case "f":
element.type = Element.AddFace;
element.value = new THREE.Face3(
parseInt(split[1], 10) - 1,
parseInt(split[2], 10) - 1,
parseInt(split[3], 10) - 1,
);
return element;
case "fc":
element.type = Element.SetFaceColor;
element.id = parseInt(split[1], 10) - 1;
element.value = new THREE.Color(
parseFloat(split[2]),
parseFloat(split[3]),
parseFloat(split[4]),
);
return element;
case "ts":
element.type = Element.AddTriangleStrip;
element.value = [];
for (let i = 1; i < split.length - 2; i++) {
element.value.push(new THREE.Face3(
parseInt(split[i] , 10) - 1,
i % 2 === 1 ? parseInt(split[i+1], 10) - 1 : parseInt(split[i+2], 10) - 1,
i % 2 === 1 ? parseInt(split[i+2], 10) - 1 : parseInt(split[i+1], 10) - 1,
));
}
return element;
case "tf":
element.type = Element.AddTriangleFan;
element.value = [];
for (let i = 1; i < split.length - 2; i++) {
element.value.push(new THREE.Face3(
parseInt(split[1] , 10) - 1,
parseInt(split[i+1], 10) - 1,
parseInt(split[i+2], 10) - 1,
));
}
return element;
case "ev":
element.type = Element.EditVertex;
element.id = parseInt(split[1], 10) - 1;
element.value = new THREE.Vector3(
parseFloat(split[2]),
parseFloat(split[3]),
parseFloat(split[4]),
);
return element;
case "tv":
element.type = Element.TranslateVertex;
element.id = parseInt(split[1], 10) - 1;
element.value = new THREE.Vector3(
parseFloat(split[2]),
parseFloat(split[3]),
parseFloat(split[4]),
);
return element;
case "pv":
element.type = Element.PredictVertex;
element.value = new THREE.Face3(
parseInt(split[2], 10) - 1,
parseInt(split[2], 10) - 1,
parseInt(split[2], 10) - 1,
);
return element;
case "efv":
element.type = Element.EditFaceVertex;
element.id = parseInt(split[1], 10) - 1;
element.oldIndex = parseInt(split[2], 10) - 1;
element.value = parseInt(split[3], 10) - 1;
return element;
case "ef":
element.type = Element.EditFace;
element.id = parseInt(split[1], 10) - 1;
element.value = new THREE.Face3(
parseInt(split[2], 10) - 1,
parseInt(split[3], 10) - 1,
parseInt(split[4], 10) - 1,
);
return element;
case "df":
element.type = Element.DeleteFace;
element.id = parseInt(split[1], 10) - 1;
return element;
case "":
case "#":
return;
default:
return;
// throw new Error(split[0] + " is not a defined macro in line " + number);
}
}
const Element = {};
Element.AddVertex = "AddVertex";
Element.AddFace = "AddFace";
Element.AddTriangleStrip = "AddTriangleStrip";
Element.AddTriangleFan = "AddTriangleFan";
Element.EditVertex = "EditVertex";
Element.EditFace = "EditFace";
Element.EditFaceVertex = "EditFaceVertex";
Element.TranslateVertex = "TranslateVertex";
Element.DeleteFace = "DeleteFace";
Element.SetFaceColor = "SetFaceColor";
Element.PredictVertex = "PredictVertex";
class Loader {
constructor(path, chunkSize = 1024, timeout = 20) {
this.path = path;
this.chunkSize = chunkSize;
this.timeout = timeout;
this.currentByte = 0;
this.remainder = "";
}
start(callback) {
fetchDataLength(this.path, (length) => {
this.dataLength = length;
this.next(callback);
});
}
percentage() {
return 100 * this.currentByte / this.dataLength;
}
next(callback) {
this.downloadAndParseNextChunk((data) => {
callback(data);
setTimeout(() => {
this.next(callback);
}, this.timeout);
});
}
downloadAndParseNextChunk(callback) {
let upperBound = Math.min(this.currentByte + this.chunkSize, this.dataLength);
if (upperBound <= this.currentByte) {
if (this.remainder !== "") {
callback([parseLine(this.remainder, 0)]);
}
return;
}
fetchData(this.path, this.currentByte, upperBound, (data) => {
this.currentByte = upperBound;
let elements = [];
let split = data.split('\n');
split[0] = this.remainder + split[0];
this.remainder = split.pop();
for (let i = 0; i < split.length; i++) {
elements.push(parseLine(split[i], i));
}
callback(elements);
});
}
}
class Model extends THREE.Mesh {
constructor(path) {
let geometry = new THREE.Geometry();
let materials = [
new THREE.MeshLambertMaterial( { color: 0xffffff, side: THREE.DoubleSide } ),
new THREE.MeshBasicMaterial( { transparent: true, opacity: 0 } )
];
materials[0].vertexColors = true;
super(geometry, materials);
this.frustumCulled = false;
this.path = path;
this.vertices = [];
this.currentLine = 1;
}
throwError(message) {
let e = new Error("In " + this.path + ":L" + this.currentLine + " " + message);
e.type = "custom";
throw e;
}
checkVertex(id) {
if (this.geometry.vertices[id] === undefined) {
this.throwError("EditVertex requires vertex " + (id + 1) + " but there is no such vertex");
}
}
checkFaceId(id) {
if (this.geometry.faces[id] === undefined) {
this.throwError("EditFace requires face " + (id + 1) + " but there is no such face");
}
}
checkFace(f) {
let vertices = this.geometry.vertices;
if (vertices[f.a] === undefined) {
this.throwError("Face requires vertex " + (f.a + 1) + " but there is no such vertex");
}
if (vertices[f.b] === undefined) {
this.throwError("Face requires vertex " + (f.b + 1) + " but there is no such vertex");
}
if (vertices[f.c] === undefined) {
this.throwError("Face requires vertex " + (f.c + 1) + " but there is no such vertex");
}
}
checkVertexPrediction(f) {
let vertices = this.geometry.vertices;
if (vertices[f.a] === undefined) {
this.throwError("Vertex prediction requires vertex " + (f.a + 1) + " but there is no such vertex");
}
if (vertices[f.b] === undefined) {
this.throwError("Vertex prediction requires vertex " + (f.b + 1) + " but there is no such vertex");
}
if (vertices[f.c] === undefined) {
this.throwError("Vertex prediction requires vertex " + (f.c + 1) + " but there is no such vertex");
}
}
manageElement(element) {
let vertices = this.geometry.vertices;
let f, normal;
switch (element.type) {
case Element.AddVertex:
this.geometry.vertices.push(element.value);
this.geometry.verticesNeedUpdate = true;
break;
case Element.EditVertex:
this.checkVertex(element.id);
this.geometry.vertices[element.id].copy(element.value);
this.geometry.verticesNeedUpdate = true;
break;
case Element.TranslateVertex:
this.checkVertex(element.id);
this.geometry.vertices[element.id].add(element.value);
this.geometry.verticesNeedUpdate = true;
break;
case Element.AddFace:
f = element.value;
this.checkFace(f);
normal =
vertices[f.b].clone().sub(vertices[f.a])
.cross(vertices[f.c].clone().sub(vertices[f.a]));
normal.normalize();
f.normal = normal;
f.materialIndex = 0;
this.geometry.faces.push(f);
this.geometry.elementsNeedUpdate = true;
break;
case Element.AddTriangleStrip:
case Element.AddTriangleFan:
for (let f of element.value) {
this.checkFace(f);
let normal =
vertices[f.b].clone().sub(vertices[f.a])
.cross(vertices[f.c].clone().sub(vertices[f.a]));
normal.normalize();
f.normal = normal;
f.materialIndex = 0;
this.geometry.faces.push(f);
}
this.geometry.elementsNeedUpdate = true;
break;
case Element.EditFace:
f = element.value;
this.checkFaceId(element.id);
this.checkFace(f);
normal =
vertices[f.b].clone().sub(vertices[f.a])
.cross(vertices[f.c].clone().sub(vertices[f.a]));
normal.normalize();
f.normal = normal;
f.color = this.geometry.faces[element.id].color;
this.geometry.faces[element.id] = f;
this.geometry.elementsNeedUpdate = true;
break;
case Element.EditFaceVertex:
this.checkFaceId(element.id);
switch (element.oldIndex) {
case 0: this.geometry.faces[element.id].a = element.value; break;
case 1: this.geometry.faces[element.id].b = element.value; break;
case 2: this.geometry.faces[element.id].c = element.value; break;
default: this.throwError("Old vertex id in EditFaceVertex must be 1, 2 or 3, but was " + element.oldIndex + 1);
}
this.geometry.elementsNeedUpdate = true;
break;
case Element.DeleteFace:
this.geometry.faces[element.id].materialIndex = 1;
this.geometry.elementsNeedUpdate = true;
break;
case Element.SetFaceColor:
this.geometry.faces[element.id].color.r = element.value.r;
this.geometry.faces[element.id].color.g = element.value.g;
this.geometry.faces[element.id].color.b = element.value.b;
this.geometry.colorsNeedUpdate = true;
break;
case Element.PredictVertex:
this.checkVertexPrediction(element.value);
vertices.push(vertices[element.value.a].clone()
.add(vertices[element.value.c])
.sub(vertices[element.value.b]));
this.geometry.verticesNeedUpdate = true;
break;
default:
throw new Error("unknown element type: " + element.type);
}
this.currentLine++;
}
}