@loaders.gl/obj
Version:
Framework-independent loader for the OBJ format
780 lines (768 loc) • 24.9 kB
JavaScript
"use strict";
(() => {
// ../worker-utils/src/lib/node/worker_threads-browser.ts
var parentPort = null;
// ../worker-utils/src/lib/worker-utils/get-transfer-list.ts
function getTransferList(object, recursive = true, transfers) {
const transfersSet = transfers || /* @__PURE__ */ new Set();
if (!object) {
} else if (isTransferable(object)) {
transfersSet.add(object);
} else if (isTransferable(object.buffer)) {
transfersSet.add(object.buffer);
} else if (ArrayBuffer.isView(object)) {
} else if (recursive && typeof object === "object") {
for (const key in object) {
getTransferList(object[key], recursive, transfersSet);
}
}
return transfers === void 0 ? Array.from(transfersSet) : [];
}
function isTransferable(object) {
if (!object) {
return false;
}
if (object instanceof ArrayBuffer) {
return true;
}
if (typeof MessagePort !== "undefined" && object instanceof MessagePort) {
return true;
}
if (typeof ImageBitmap !== "undefined" && object instanceof ImageBitmap) {
return true;
}
if (typeof OffscreenCanvas !== "undefined" && object instanceof OffscreenCanvas) {
return true;
}
return false;
}
// ../worker-utils/src/lib/worker-farm/worker-body.ts
async function getParentPort() {
return parentPort;
}
var onMessageWrapperMap = /* @__PURE__ */ new Map();
var WorkerBody = class {
/** Check that we are actually in a worker thread */
static async inWorkerThread() {
return typeof self !== "undefined" || Boolean(await getParentPort());
}
/*
* (type: WorkerMessageType, payload: WorkerMessagePayload) => any
*/
static set onmessage(onMessage) {
async function handleMessage(message) {
const parentPort2 = await getParentPort();
const { type, payload } = parentPort2 ? message : message.data;
onMessage(type, payload);
}
getParentPort().then((parentPort2) => {
if (parentPort2) {
parentPort2.on("message", (message) => {
handleMessage(message);
});
parentPort2.on("exit", () => console.debug("Node worker closing"));
} else {
globalThis.onmessage = handleMessage;
}
});
}
static async addEventListener(onMessage) {
let onMessageWrapper = onMessageWrapperMap.get(onMessage);
if (!onMessageWrapper) {
onMessageWrapper = async (message) => {
if (!isKnownMessage(message)) {
return;
}
const parentPort3 = await getParentPort();
const { type, payload } = parentPort3 ? message : message.data;
onMessage(type, payload);
};
}
const parentPort2 = await getParentPort();
if (parentPort2) {
console.error("not implemented");
} else {
globalThis.addEventListener("message", onMessageWrapper);
}
}
static async removeEventListener(onMessage) {
const onMessageWrapper = onMessageWrapperMap.get(onMessage);
onMessageWrapperMap.delete(onMessage);
const parentPort2 = await getParentPort();
if (parentPort2) {
console.error("not implemented");
} else {
globalThis.removeEventListener("message", onMessageWrapper);
}
}
/**
* Send a message from a worker to creating thread (main thread)
* @param type
* @param payload
*/
static async postMessage(type, payload) {
const data = { source: "loaders.gl", type, payload };
const transferList = getTransferList(payload);
const parentPort2 = await getParentPort();
if (parentPort2) {
parentPort2.postMessage(data, transferList);
} else {
globalThis.postMessage(data, transferList);
}
}
};
function isKnownMessage(message) {
const { type, data } = message;
return type === "message" && data && typeof data.source === "string" && data.source.startsWith("loaders.gl");
}
// ../loader-utils/src/lib/worker-loader-utils/create-loader-worker.ts
var requestId = 0;
async function createLoaderWorker(loader) {
if (!await WorkerBody.inWorkerThread()) {
return;
}
WorkerBody.onmessage = async (type, payload) => {
switch (type) {
case "process":
try {
const { input, options = {}, context = {} } = payload;
const result = await parseData({
loader,
arrayBuffer: input,
options,
// @ts-expect-error fetch missing
context: {
...context,
_parse: parseOnMainThread
}
});
WorkerBody.postMessage("done", { result });
} catch (error) {
const message = error instanceof Error ? error.message : "";
WorkerBody.postMessage("error", { error: message });
}
break;
default:
}
};
}
function parseOnMainThread(arrayBuffer, loader, options, context) {
return new Promise((resolve, reject) => {
const id = requestId++;
const onMessage = (type, payload2) => {
if (payload2.id !== id) {
return;
}
switch (type) {
case "done":
WorkerBody.removeEventListener(onMessage);
resolve(payload2.result);
break;
case "error":
WorkerBody.removeEventListener(onMessage);
reject(payload2.error);
break;
default:
}
};
WorkerBody.addEventListener(onMessage);
const payload = { id, input: arrayBuffer, options };
WorkerBody.postMessage("process", payload);
});
}
async function parseData({
loader,
arrayBuffer,
options,
context
}) {
let data;
let parser;
if (loader.parseSync || loader.parse) {
data = arrayBuffer;
parser = loader.parseSync || loader.parse;
} else if (loader.parseTextSync) {
const textDecoder = new TextDecoder();
data = textDecoder.decode(arrayBuffer);
parser = loader.parseTextSync;
} else {
throw new Error(`Could not load data with ${loader.name} loader`);
}
options = {
...options,
modules: loader && loader.options && loader.options.modules || {},
core: {
...options.core,
worker: false
}
};
return await parser(data, { ...options }, context, loader);
}
// src/obj-format.ts
var OBJFormat = {
name: "OBJ",
id: "obj",
module: "obj",
extensions: ["obj"],
mimeTypes: ["text/plain"]
};
// ../schema-utils/src/lib/schema/data-type.ts
function getDataTypeFromValue(value, defaultNumberType = "float32") {
if (value instanceof Date) {
return "date-millisecond";
}
if (value instanceof Number) {
return defaultNumberType;
}
if (typeof value === "string") {
return "utf8";
}
if (value === null || value === "undefined") {
return "null";
}
return "null";
}
function getDataTypeFromArray(array) {
let type = getDataTypeFromTypedArray(array);
if (type !== "null") {
return { type, nullable: false };
}
if (array.length > 0) {
type = getDataTypeFromValue(array[0]);
return { type, nullable: true };
}
return { type: "null", nullable: true };
}
function getDataTypeFromTypedArray(array) {
switch (array.constructor) {
case Int8Array:
return "int8";
case Uint8Array:
case Uint8ClampedArray:
return "uint8";
case Int16Array:
return "int16";
case Uint16Array:
return "uint16";
case Int32Array:
return "int32";
case Uint32Array:
return "uint32";
case Float32Array:
return "float32";
case Float64Array:
return "float64";
default:
return "null";
}
}
// ../schema-utils/src/lib/mesh/mesh-utils.ts
function getMeshBoundingBox(attributes) {
let minX = Infinity;
let minY = Infinity;
let minZ = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
let maxZ = -Infinity;
const positions = attributes.POSITION ? attributes.POSITION.value : [];
const len = positions && positions.length;
for (let i = 0; i < len; i += 3) {
const x = positions[i];
const y = positions[i + 1];
const z = positions[i + 2];
minX = x < minX ? x : minX;
minY = y < minY ? y : minY;
minZ = z < minZ ? z : minZ;
maxX = x > maxX ? x : maxX;
maxY = y > maxY ? y : maxY;
maxZ = z > maxZ ? z : maxZ;
}
return [
[minX, minY, minZ],
[maxX, maxY, maxZ]
];
}
// src/lib/parse-obj-meshes.ts
var OBJECT_RE = /^[og]\s*(.+)?/;
var MATERIAL_RE = /^mtllib /;
var MATERIAL_USE_RE = /^usemtl /;
var MeshMaterial = class {
constructor({ index, name = "", mtllib, smooth, groupStart }) {
this.index = index;
this.name = name;
this.mtllib = mtllib;
this.smooth = smooth;
this.groupStart = groupStart;
this.groupEnd = -1;
this.groupCount = -1;
this.inherited = false;
}
clone(index = this.index) {
return new MeshMaterial({
index,
name: this.name,
mtllib: this.mtllib,
smooth: this.smooth,
groupStart: 0
});
}
};
var MeshObject = class {
constructor(name = "") {
this.name = name;
this.geometry = {
vertices: [],
normals: [],
colors: [],
uvs: []
};
this.materials = [];
this.smooth = true;
this.fromDeclaration = null;
}
startMaterial(name, libraries) {
const previous = this._finalize(false);
if (previous && (previous.inherited || previous.groupCount <= 0)) {
this.materials.splice(previous.index, 1);
}
const material = new MeshMaterial({
index: this.materials.length,
name,
mtllib: Array.isArray(libraries) && libraries.length > 0 ? libraries[libraries.length - 1] : "",
smooth: previous !== void 0 ? previous.smooth : this.smooth,
groupStart: previous !== void 0 ? previous.groupEnd : 0
});
this.materials.push(material);
return material;
}
currentMaterial() {
if (this.materials.length > 0) {
return this.materials[this.materials.length - 1];
}
return void 0;
}
_finalize(end) {
const lastMultiMaterial = this.currentMaterial();
if (lastMultiMaterial && lastMultiMaterial.groupEnd === -1) {
lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3;
lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart;
lastMultiMaterial.inherited = false;
}
if (end && this.materials.length > 1) {
for (let mi = this.materials.length - 1; mi >= 0; mi--) {
if (this.materials[mi].groupCount <= 0) {
this.materials.splice(mi, 1);
}
}
}
if (end && this.materials.length === 0) {
this.materials.push({
name: "",
smooth: this.smooth
});
}
return lastMultiMaterial;
}
};
var ParserState = class {
constructor() {
this.objects = [];
this.object = null;
this.vertices = [];
this.normals = [];
this.colors = [];
this.uvs = [];
this.materialLibraries = [];
this.startObject("", false);
}
startObject(name, fromDeclaration = true) {
if (this.object && !this.object.fromDeclaration) {
this.object.name = name;
this.object.fromDeclaration = fromDeclaration;
return;
}
const previousMaterial = this.object && typeof this.object.currentMaterial === "function" ? this.object.currentMaterial() : void 0;
if (this.object && typeof this.object._finalize === "function") {
this.object._finalize(true);
}
this.object = new MeshObject(name);
this.object.fromDeclaration = fromDeclaration;
if (previousMaterial && previousMaterial.name && typeof previousMaterial.clone === "function") {
const declared = previousMaterial.clone(0);
declared.inherited = true;
this.object.materials.push(declared);
}
this.objects.push(this.object);
}
finalize() {
if (this.object && typeof this.object._finalize === "function") {
this.object._finalize(true);
}
}
parseVertexIndex(value, len) {
const index = parseInt(value);
return (index >= 0 ? index - 1 : index + len / 3) * 3;
}
parseNormalIndex(value, len) {
const index = parseInt(value);
return (index >= 0 ? index - 1 : index + len / 3) * 3;
}
parseUVIndex(value, len) {
const index = parseInt(value);
return (index >= 0 ? index - 1 : index + len / 2) * 2;
}
addVertex(a, b, c) {
const src = this.vertices;
const dst = this.object.geometry.vertices;
dst.push(src[a + 0], src[a + 1], src[a + 2]);
dst.push(src[b + 0], src[b + 1], src[b + 2]);
dst.push(src[c + 0], src[c + 1], src[c + 2]);
}
addVertexPoint(a) {
const src = this.vertices;
const dst = this.object.geometry.vertices;
dst.push(src[a + 0], src[a + 1], src[a + 2]);
}
addVertexLine(a) {
const src = this.vertices;
const dst = this.object.geometry.vertices;
dst.push(src[a + 0], src[a + 1], src[a + 2]);
}
addNormal(a, b, c) {
const src = this.normals;
const dst = this.object.geometry.normals;
dst.push(src[a + 0], src[a + 1], src[a + 2]);
dst.push(src[b + 0], src[b + 1], src[b + 2]);
dst.push(src[c + 0], src[c + 1], src[c + 2]);
}
addColor(a, b, c) {
const src = this.colors;
const dst = this.object.geometry.colors;
dst.push(src[a + 0], src[a + 1], src[a + 2]);
dst.push(src[b + 0], src[b + 1], src[b + 2]);
dst.push(src[c + 0], src[c + 1], src[c + 2]);
}
addUV(a, b, c) {
const src = this.uvs;
const dst = this.object.geometry.uvs;
dst.push(src[a + 0], src[a + 1]);
dst.push(src[b + 0], src[b + 1]);
dst.push(src[c + 0], src[c + 1]);
}
addUVLine(a) {
const src = this.uvs;
const dst = this.object.geometry.uvs;
dst.push(src[a + 0], src[a + 1]);
}
// eslint-disable-next-line max-params
addFace(a, b, c, ua, ub, uc, na, nb, nc) {
const vLen = this.vertices.length;
let ia = this.parseVertexIndex(a, vLen);
let ib = this.parseVertexIndex(b, vLen);
let ic = this.parseVertexIndex(c, vLen);
this.addVertex(ia, ib, ic);
if (ua !== void 0 && ua !== "") {
const uvLen = this.uvs.length;
ia = this.parseUVIndex(ua, uvLen);
ib = this.parseUVIndex(ub, uvLen);
ic = this.parseUVIndex(uc, uvLen);
this.addUV(ia, ib, ic);
}
if (na !== void 0 && na !== "") {
const nLen = this.normals.length;
ia = this.parseNormalIndex(na, nLen);
ib = na === nb ? ia : this.parseNormalIndex(nb, nLen);
ic = na === nc ? ia : this.parseNormalIndex(nc, nLen);
this.addNormal(ia, ib, ic);
}
if (this.colors.length > 0) {
this.addColor(ia, ib, ic);
}
}
addPointGeometry(vertices) {
this.object.geometry.type = "Points";
const vLen = this.vertices.length;
for (const vertex of vertices) {
this.addVertexPoint(this.parseVertexIndex(vertex, vLen));
}
}
addLineGeometry(vertices, uvs) {
this.object.geometry.type = "Line";
const vLen = this.vertices.length;
const uvLen = this.uvs.length;
for (const vertex of vertices) {
this.addVertexLine(this.parseVertexIndex(vertex, vLen));
}
for (const uv of uvs) {
this.addUVLine(this.parseUVIndex(uv, uvLen));
}
}
};
function parseOBJMeshes(text) {
const state = new ParserState();
if (text.indexOf("\r\n") !== -1) {
text = text.replace(/\r\n/g, "\n");
}
if (text.indexOf("\\\n") !== -1) {
text = text.replace(/\\\n/g, "");
}
const lines = text.split("\n");
let line = "";
let lineFirstChar = "";
let lineLength = 0;
let result = [];
const trimLeft = typeof "".trimLeft === "function";
for (let i = 0, l = lines.length; i < l; i++) {
line = lines[i];
line = trimLeft ? line.trimLeft() : line.trim();
lineLength = line.length;
if (lineLength === 0)
continue;
lineFirstChar = line.charAt(0);
if (lineFirstChar === "#")
continue;
if (lineFirstChar === "v") {
const data = line.split(/\s+/);
switch (data[0]) {
case "v":
state.vertices.push(parseFloat(data[1]), parseFloat(data[2]), parseFloat(data[3]));
if (data.length >= 7) {
state.colors.push(parseFloat(data[4]), parseFloat(data[5]), parseFloat(data[6]));
}
break;
case "vn":
state.normals.push(parseFloat(data[1]), parseFloat(data[2]), parseFloat(data[3]));
break;
case "vt":
state.uvs.push(parseFloat(data[1]), parseFloat(data[2]));
break;
default:
}
} else if (lineFirstChar === "f") {
const lineData = line.substr(1).trim();
const vertexData = lineData.split(/\s+/);
const faceVertices = [];
for (let j = 0, jl = vertexData.length; j < jl; j++) {
const vertex = vertexData[j];
if (vertex.length > 0) {
const vertexParts = vertex.split("/");
faceVertices.push(vertexParts);
}
}
const v1 = faceVertices[0];
for (let j = 1, jl = faceVertices.length - 1; j < jl; j++) {
const v2 = faceVertices[j];
const v3 = faceVertices[j + 1];
state.addFace(v1[0], v2[0], v3[0], v1[1], v2[1], v3[1], v1[2], v2[2], v3[2]);
}
} else if (lineFirstChar === "l") {
const lineParts = line.substring(1).trim().split(" ");
let lineVertices;
const lineUVs = [];
if (line.indexOf("/") === -1) {
lineVertices = lineParts;
} else {
lineVertices = [];
for (let li = 0, llen = lineParts.length; li < llen; li++) {
const parts = lineParts[li].split("/");
if (parts[0] !== "")
lineVertices.push(parts[0]);
if (parts[1] !== "")
lineUVs.push(parts[1]);
}
}
state.addLineGeometry(lineVertices, lineUVs);
} else if (lineFirstChar === "p") {
const lineData = line.substr(1).trim();
const pointData = lineData.split(" ");
state.addPointGeometry(pointData);
} else if ((result = OBJECT_RE.exec(line)) !== null) {
const name = (" " + result[0].substr(1).trim()).substr(1);
state.startObject(name);
} else if (MATERIAL_USE_RE.test(line)) {
state.object.startMaterial(line.substring(7).trim(), state.materialLibraries);
} else if (MATERIAL_RE.test(line)) {
state.materialLibraries.push(line.substring(7).trim());
} else if (lineFirstChar === "s") {
result = line.split(" ");
if (result.length > 1) {
const value = result[1].trim().toLowerCase();
state.object.smooth = value !== "0" && value !== "off";
} else {
state.object.smooth = true;
}
const material = state.object.currentMaterial();
if (material)
material.smooth = state.object.smooth;
} else {
if (line === "\0")
continue;
throw new Error(`Unexpected line: "${line}"`);
}
}
state.finalize();
const meshes = [];
const materials = [];
for (const object of state.objects) {
const { geometry } = object;
if (geometry.vertices.length === 0)
continue;
const mesh = {
header: {
vertexCount: geometry.vertices.length / 3
},
attributes: {}
};
switch (geometry.type) {
case "Points":
mesh.mode = 0;
break;
case "Line":
mesh.mode = 1;
break;
default:
mesh.mode = 4;
break;
}
mesh.attributes.POSITION = { value: new Float32Array(geometry.vertices), size: 3 };
if (geometry.normals.length > 0) {
mesh.attributes.NORMAL = { value: new Float32Array(geometry.normals), size: 3 };
}
if (geometry.colors.length > 0) {
mesh.attributes.COLOR_0 = { value: new Float32Array(geometry.colors), size: 3 };
}
if (geometry.uvs.length > 0) {
mesh.attributes.TEXCOORD_0 = { value: new Float32Array(geometry.uvs), size: 2 };
}
mesh.materials = [];
for (const sourceMaterial of object.materials) {
const _material = {
name: sourceMaterial.name,
flatShading: !sourceMaterial.smooth
};
mesh.materials.push(_material);
materials.push(_material);
}
mesh.name = object.name;
meshes.push(mesh);
}
return { meshes, materials };
}
// src/lib/get-obj-schema.ts
function getOBJSchema(attributes, metadata = {}) {
const stringMetadata = {};
for (const key in metadata) {
if (key !== "value") {
stringMetadata[key] = JSON.stringify(metadata[key]);
}
}
const fields = [];
for (const attributeName in attributes) {
const attribute = attributes[attributeName];
const field = getFieldFromAttribute(attributeName, attribute);
fields.push(field);
}
return { fields, metadata: stringMetadata };
}
function getFieldFromAttribute(name, attribute) {
const metadata = {};
for (const key in attribute) {
if (key !== "value") {
metadata[key] = JSON.stringify(attribute[key]);
}
}
let { type } = getDataTypeFromArray(attribute.value);
const isSingleValue = attribute.size === 1 || attribute.size === void 0;
if (!isSingleValue) {
type = { type: "fixed-size-list", listSize: attribute.size, children: [{ name: "values", type }] };
}
return { name, type, nullable: false, metadata };
}
// src/lib/parse-obj.ts
function parseOBJ(text, options) {
const { meshes } = parseOBJMeshes(text);
const vertexCount = meshes.reduce((s, mesh) => s + mesh.header.vertexCount, 0);
const attributes = mergeAttributes(meshes, vertexCount);
const header = {
vertexCount,
// @ts-ignore Need to export Attributes type
boundingBox: getMeshBoundingBox(attributes)
};
const schema = getOBJSchema(attributes, {
mode: 4,
boundingBox: header.boundingBox
});
return {
// Data return by this loader implementation
loaderData: {
header: {}
},
// Normalised data
schema,
header,
mode: 4,
// TRIANGLES
topology: "point-list",
attributes
};
}
function mergeAttributes(meshes, vertexCount) {
const positions = new Float32Array(vertexCount * 3);
let normals;
let colors;
let uvs;
let i = 0;
for (const mesh of meshes) {
const { POSITION, NORMAL, COLOR_0, TEXCOORD_0 } = mesh.attributes;
positions.set(POSITION.value, i * 3);
if (NORMAL) {
normals = normals || new Float32Array(vertexCount * 3);
normals.set(NORMAL.value, i * 3);
}
if (COLOR_0) {
colors = colors || new Float32Array(vertexCount * 3);
colors.set(COLOR_0.value, i * 3);
}
if (TEXCOORD_0) {
uvs = uvs || new Float32Array(vertexCount * 2);
uvs.set(TEXCOORD_0.value, i * 2);
}
i += POSITION.value.length / 3;
}
const attributes = {};
attributes.POSITION = { value: positions, size: 3 };
if (normals) {
attributes.NORMAL = { value: normals, size: 3 };
}
if (colors) {
attributes.COLOR_0 = { value: colors, size: 3 };
}
if (uvs) {
attributes.TEXCOORD_0 = { value: uvs, size: 2 };
}
return attributes;
}
// src/obj-loader.ts
var VERSION = true ? "4.4.5" : "latest";
var OBJWorkerLoader = {
...OBJFormat,
dataType: null,
batchType: null,
version: VERSION,
worker: true,
testText: testOBJFile,
options: {
obj: {}
}
};
function testOBJFile(text) {
return text[0] === "v";
}
var OBJLoader = {
...OBJWorkerLoader,
parse: async (arrayBuffer, options) => parseOBJ(new TextDecoder().decode(arrayBuffer), options),
parseTextSync: (text, options) => parseOBJ(text, options)
};
// src/workers/obj-worker.ts
createLoaderWorker(OBJLoader);
})();