@loaders.gl/draco
Version:
Framework-independent loader and writer for Draco compressed meshes and point clouds
686 lines (674 loc) • 24.2 kB
JavaScript
"use strict";
(() => {
// ../worker-utils/src/lib/env-utils/version.ts
function getVersion() {
if (!globalThis._loadersgl_?.version) {
globalThis._loadersgl_ = globalThis._loadersgl_ || {};
if (false) {
console.warn(
"loaders.gl: The __VERSION__ variable is not injected using babel plugin. Latest unstable workers would be fetched from the CDN."
);
globalThis._loadersgl_.version = NPM_TAG;
} else {
globalThis._loadersgl_.version = "4.3.2";
}
}
return globalThis._loadersgl_.version;
}
var VERSION = getVersion();
// ../worker-utils/src/lib/env-utils/assert.ts
function assert(condition, message) {
if (!condition) {
throw new Error(message || "loaders.gl assertion failed.");
}
}
// ../worker-utils/src/lib/env-utils/globals.ts
var globals = {
self: typeof self !== "undefined" && self,
window: typeof window !== "undefined" && window,
global: typeof global !== "undefined" && global,
document: typeof document !== "undefined" && document
};
var self_ = globals.self || globals.window || globals.global || {};
var window_ = globals.window || globals.self || globals.global || {};
var global_ = globals.global || globals.self || globals.window || {};
var document_ = globals.document || {};
var isBrowser = (
// @ts-ignore process.browser
typeof process !== "object" || String(process) !== "[object process]" || process.browser
);
var isWorker = typeof importScripts === "function";
var isMobile = typeof window !== "undefined" && typeof window.orientation !== "undefined";
var matches = typeof process !== "undefined" && process.version && /v([0-9]*)/.exec(process.version);
var nodeVersion = matches && parseFloat(matches[1]) || 0;
// ../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");
}
// ../worker-utils/src/lib/library-utils/library-utils.ts
var loadLibraryPromises = {};
async function loadLibrary(libraryUrl, moduleName = null, options = {}, libraryName = null) {
if (moduleName) {
libraryUrl = getLibraryUrl(libraryUrl, moduleName, options, libraryName);
}
loadLibraryPromises[libraryUrl] = // eslint-disable-next-line @typescript-eslint/no-misused-promises
loadLibraryPromises[libraryUrl] || loadLibraryFromFile(libraryUrl);
return await loadLibraryPromises[libraryUrl];
}
function getLibraryUrl(library, moduleName, options = {}, libraryName = null) {
if (!options.useLocalLibraries && library.startsWith("http")) {
return library;
}
libraryName = libraryName || library;
const modules = options.modules || {};
if (modules[libraryName]) {
return modules[libraryName];
}
if (!isBrowser) {
return `modules/${moduleName}/dist/libs/${libraryName}`;
}
if (options.CDN) {
assert(options.CDN.startsWith("http"));
return `${options.CDN}/${moduleName}@${VERSION}/dist/libs/${libraryName}`;
}
if (isWorker) {
return `../src/libs/${libraryName}`;
}
return `modules/${moduleName}/src/libs/${libraryName}`;
}
async function loadLibraryFromFile(libraryUrl) {
if (libraryUrl.endsWith("wasm")) {
return await loadAsArrayBuffer(libraryUrl);
}
if (!isBrowser) {
try {
const { requireFromFile } = globalThis.loaders || {};
return await requireFromFile?.(libraryUrl);
} catch (error) {
console.error(error);
return null;
}
}
if (isWorker) {
return importScripts(libraryUrl);
}
const scriptSource = await loadAsText(libraryUrl);
return loadLibraryFromString(scriptSource, libraryUrl);
}
function loadLibraryFromString(scriptSource, id) {
if (!isBrowser) {
const { requireFromString } = globalThis.loaders || {};
return requireFromString?.(scriptSource, id);
}
if (isWorker) {
eval.call(globalThis, scriptSource);
return null;
}
const script = document.createElement("script");
script.id = id;
try {
script.appendChild(document.createTextNode(scriptSource));
} catch (e) {
script.text = scriptSource;
}
document.body.appendChild(script);
return null;
}
async function loadAsArrayBuffer(url) {
const { readFileAsArrayBuffer } = globalThis.loaders || {};
if (isBrowser || !readFileAsArrayBuffer || url.startsWith("http")) {
const response = await fetch(url);
return await response.arrayBuffer();
}
return await readFileAsArrayBuffer(url);
}
async function loadAsText(url) {
const { readFileAsText } = globalThis.loaders || {};
if (isBrowser || !readFileAsText || url.startsWith("http")) {
const response = await fetch(url);
return await response.text();
}
return await readFileAsText(url);
}
// src/lib/draco-builder.ts
var GLTF_TO_DRACO_ATTRIBUTE_NAME_MAP = {
POSITION: "POSITION",
NORMAL: "NORMAL",
COLOR_0: "COLOR",
TEXCOORD_0: "TEX_COORD"
};
var noop = () => {
};
var DracoBuilder = class {
draco;
dracoEncoder;
dracoMeshBuilder;
dracoMetadataBuilder;
log;
// draco - the draco decoder, either import `draco3d` or load dynamically
constructor(draco) {
this.draco = draco;
this.dracoEncoder = new this.draco.Encoder();
this.dracoMeshBuilder = new this.draco.MeshBuilder();
this.dracoMetadataBuilder = new this.draco.MetadataBuilder();
}
destroy() {
this.destroyEncodedObject(this.dracoMeshBuilder);
this.destroyEncodedObject(this.dracoEncoder);
this.destroyEncodedObject(this.dracoMetadataBuilder);
this.dracoMeshBuilder = null;
this.dracoEncoder = null;
this.draco = null;
}
// TBD - when does this need to be called?
destroyEncodedObject(object) {
if (object) {
this.draco.destroy(object);
}
}
/**
* Encode mesh or point cloud
* @param mesh =({})
* @param options
*/
encodeSync(mesh, options = {}) {
this.log = noop;
this._setOptions(options);
return options.pointcloud ? this._encodePointCloud(mesh, options) : this._encodeMesh(mesh, options);
}
// PRIVATE
_getAttributesFromMesh(mesh) {
const attributes = { ...mesh, ...mesh.attributes };
if (mesh.indices) {
attributes.indices = mesh.indices;
}
return attributes;
}
_encodePointCloud(pointcloud, options) {
const dracoPointCloud = new this.draco.PointCloud();
if (options.metadata) {
this._addGeometryMetadata(dracoPointCloud, options.metadata);
}
const attributes = this._getAttributesFromMesh(pointcloud);
this._createDracoPointCloud(dracoPointCloud, attributes, options);
const dracoData = new this.draco.DracoInt8Array();
try {
const encodedLen = this.dracoEncoder.EncodePointCloudToDracoBuffer(
dracoPointCloud,
false,
dracoData
);
if (!(encodedLen > 0)) {
throw new Error("Draco encoding failed.");
}
this.log(`DRACO encoded ${dracoPointCloud.num_points()} points
with ${dracoPointCloud.num_attributes()} attributes into ${encodedLen} bytes`);
return dracoInt8ArrayToArrayBuffer(dracoData);
} finally {
this.destroyEncodedObject(dracoData);
this.destroyEncodedObject(dracoPointCloud);
}
}
_encodeMesh(mesh, options) {
const dracoMesh = new this.draco.Mesh();
if (options.metadata) {
this._addGeometryMetadata(dracoMesh, options.metadata);
}
const attributes = this._getAttributesFromMesh(mesh);
this._createDracoMesh(dracoMesh, attributes, options);
const dracoData = new this.draco.DracoInt8Array();
try {
const encodedLen = this.dracoEncoder.EncodeMeshToDracoBuffer(dracoMesh, dracoData);
if (encodedLen <= 0) {
throw new Error("Draco encoding failed.");
}
this.log(`DRACO encoded ${dracoMesh.num_points()} points
with ${dracoMesh.num_attributes()} attributes into ${encodedLen} bytes`);
return dracoInt8ArrayToArrayBuffer(dracoData);
} finally {
this.destroyEncodedObject(dracoData);
this.destroyEncodedObject(dracoMesh);
}
}
/**
* Set encoding options.
* @param {{speed?: any; method?: any; quantization?: any;}} options
*/
_setOptions(options) {
if ("speed" in options) {
this.dracoEncoder.SetSpeedOptions(...options.speed);
}
if ("method" in options) {
const dracoMethod = this.draco[options.method || "MESH_SEQUENTIAL_ENCODING"];
this.dracoEncoder.SetEncodingMethod(dracoMethod);
}
if ("quantization" in options) {
for (const attribute in options.quantization) {
const bits = options.quantization[attribute];
const dracoPosition = this.draco[attribute];
this.dracoEncoder.SetAttributeQuantization(dracoPosition, bits);
}
}
}
/**
* @param {Mesh} dracoMesh
* @param {object} attributes
* @returns {Mesh}
*/
_createDracoMesh(dracoMesh, attributes, options) {
const optionalMetadata = options.attributesMetadata || {};
try {
const positions = this._getPositionAttribute(attributes);
if (!positions) {
throw new Error("positions");
}
const vertexCount = positions.length / 3;
for (let attributeName in attributes) {
const attribute = attributes[attributeName];
attributeName = GLTF_TO_DRACO_ATTRIBUTE_NAME_MAP[attributeName] || attributeName;
const uniqueId = this._addAttributeToMesh(dracoMesh, attributeName, attribute, vertexCount);
if (uniqueId !== -1) {
this._addAttributeMetadata(dracoMesh, uniqueId, {
name: attributeName,
...optionalMetadata[attributeName] || {}
});
}
}
} catch (error) {
this.destroyEncodedObject(dracoMesh);
throw error;
}
return dracoMesh;
}
/**
* @param {} dracoPointCloud
* @param {object} attributes
*/
_createDracoPointCloud(dracoPointCloud, attributes, options) {
const optionalMetadata = options.attributesMetadata || {};
try {
const positions = this._getPositionAttribute(attributes);
if (!positions) {
throw new Error("positions");
}
const vertexCount = positions.length / 3;
for (let attributeName in attributes) {
const attribute = attributes[attributeName];
attributeName = GLTF_TO_DRACO_ATTRIBUTE_NAME_MAP[attributeName] || attributeName;
const uniqueId = this._addAttributeToMesh(
dracoPointCloud,
attributeName,
attribute,
vertexCount
);
if (uniqueId !== -1) {
this._addAttributeMetadata(dracoPointCloud, uniqueId, {
name: attributeName,
...optionalMetadata[attributeName] || {}
});
}
}
} catch (error) {
this.destroyEncodedObject(dracoPointCloud);
throw error;
}
return dracoPointCloud;
}
/**
* @param mesh
* @param attributeName
* @param attribute
* @param vertexCount
*/
_addAttributeToMesh(mesh, attributeName, attribute, vertexCount) {
if (!ArrayBuffer.isView(attribute)) {
return -1;
}
const type = this._getDracoAttributeType(attributeName);
const size = attribute.length / vertexCount;
if (type === "indices") {
const numFaces = attribute.length / 3;
this.log(`Adding attribute ${attributeName}, size ${numFaces}`);
this.dracoMeshBuilder.AddFacesToMesh(mesh, numFaces, attribute);
return -1;
}
this.log(`Adding attribute ${attributeName}, size ${size}`);
const builder = this.dracoMeshBuilder;
const { buffer } = attribute;
switch (attribute.constructor) {
case Int8Array:
return builder.AddInt8Attribute(mesh, type, vertexCount, size, new Int8Array(buffer));
case Int16Array:
return builder.AddInt16Attribute(mesh, type, vertexCount, size, new Int16Array(buffer));
case Int32Array:
return builder.AddInt32Attribute(mesh, type, vertexCount, size, new Int32Array(buffer));
case Uint8Array:
case Uint8ClampedArray:
return builder.AddUInt8Attribute(mesh, type, vertexCount, size, new Uint8Array(buffer));
case Uint16Array:
return builder.AddUInt16Attribute(mesh, type, vertexCount, size, new Uint16Array(buffer));
case Uint32Array:
return builder.AddUInt32Attribute(mesh, type, vertexCount, size, new Uint32Array(buffer));
case Float32Array:
return builder.AddFloatAttribute(mesh, type, vertexCount, size, new Float32Array(buffer));
default:
console.warn("Unsupported attribute type", attribute);
return -1;
}
}
/**
* DRACO can compress attributes of know type better
* TODO - expose an attribute type map?
* @param attributeName
*/
_getDracoAttributeType(attributeName) {
switch (attributeName.toLowerCase()) {
case "indices":
return "indices";
case "position":
case "positions":
case "vertices":
return this.draco.POSITION;
case "normal":
case "normals":
return this.draco.NORMAL;
case "color":
case "colors":
return this.draco.COLOR;
case "texcoord":
case "texcoords":
return this.draco.TEX_COORD;
default:
return this.draco.GENERIC;
}
}
_getPositionAttribute(attributes) {
for (const attributeName in attributes) {
const attribute = attributes[attributeName];
const dracoType = this._getDracoAttributeType(attributeName);
if (dracoType === this.draco.POSITION) {
return attribute;
}
}
return null;
}
/**
* Add metadata for the geometry.
* @param dracoGeometry - WASM Draco Object
* @param metadata
*/
_addGeometryMetadata(dracoGeometry, metadata) {
const dracoMetadata = new this.draco.Metadata();
this._populateDracoMetadata(dracoMetadata, metadata);
this.dracoMeshBuilder.AddMetadata(dracoGeometry, dracoMetadata);
}
/**
* Add metadata for an attribute to geometry.
* @param dracoGeometry - WASM Draco Object
* @param uniqueAttributeId
* @param metadata
*/
_addAttributeMetadata(dracoGeometry, uniqueAttributeId, metadata) {
const dracoAttributeMetadata = new this.draco.Metadata();
this._populateDracoMetadata(dracoAttributeMetadata, metadata);
this.dracoMeshBuilder.SetMetadataForAttribute(
dracoGeometry,
uniqueAttributeId,
dracoAttributeMetadata
);
}
/**
* Add contents of object or map to a WASM Draco Metadata Object
* @param dracoMetadata - WASM Draco Object
* @param metadata
*/
_populateDracoMetadata(dracoMetadata, metadata) {
for (const [key, value] of getEntries(metadata)) {
switch (typeof value) {
case "number":
if (Math.trunc(value) === value) {
this.dracoMetadataBuilder.AddIntEntry(dracoMetadata, key, value);
} else {
this.dracoMetadataBuilder.AddDoubleEntry(dracoMetadata, key, value);
}
break;
case "object":
if (value instanceof Int32Array) {
this.dracoMetadataBuilder.AddIntEntryArray(dracoMetadata, key, value, value.length);
}
break;
case "string":
default:
this.dracoMetadataBuilder.AddStringEntry(dracoMetadata, key, value);
}
}
}
};
function dracoInt8ArrayToArrayBuffer(dracoData) {
const byteLength = dracoData.size();
const outputBuffer = new ArrayBuffer(byteLength);
const outputData = new Int8Array(outputBuffer);
for (let i = 0; i < byteLength; ++i) {
outputData[i] = dracoData.GetValue(i);
}
return outputBuffer;
}
function getEntries(container) {
const hasEntriesFunc = container.entries && !container.hasOwnProperty("entries");
return hasEntriesFunc ? container.entries() : Object.entries(container);
}
// src/lib/draco-module-loader.ts
var DRACO_DECODER_VERSION = "1.5.6";
var DRACO_ENCODER_VERSION = "1.4.1";
var STATIC_DECODER_URL = `https://www.gstatic.com/draco/versioned/decoders/${DRACO_DECODER_VERSION}`;
var DRACO_EXTERNAL_LIBRARIES = {
/** The primary Draco3D encoder, javascript wrapper part */
DECODER: "draco_wasm_wrapper.js",
/** The primary draco decoder, compiled web assembly part */
DECODER_WASM: "draco_decoder.wasm",
/** Fallback decoder for non-webassebly environments. Very big bundle, lower performance */
FALLBACK_DECODER: "draco_decoder.js",
/** Draco encoder */
ENCODER: "draco_encoder.js"
};
var DRACO_EXTERNAL_LIBRARY_URLS = {
[DRACO_EXTERNAL_LIBRARIES.DECODER]: `${STATIC_DECODER_URL}/${DRACO_EXTERNAL_LIBRARIES.DECODER}`,
[DRACO_EXTERNAL_LIBRARIES.DECODER_WASM]: `${STATIC_DECODER_URL}/${DRACO_EXTERNAL_LIBRARIES.DECODER_WASM}`,
[DRACO_EXTERNAL_LIBRARIES.FALLBACK_DECODER]: `${STATIC_DECODER_URL}/${DRACO_EXTERNAL_LIBRARIES.FALLBACK_DECODER}`,
[DRACO_EXTERNAL_LIBRARIES.ENCODER]: `https://raw.githubusercontent.com/google/draco/${DRACO_ENCODER_VERSION}/javascript/${DRACO_EXTERNAL_LIBRARIES.ENCODER}`
};
var loadEncoderPromise;
async function loadDracoEncoderModule(options) {
const modules = options.modules || {};
if (modules.draco3d) {
loadEncoderPromise ||= modules.draco3d.createEncoderModule({}).then((draco) => {
return { draco };
});
} else {
loadEncoderPromise ||= loadDracoEncoder(options);
}
return await loadEncoderPromise;
}
async function loadDracoEncoder(options) {
let DracoEncoderModule = await loadLibrary(
DRACO_EXTERNAL_LIBRARY_URLS[DRACO_EXTERNAL_LIBRARIES.ENCODER],
"draco",
options,
DRACO_EXTERNAL_LIBRARIES.ENCODER
);
DracoEncoderModule = DracoEncoderModule || globalThis.DracoEncoderModule;
return new Promise((resolve) => {
DracoEncoderModule({
onModuleLoaded: (draco) => resolve({ draco })
// Module is Promise-like. Wrap in object to avoid loop.
});
});
}
// src/lib/utils/version.ts
var VERSION2 = true ? "4.3.2" : "latest";
// src/draco-writer.ts
var DEFAULT_DRACO_WRITER_OPTIONS = {
pointcloud: false,
// Set to true if pointcloud (mode: 0, no indices)
attributeNameEntry: "name"
// Draco Compression Parameters
// method: 'MESH_EDGEBREAKER_ENCODING', // Use draco defaults
// speed: [5, 5], // Use draco defaults
// quantization: { // Use draco defaults
// POSITION: 10
// }
};
var DracoWriter = {
name: "DRACO",
id: "draco",
module: "draco",
version: VERSION2,
extensions: ["drc"],
options: {
draco: DEFAULT_DRACO_WRITER_OPTIONS
},
encode
};
async function encode(data, options = {}) {
const { draco } = await loadDracoEncoderModule(options);
const dracoBuilder = new DracoBuilder(draco);
try {
return dracoBuilder.encodeSync(data, options.draco);
} finally {
dracoBuilder.destroy();
}
}
// src/workers/draco-writer-worker.ts
(async () => {
if (!await WorkerBody.inWorkerThread()) {
return;
}
WorkerBody.onmessage = async (type, payload) => {
switch (type) {
case "process":
try {
const { input, options } = payload;
const result = await DracoWriter.encode(input, options);
WorkerBody.postMessage("done", { result });
} catch (error) {
const message = error instanceof Error ? error.message : "";
WorkerBody.postMessage("error", { error: message });
}
break;
default:
}
};
})();
})();
//# sourceMappingURL=draco-writer-worker.js.map