UNPKG

@gltf-transform/core

Version:

glTF 2.0 SDK for JavaScript and TypeScript, on Web and Node.js.

1,570 lines (1,530 loc) 230 kB
import { GraphNode, $attributes, GraphEdge, $immutableKeys, RefList, RefSet, RefMap, Graph } from 'property-graph'; export { Graph, GraphEdge, RefList, RefMap, RefSet } from 'property-graph'; /** * Current version of the package. * @hidden */ const VERSION = `v${"4.2.1"}`; /** @hidden */ const GLB_BUFFER = '@glb.bin'; /** String IDs for core {@link Property} types. */ var PropertyType; (function (PropertyType) { PropertyType["ACCESSOR"] = "Accessor"; PropertyType["ANIMATION"] = "Animation"; PropertyType["ANIMATION_CHANNEL"] = "AnimationChannel"; PropertyType["ANIMATION_SAMPLER"] = "AnimationSampler"; PropertyType["BUFFER"] = "Buffer"; PropertyType["CAMERA"] = "Camera"; PropertyType["MATERIAL"] = "Material"; PropertyType["MESH"] = "Mesh"; PropertyType["PRIMITIVE"] = "Primitive"; PropertyType["PRIMITIVE_TARGET"] = "PrimitiveTarget"; PropertyType["NODE"] = "Node"; PropertyType["ROOT"] = "Root"; PropertyType["SCENE"] = "Scene"; PropertyType["SKIN"] = "Skin"; PropertyType["TEXTURE"] = "Texture"; PropertyType["TEXTURE_INFO"] = "TextureInfo"; })(PropertyType || (PropertyType = {})); /** Vertex layout method. */ var VertexLayout; (function (VertexLayout) { /** * Stores vertex attributes in a single buffer view per mesh primitive. Interleaving vertex * data may improve performance by reducing page-thrashing in GPU memory. */ VertexLayout["INTERLEAVED"] = "interleaved"; /** * Stores each vertex attribute in a separate buffer view. May decrease performance by causing * page-thrashing in GPU memory. Some 3D engines may prefer this layout, e.g. for simplicity. */ VertexLayout["SEPARATE"] = "separate"; })(VertexLayout || (VertexLayout = {})); /** Accessor usage. */ var BufferViewUsage$1; (function (BufferViewUsage) { BufferViewUsage["ARRAY_BUFFER"] = "ARRAY_BUFFER"; BufferViewUsage["ELEMENT_ARRAY_BUFFER"] = "ELEMENT_ARRAY_BUFFER"; BufferViewUsage["INVERSE_BIND_MATRICES"] = "INVERSE_BIND_MATRICES"; BufferViewUsage["OTHER"] = "OTHER"; BufferViewUsage["SPARSE"] = "SPARSE"; })(BufferViewUsage$1 || (BufferViewUsage$1 = {})); /** Texture channels. */ var TextureChannel; (function (TextureChannel) { TextureChannel[TextureChannel["R"] = 4096] = "R"; TextureChannel[TextureChannel["G"] = 256] = "G"; TextureChannel[TextureChannel["B"] = 16] = "B"; TextureChannel[TextureChannel["A"] = 1] = "A"; })(TextureChannel || (TextureChannel = {})); var Format; (function (Format) { Format["GLTF"] = "GLTF"; Format["GLB"] = "GLB"; })(Format || (Format = {})); const ComponentTypeToTypedArray = { '5120': Int8Array, '5121': Uint8Array, '5122': Int16Array, '5123': Uint16Array, '5125': Uint32Array, '5126': Float32Array }; /** * *Common utilities for working with Uint8Array and Buffer objects.* * * @category Utilities */ class BufferUtils { /** Creates a byte array from a Data URI. */ static createBufferFromDataURI(dataURI) { if (typeof Buffer === 'undefined') { // Browser. const byteString = atob(dataURI.split(',')[1]); const ia = new Uint8Array(byteString.length); for (let i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); } return ia; } else { // Node.js. const data = dataURI.split(',')[1]; const isBase64 = dataURI.indexOf('base64') >= 0; return Buffer.from(data, isBase64 ? 'base64' : 'utf8'); } } /** Encodes text to a byte array. */ static encodeText(text) { return new TextEncoder().encode(text); } /** Decodes a byte array to text. */ static decodeText(array) { return new TextDecoder().decode(array); } /** * Concatenates N byte arrays. */ static concat(arrays) { let totalByteLength = 0; for (const array of arrays) { totalByteLength += array.byteLength; } const result = new Uint8Array(totalByteLength); let byteOffset = 0; for (const array of arrays) { result.set(array, byteOffset); byteOffset += array.byteLength; } return result; } /** * Pads a Uint8Array to the next 4-byte boundary. * * Reference: [glTF → Data Alignment](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#data-alignment) */ static pad(srcArray, paddingByte = 0) { const paddedLength = this.padNumber(srcArray.byteLength); if (paddedLength === srcArray.byteLength) return srcArray; const dstArray = new Uint8Array(paddedLength); dstArray.set(srcArray); if (paddingByte !== 0) { for (let i = srcArray.byteLength; i < paddedLength; i++) { dstArray[i] = paddingByte; } } return dstArray; } /** Pads a number to 4-byte boundaries. */ static padNumber(v) { return Math.ceil(v / 4) * 4; } /** Returns true if given byte array instances are equal. */ static equals(a, b) { if (a === b) return true; if (a.byteLength !== b.byteLength) return false; let i = a.byteLength; while (i--) { if (a[i] !== b[i]) return false; } return true; } /** * Returns a Uint8Array view of a typed array, with the same underlying ArrayBuffer. * * A shorthand for: * * ```js * const buffer = new Uint8Array( * array.buffer, * array.byteOffset + byteOffset, * Math.min(array.byteLength, byteLength) * ); * ``` * */ static toView(a, byteOffset = 0, byteLength = Infinity) { return new Uint8Array(a.buffer, a.byteOffset + byteOffset, Math.min(a.byteLength, byteLength)); } static assertView(view) { if (view && !ArrayBuffer.isView(view)) { throw new Error(`Method requires Uint8Array parameter; received "${typeof view}".`); } return view; } } /** * *Common utilities for working with colors in vec3, vec4, or hexadecimal form.* * * Provides methods to convert linear components (vec3, vec4) to sRGB hex values. All colors in * the glTF specification, excluding color textures, are linear. Hexadecimal values, in sRGB * colorspace, are accessible through helper functions in the API as a convenience. * * ```typescript * // Hex (sRGB) to factor (linear). * const factor = ColorUtils.hexToFactor(0xFFCCCC, []); * * // Factor (linear) to hex (sRGB). * const hex = ColorUtils.factorToHex([1, .25, .25]) * ``` * * @category Utilities */ class ColorUtils { /** * Converts sRGB hexadecimal to linear components. * @typeParam T vec3 or vec4 linear components. */ static hexToFactor(hex, target) { hex = Math.floor(hex); const _target = target; _target[0] = (hex >> 16 & 255) / 255; _target[1] = (hex >> 8 & 255) / 255; _target[2] = (hex & 255) / 255; return this.convertSRGBToLinear(target, target); } /** * Converts linear components to sRGB hexadecimal. * @typeParam T vec3 or vec4 linear components. */ static factorToHex(factor) { const target = [...factor]; const [r, g, b] = this.convertLinearToSRGB(factor, target); return r * 255 << 16 ^ g * 255 << 8 ^ b * 255 << 0; } /** * Converts sRGB components to linear components. * @typeParam T vec3 or vec4 linear components. */ static convertSRGBToLinear(source, target) { const _source = source; const _target = target; for (let i = 0; i < 3; i++) { _target[i] = _source[i] < 0.04045 ? _source[i] * 0.0773993808 : Math.pow(_source[i] * 0.9478672986 + 0.0521327014, 2.4); } return target; } /** * Converts linear components to sRGB components. * @typeParam T vec3 or vec4 linear components. */ static convertLinearToSRGB(source, target) { const _source = source; const _target = target; for (let i = 0; i < 3; i++) { _target[i] = _source[i] < 0.0031308 ? _source[i] * 12.92 : 1.055 * Math.pow(_source[i], 0.41666) - 0.055; } return target; } } /** JPEG image support. */ class JPEGImageUtils { match(array) { return array.length >= 3 && array[0] === 255 && array[1] === 216 && array[2] === 255; } getSize(array) { // Skip 4 chars, they are for signature let view = new DataView(array.buffer, array.byteOffset + 4); let i, next; while (view.byteLength) { // read length of the next block i = view.getUint16(0, false); // i = buffer.readUInt16BE(0); // ensure correct format validateJPEGBuffer(view, i); // 0xFFC0 is baseline standard(SOF) // 0xFFC1 is baseline optimized(SOF) // 0xFFC2 is progressive(SOF2) next = view.getUint8(i + 1); if (next === 0xc0 || next === 0xc1 || next === 0xc2) { return [view.getUint16(i + 7, false), view.getUint16(i + 5, false)]; } // move to the next block view = new DataView(array.buffer, view.byteOffset + i + 2); } throw new TypeError('Invalid JPG, no size found'); } getChannels(_buffer) { return 3; } } /** * PNG image support. * * PNG signature: 'PNG\r\n\x1a\n' * PNG image header chunk name: 'IHDR' */ class PNGImageUtils { match(array) { return array.length >= 8 && array[0] === 0x89 && array[1] === 0x50 && array[2] === 0x4e && array[3] === 0x47 && array[4] === 0x0d && array[5] === 0x0a && array[6] === 0x1a && array[7] === 0x0a; } getSize(array) { const view = new DataView(array.buffer, array.byteOffset); const magic = BufferUtils.decodeText(array.slice(12, 16)); if (magic === PNGImageUtils.PNG_FRIED_CHUNK_NAME) { return [view.getUint32(32, false), view.getUint32(36, false)]; } return [view.getUint32(16, false), view.getUint32(20, false)]; } getChannels(_buffer) { return 4; } } /** * *Common utilities for working with image data.* * * @category Utilities */ // Used to detect "fried" png's: http://www.jongware.com/pngdefry.html PNGImageUtils.PNG_FRIED_CHUNK_NAME = 'CgBI'; class ImageUtils { /** Registers support for a new image format; useful for certain extensions. */ static registerFormat(mimeType, impl) { this.impls[mimeType] = impl; } /** * Returns detected MIME type of the given image buffer. Note that for image * formats with support provided by extensions, the extension must be * registered with an I/O class before it can be detected by ImageUtils. */ static getMimeType(buffer) { for (const mimeType in this.impls) { if (this.impls[mimeType].match(buffer)) { return mimeType; } } return null; } /** Returns the dimensions of the image. */ static getSize(buffer, mimeType) { if (!this.impls[mimeType]) return null; return this.impls[mimeType].getSize(buffer); } /** * Returns a conservative estimate of the number of channels in the image. For some image * formats, the method may return 4 indicating the possibility of an alpha channel, without * the ability to guarantee that an alpha channel is present. */ static getChannels(buffer, mimeType) { if (!this.impls[mimeType]) return null; return this.impls[mimeType].getChannels(buffer); } /** Returns a conservative estimate of the GPU memory required by this image. */ static getVRAMByteLength(buffer, mimeType) { if (!this.impls[mimeType]) return null; if (this.impls[mimeType].getVRAMByteLength) { return this.impls[mimeType].getVRAMByteLength(buffer); } let uncompressedBytes = 0; const channels = 4; // See https://github.com/donmccurdy/glTF-Transform/issues/151. const resolution = this.getSize(buffer, mimeType); if (!resolution) return null; while (resolution[0] > 1 || resolution[1] > 1) { uncompressedBytes += resolution[0] * resolution[1] * channels; resolution[0] = Math.max(Math.floor(resolution[0] / 2), 1); resolution[1] = Math.max(Math.floor(resolution[1] / 2), 1); } uncompressedBytes += 1 * 1 * channels; return uncompressedBytes; } /** Returns the preferred file extension for the given MIME type. */ static mimeTypeToExtension(mimeType) { if (mimeType === 'image/jpeg') return 'jpg'; return mimeType.split('/').pop(); } /** Returns the MIME type for the given file extension. */ static extensionToMimeType(extension) { if (extension === 'jpg') return 'image/jpeg'; if (!extension) return ''; return `image/${extension}`; } } ImageUtils.impls = { 'image/jpeg': new JPEGImageUtils(), 'image/png': new PNGImageUtils() }; function validateJPEGBuffer(view, i) { // index should be within buffer limits if (i > view.byteLength) { throw new TypeError('Corrupt JPG, exceeded buffer limits'); } // Every JPEG block must begin with a 0xFF if (view.getUint8(i) !== 0xff) { throw new TypeError('Invalid JPG, marker table corrupted'); } return view; } /** * *Utility class for working with file systems and URI paths.* * * @category Utilities */ class FileUtils { /** * Extracts the basename from a file path, e.g. "folder/model.glb" -> "model". * See: {@link HTTPUtils.basename} */ static basename(uri) { const fileName = uri.split(/[\\/]/).pop(); return fileName.substring(0, fileName.lastIndexOf('.')); } /** * Extracts the extension from a file path, e.g. "folder/model.glb" -> "glb". * See: {@link HTTPUtils.extension} */ static extension(uri) { if (uri.startsWith('data:image/')) { const mimeType = uri.match(/data:(image\/\w+)/)[1]; return ImageUtils.mimeTypeToExtension(mimeType); } else if (uri.startsWith('data:model/gltf+json')) { return 'gltf'; } else if (uri.startsWith('data:model/gltf-binary')) { return 'glb'; } else if (uri.startsWith('data:application/')) { return 'bin'; } return uri.split(/[\\/]/).pop().split(/[.]/).pop(); } } /** * Common utilities * @module glMatrix */ var ARRAY_TYPE = typeof Float32Array !== 'undefined' ? Float32Array : Array; if (!Math.hypot) Math.hypot = function () { var y = 0, i = arguments.length; while (i--) { y += arguments[i] * arguments[i]; } return Math.sqrt(y); }; /** * 3 Dimensional Vector * @module vec3 */ /** * Creates a new, empty vec3 * * @returns {vec3} a new 3D vector */ function create() { var out = new ARRAY_TYPE(3); if (ARRAY_TYPE != Float32Array) { out[0] = 0; out[1] = 0; out[2] = 0; } return out; } /** * Calculates the length of a vec3 * * @param {ReadonlyVec3} a vector to calculate length of * @returns {Number} length of a */ function length(a) { var x = a[0]; var y = a[1]; var z = a[2]; return Math.hypot(x, y, z); } /** * Transforms the vec3 with a mat4. * 4th vector component is implicitly '1' * * @param {vec3} out the receiving vector * @param {ReadonlyVec3} a the vector to transform * @param {ReadonlyMat4} m matrix to transform with * @returns {vec3} out */ function transformMat4(out, a, m) { var x = a[0], y = a[1], z = a[2]; var w = m[3] * x + m[7] * y + m[11] * z + m[15]; w = w || 1.0; out[0] = (m[0] * x + m[4] * y + m[8] * z + m[12]) / w; out[1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) / w; out[2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) / w; return out; } /** * Perform some operation over an array of vec3s. * * @param {Array} a the array of vectors to iterate over * @param {Number} stride Number of elements between the start of each vec3. If 0 assumes tightly packed * @param {Number} offset Number of elements to skip at the beginning of the array * @param {Number} count Number of vec3s to iterate over. If 0 iterates over entire array * @param {Function} fn Function to call for each vector in the array * @param {Object} [arg] additional argument to pass to fn * @returns {Array} a * @function */ (function () { var vec = create(); return function (a, stride, offset, count, fn, arg) { var i, l; if (!stride) { stride = 3; } if (!offset) { offset = 0; } if (count) { l = Math.min(count * stride + offset, a.length); } else { l = a.length; } for (i = offset; i < l; i += stride) { vec[0] = a[i]; vec[1] = a[i + 1]; vec[2] = a[i + 2]; fn(vec, vec, arg); a[i] = vec[0]; a[i + 1] = vec[1]; a[i + 2] = vec[2]; } return a; }; })(); /** @hidden Implemented in /core for use by /extensions, publicly exported from /functions. */ function getBounds(node) { const resultBounds = createBounds(); const parents = node.propertyType === PropertyType.NODE ? [node] : node.listChildren(); for (const parent of parents) { parent.traverse(node => { const mesh = node.getMesh(); if (!mesh) return; // Compute mesh bounds and update result. const meshBounds = getMeshBounds(mesh, node.getWorldMatrix()); if (meshBounds.min.every(isFinite) && meshBounds.max.every(isFinite)) { expandBounds(meshBounds.min, resultBounds); expandBounds(meshBounds.max, resultBounds); } }); } return resultBounds; } /** Computes mesh bounds in world space. */ function getMeshBounds(mesh, worldMatrix) { const meshBounds = createBounds(); // We can't transform a local AABB into world space and still have a tight AABB in world space, // so we need to compute the world AABB vertex by vertex here. for (const prim of mesh.listPrimitives()) { const position = prim.getAttribute('POSITION'); const indices = prim.getIndices(); if (!position) continue; let localPos = [0, 0, 0]; let worldPos = [0, 0, 0]; for (let i = 0, il = indices ? indices.getCount() : position.getCount(); i < il; i++) { const index = indices ? indices.getScalar(i) : i; localPos = position.getElement(index, localPos); worldPos = transformMat4(worldPos, localPos, worldMatrix); expandBounds(worldPos, meshBounds); } } return meshBounds; } /** Expands bounds of target by given source. */ function expandBounds(point, target) { for (let i = 0; i < 3; i++) { target.min[i] = Math.min(point[i], target.min[i]); target.max[i] = Math.max(point[i], target.max[i]); } } /** Creates new bounds with min=Infinity, max=-Infinity. */ function createBounds() { return { min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity] }; } // Need a placeholder domain to construct a URL from a relative path. We only // access `url.pathname`, so the domain doesn't matter. const NULL_DOMAIN = 'https://null.example'; /** * *Utility class for working with URLs.* * * @category Utilities */ class HTTPUtils { static dirname(path) { const index = path.lastIndexOf('/'); if (index === -1) return './'; return path.substring(0, index + 1); } /** * Extracts the basename from a URL, e.g. "folder/model.glb" -> "model". * See: {@link FileUtils.basename} */ static basename(uri) { return FileUtils.basename(new URL(uri, NULL_DOMAIN).pathname); } /** * Extracts the extension from a URL, e.g. "folder/model.glb" -> "glb". * See: {@link FileUtils.extension} */ static extension(uri) { return FileUtils.extension(new URL(uri, NULL_DOMAIN).pathname); } static resolve(base, path) { if (!this.isRelativePath(path)) return path; const stack = base.split('/'); const parts = path.split('/'); stack.pop(); for (let i = 0; i < parts.length; i++) { if (parts[i] === '.') continue; if (parts[i] === '..') { stack.pop(); } else { stack.push(parts[i]); } } return stack.join('/'); } /** * Returns true for URLs containing a protocol, and false for both * absolute and relative paths. */ static isAbsoluteURL(path) { return this.PROTOCOL_REGEXP.test(path); } /** * Returns true for paths that are declared relative to some unknown base * path. For example, "foo/bar/" is relative both "/foo/bar/" is not. */ static isRelativePath(path) { return !/^(?:[a-zA-Z]+:)?\//.test(path); } } HTTPUtils.DEFAULT_INIT = {}; HTTPUtils.PROTOCOL_REGEXP = /^[a-zA-Z]+:\/\//; // Reference: https://github.com/jonschlinkert/is-plain-object function isObject(o) { return Object.prototype.toString.call(o) === '[object Object]'; } function isPlainObject(o) { if (isObject(o) === false) return false; // If has modified constructor const ctor = o.constructor; if (ctor === undefined) return true; // If has modified prototype const prot = ctor.prototype; if (isObject(prot) === false) return false; // If constructor does not have an Object-specific method if (Object.hasOwn(prot, 'isPrototypeOf') === false) { return false; } // Most likely a plain Object return true; } var _Logger; /** Logger verbosity thresholds. */ var Verbosity; (function (Verbosity) { /** No events are logged. */ Verbosity[Verbosity["SILENT"] = 4] = "SILENT"; /** Only error events are logged. */ Verbosity[Verbosity["ERROR"] = 3] = "ERROR"; /** Only error and warn events are logged. */ Verbosity[Verbosity["WARN"] = 2] = "WARN"; /** Only error, warn, and info events are logged. (DEFAULT) */ Verbosity[Verbosity["INFO"] = 1] = "INFO"; /** All events are logged. */ Verbosity[Verbosity["DEBUG"] = 0] = "DEBUG"; })(Verbosity || (Verbosity = {})); /** * *Logger utility class.* * * @category Utilities */ class Logger { /** Constructs a new Logger instance. */ constructor(verbosity) { this.verbosity = void 0; this.verbosity = verbosity; } /** Logs an event at level {@link Logger.Verbosity.DEBUG}. */ debug(text) { if (this.verbosity <= Logger.Verbosity.DEBUG) { console.debug(text); } } /** Logs an event at level {@link Logger.Verbosity.INFO}. */ info(text) { if (this.verbosity <= Logger.Verbosity.INFO) { console.info(text); } } /** Logs an event at level {@link Logger.Verbosity.WARN}. */ warn(text) { if (this.verbosity <= Logger.Verbosity.WARN) { console.warn(text); } } /** Logs an event at level {@link Logger.Verbosity.ERROR}. */ error(text) { if (this.verbosity <= Logger.Verbosity.ERROR) { console.error(text); } } } _Logger = Logger; /** Logger verbosity thresholds. */ Logger.Verbosity = Verbosity; /** Default logger instance. */ Logger.DEFAULT_INSTANCE = new _Logger(_Logger.Verbosity.INFO); /** * Calculates the determinant of a mat4 * * @param {ReadonlyMat4} a the source matrix * @returns {Number} determinant of a */ function determinant(a) { var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]; var a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7]; var a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; var a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; var b00 = a00 * a11 - a01 * a10; var b01 = a00 * a12 - a02 * a10; var b02 = a00 * a13 - a03 * a10; var b03 = a01 * a12 - a02 * a11; var b04 = a01 * a13 - a03 * a11; var b05 = a02 * a13 - a03 * a12; var b06 = a20 * a31 - a21 * a30; var b07 = a20 * a32 - a22 * a30; var b08 = a20 * a33 - a23 * a30; var b09 = a21 * a32 - a22 * a31; var b10 = a21 * a33 - a23 * a31; var b11 = a22 * a33 - a23 * a32; // Calculate the determinant return b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; } /** * Multiplies two mat4s * * @param {mat4} out the receiving matrix * @param {ReadonlyMat4} a the first operand * @param {ReadonlyMat4} b the second operand * @returns {mat4} out */ function multiply(out, a, b) { var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]; var a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7]; var a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; var a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; // Cache only the current line of the second matrix var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; out[0] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; out[1] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; out[2] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; out[3] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7]; out[4] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; out[5] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; out[6] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; out[7] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11]; out[8] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; out[9] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; out[10] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; out[11] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15]; out[12] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; out[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; out[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; out[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; return out; } /** * Returns the scaling factor component of a transformation * matrix. If a matrix is built with fromRotationTranslationScale * with a normalized Quaternion paramter, the returned vector will be * the same as the scaling vector * originally supplied. * @param {vec3} out Vector to receive scaling factor component * @param {ReadonlyMat4} mat Matrix to be decomposed (input) * @return {vec3} out */ function getScaling(out, mat) { var m11 = mat[0]; var m12 = mat[1]; var m13 = mat[2]; var m21 = mat[4]; var m22 = mat[5]; var m23 = mat[6]; var m31 = mat[8]; var m32 = mat[9]; var m33 = mat[10]; out[0] = Math.hypot(m11, m12, m13); out[1] = Math.hypot(m21, m22, m23); out[2] = Math.hypot(m31, m32, m33); return out; } /** * Returns a quaternion representing the rotational component * of a transformation matrix. If a matrix is built with * fromRotationTranslation, the returned quaternion will be the * same as the quaternion originally supplied. * @param {quat} out Quaternion to receive the rotation component * @param {ReadonlyMat4} mat Matrix to be decomposed (input) * @return {quat} out */ function getRotation(out, mat) { var scaling = new ARRAY_TYPE(3); getScaling(scaling, mat); var is1 = 1 / scaling[0]; var is2 = 1 / scaling[1]; var is3 = 1 / scaling[2]; var sm11 = mat[0] * is1; var sm12 = mat[1] * is2; var sm13 = mat[2] * is3; var sm21 = mat[4] * is1; var sm22 = mat[5] * is2; var sm23 = mat[6] * is3; var sm31 = mat[8] * is1; var sm32 = mat[9] * is2; var sm33 = mat[10] * is3; var trace = sm11 + sm22 + sm33; var S = 0; if (trace > 0) { S = Math.sqrt(trace + 1.0) * 2; out[3] = 0.25 * S; out[0] = (sm23 - sm32) / S; out[1] = (sm31 - sm13) / S; out[2] = (sm12 - sm21) / S; } else if (sm11 > sm22 && sm11 > sm33) { S = Math.sqrt(1.0 + sm11 - sm22 - sm33) * 2; out[3] = (sm23 - sm32) / S; out[0] = 0.25 * S; out[1] = (sm12 + sm21) / S; out[2] = (sm31 + sm13) / S; } else if (sm22 > sm33) { S = Math.sqrt(1.0 + sm22 - sm11 - sm33) * 2; out[3] = (sm31 - sm13) / S; out[0] = (sm12 + sm21) / S; out[1] = 0.25 * S; out[2] = (sm23 + sm32) / S; } else { S = Math.sqrt(1.0 + sm33 - sm11 - sm22) * 2; out[3] = (sm12 - sm21) / S; out[0] = (sm31 + sm13) / S; out[1] = (sm23 + sm32) / S; out[2] = 0.25 * S; } return out; } /** @hidden */ class MathUtils { static identity(v) { return v; } static eq(a, b, tolerance = 10e-6) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (Math.abs(a[i] - b[i]) > tolerance) return false; } return true; } static clamp(value, min, max) { if (value < min) return min; if (value > max) return max; return value; } // TODO(perf): Compare performance if we replace the switch with individual functions. static decodeNormalizedInt(i, componentType) { // Hardcode enums from accessor.ts to avoid a circular dependency. switch (componentType) { case 5126: // FLOAT return i; case 5123: // UNSIGNED_SHORT return i / 65535.0; case 5121: // UNSIGNED_BYTE return i / 255.0; case 5122: // SHORT return Math.max(i / 32767.0, -1.0); case 5120: // BYTE return Math.max(i / 127.0, -1.0); default: throw new Error('Invalid component type.'); } } // TODO(perf): Compare performance if we replace the switch with individual functions. static encodeNormalizedInt(f, componentType) { // Hardcode enums from accessor.ts to avoid a circular dependency. switch (componentType) { case 5126: // FLOAT return f; case 5123: // UNSIGNED_SHORT return Math.round(MathUtils.clamp(f, 0, 1) * 65535.0); case 5121: // UNSIGNED_BYTE return Math.round(MathUtils.clamp(f, 0, 1) * 255.0); case 5122: // SHORT return Math.round(MathUtils.clamp(f, -1, 1) * 32767.0); case 5120: // BYTE return Math.round(MathUtils.clamp(f, -1, 1) * 127.0); default: throw new Error('Invalid component type.'); } } /** * Decompose a mat4 to TRS properties. * * Equivalent to the Matrix4 decompose() method in three.js, and intentionally not using the * gl-matrix version. See: https://github.com/toji/gl-matrix/issues/408 * * @param srcMat Matrix element, to be decomposed to TRS properties. * @param dstTranslation Translation element, to be overwritten. * @param dstRotation Rotation element, to be overwritten. * @param dstScale Scale element, to be overwritten. */ static decompose(srcMat, dstTranslation, dstRotation, dstScale) { let sx = length([srcMat[0], srcMat[1], srcMat[2]]); const sy = length([srcMat[4], srcMat[5], srcMat[6]]); const sz = length([srcMat[8], srcMat[9], srcMat[10]]); // if determine is negative, we need to invert one scale const det = determinant(srcMat); if (det < 0) sx = -sx; dstTranslation[0] = srcMat[12]; dstTranslation[1] = srcMat[13]; dstTranslation[2] = srcMat[14]; // scale the rotation part const _m1 = srcMat.slice(); const invSX = 1 / sx; const invSY = 1 / sy; const invSZ = 1 / sz; _m1[0] *= invSX; _m1[1] *= invSX; _m1[2] *= invSX; _m1[4] *= invSY; _m1[5] *= invSY; _m1[6] *= invSY; _m1[8] *= invSZ; _m1[9] *= invSZ; _m1[10] *= invSZ; getRotation(dstRotation, _m1); dstScale[0] = sx; dstScale[1] = sy; dstScale[2] = sz; } /** * Compose TRS properties to a mat4. * * Equivalent to the Matrix4 compose() method in three.js, and intentionally not using the * gl-matrix version. See: https://github.com/toji/gl-matrix/issues/408 * * @param srcTranslation Translation element of matrix. * @param srcRotation Rotation element of matrix. * @param srcScale Scale element of matrix. * @param dstMat Matrix element, to be modified and returned. * @returns dstMat, overwritten to mat4 equivalent of given TRS properties. */ static compose(srcTranslation, srcRotation, srcScale, dstMat) { const te = dstMat; const x = srcRotation[0], y = srcRotation[1], z = srcRotation[2], w = srcRotation[3]; const x2 = x + x, y2 = y + y, z2 = z + z; const xx = x * x2, xy = x * y2, xz = x * z2; const yy = y * y2, yz = y * z2, zz = z * z2; const wx = w * x2, wy = w * y2, wz = w * z2; const sx = srcScale[0], sy = srcScale[1], sz = srcScale[2]; te[0] = (1 - (yy + zz)) * sx; te[1] = (xy + wz) * sx; te[2] = (xz - wy) * sx; te[3] = 0; te[4] = (xy - wz) * sy; te[5] = (1 - (xx + zz)) * sy; te[6] = (yz + wx) * sy; te[7] = 0; te[8] = (xz + wy) * sz; te[9] = (yz - wx) * sz; te[10] = (1 - (xx + yy)) * sz; te[11] = 0; te[12] = srcTranslation[0]; te[13] = srcTranslation[1]; te[14] = srcTranslation[2]; te[15] = 1; return te; } } function equalsRef(refA, refB) { if (!!refA !== !!refB) return false; const a = refA.getChild(); const b = refB.getChild(); return a === b || a.equals(b); } function equalsRefSet(refSetA, refSetB) { if (!!refSetA !== !!refSetB) return false; const refValuesA = refSetA.values(); const refValuesB = refSetB.values(); if (refValuesA.length !== refValuesB.length) return false; for (let i = 0; i < refValuesA.length; i++) { const a = refValuesA[i]; const b = refValuesB[i]; if (a.getChild() === b.getChild()) continue; if (!a.getChild().equals(b.getChild())) return false; } return true; } function equalsRefMap(refMapA, refMapB) { if (!!refMapA !== !!refMapB) return false; const keysA = refMapA.keys(); const keysB = refMapB.keys(); if (keysA.length !== keysB.length) return false; for (const key of keysA) { const refA = refMapA.get(key); const refB = refMapB.get(key); if (!!refA !== !!refB) return false; const a = refA.getChild(); const b = refB.getChild(); if (a === b) continue; if (!a.equals(b)) return false; } return true; } function equalsArray(a, b) { if (a === b) return true; if (!!a !== !!b || !a || !b) return false; if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; } return true; } function equalsObject(_a, _b) { if (_a === _b) return true; if (!!_a !== !!_b) return false; if (!isPlainObject(_a) || !isPlainObject(_b)) { return _a === _b; } const a = _a; const b = _b; let numKeysA = 0; let numKeysB = 0; let key; for (key in a) numKeysA++; for (key in b) numKeysB++; if (numKeysA !== numKeysB) return false; for (key in a) { const valueA = a[key]; const valueB = b[key]; if (isArray(valueA) && isArray(valueB)) { if (!equalsArray(valueA, valueB)) return false; } else if (isPlainObject(valueA) && isPlainObject(valueB)) { if (!equalsObject(valueA, valueB)) return false; } else { if (valueA !== valueB) return false; } } return true; } function isArray(value) { return Array.isArray(value) || ArrayBuffer.isView(value); } const ALPHABET = '23456789abdegjkmnpqrvwxyzABDEGJKMNPQRVWXYZ'; const UNIQUE_RETRIES = 999; const ID_LENGTH = 6; const previousIDs = new Set(); const generateOne = function generateOne() { let rtn = ''; for (let i = 0; i < ID_LENGTH; i++) { rtn += ALPHABET.charAt(Math.floor(Math.random() * ALPHABET.length)); } return rtn; }; /** * Short ID generator. * * Generated IDs are short, easy to type, and unique for the duration of the program's execution. * Uniqueness across multiple program executions, or on other devices, is not guaranteed. Based on * [Short ID Generation in JavaScript](https://tomspencer.dev/blog/2014/11/16/short-id-generation-in-javascript/), * with alterations. * * @category Utilities * @hidden */ const uuid = function uuid() { for (let retries = 0; retries < UNIQUE_RETRIES; retries++) { const id = generateOne(); if (!previousIDs.has(id)) { previousIDs.add(id); return id; } } return ''; }; const COPY_IDENTITY = t => t; const EMPTY_SET = new Set(); /** * *Properties represent distinct resources in a glTF asset, referenced by other properties.* * * For example, each material and texture is a property, with material properties holding * references to the textures. All properties are created with factory methods on the * {@link Document} in which they should be constructed. Properties are destroyed by calling * {@link Property.dispose}(). * * Usage: * * ```ts * const texture = doc.createTexture('myTexture'); * doc.listTextures(); // → [texture x 1] * * // Attach a texture to a material. * material.setBaseColorTexture(texture); * material.getBaseColortexture(); // → texture * * // Detaching a texture removes any references to it, except from the doc. * texture.detach(); * material.getBaseColorTexture(); // → null * doc.listTextures(); // → [texture x 1] * * // Disposing a texture removes all references to it, and its own references. * texture.dispose(); * doc.listTextures(); // → [] * ``` * * Reference: * - [glTF → Concepts](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#concepts) * * @category Properties */ class Property extends GraphNode { /** @hidden */ constructor(graph, name = '') { super(graph); this[$attributes]['name'] = name; this.init(); this.dispatchEvent({ type: 'create' }); } /** * Returns the Graph associated with this Property. For internal use. * @hidden * @experimental */ getGraph() { return this.graph; } /** * Returns default attributes for the property. Empty lists and maps should be initialized * to empty arrays and objects. Always invoke `super.getDefaults()` and extend the result. */ getDefaults() { return Object.assign(super.getDefaults(), { name: '', extras: {} }); } /** @hidden */ set(attribute, value) { if (Array.isArray(value)) value = value.slice(); // copy vector, quat, color … return super.set(attribute, value); } /********************************************************************************************** * Name. */ /** * Returns the name of this property. While names are not required to be unique, this is * encouraged, and non-unique names will be overwritten in some tools. For custom data about * a property, prefer to use Extras. */ getName() { return this.get('name'); } /** * Sets the name of this property. While names are not required to be unique, this is * encouraged, and non-unique names will be overwritten in some tools. For custom data about * a property, prefer to use Extras. */ setName(name) { return this.set('name', name); } /********************************************************************************************** * Extras. */ /** * Returns a reference to the Extras object, containing application-specific data for this * Property. Extras should be an Object, not a primitive value, for best portability. */ getExtras() { return this.get('extras'); } /** * Updates the Extras object, containing application-specific data for this Property. Extras * should be an Object, not a primitive value, for best portability. */ setExtras(extras) { return this.set('extras', extras); } /********************************************************************************************** * Graph state. */ /** * Makes a copy of this property, with the same resources (by reference) as the original. */ clone() { const PropertyClass = this.constructor; return new PropertyClass(this.graph).copy(this, COPY_IDENTITY); } /** * Copies all data from another property to this one. Child properties are copied by reference, * unless a 'resolve' function is given to override that. * @param other Property to copy references from. * @param resolve Function to resolve each Property being transferred. Default is identity. */ copy(other, resolve = COPY_IDENTITY) { // Remove previous references. for (const key in this[$attributes]) { const value = this[$attributes][key]; if (value instanceof GraphEdge) { if (!this[$immutableKeys].has(key)) { value.dispose(); } } else if (value instanceof RefList || value instanceof RefSet) { for (const ref of value.values()) { ref.dispose(); } } else if (value instanceof RefMap) { for (const ref of value.values()) { ref.dispose(); } } } // Add new references. for (const key in other[$attributes]) { const thisValue = this[$attributes][key]; const otherValue = other[$attributes][key]; if (otherValue instanceof GraphEdge) { if (this[$immutableKeys].has(key)) { const ref = thisValue; ref.getChild().copy(resolve(otherValue.getChild()), resolve); } else { // biome-ignore lint/suspicious/noExplicitAny: TODO this.setRef(key, resolve(otherValue.getChild()), otherValue.getAttributes()); } } else if (otherValue instanceof RefSet || otherValue instanceof RefList) { for (const ref of otherValue.values()) { // biome-ignore lint/suspicious/noExplicitAny: TODO this.addRef(key, resolve(ref.getChild()), ref.getAttributes()); } } else if (otherValue instanceof RefMap) { for (const subkey of otherValue.keys()) { const ref = otherValue.get(subkey); // biome-ignore lint/suspicious/noExplicitAny: TODO this.setRefMap(key, subkey, resolve(ref.getChild()), ref.getAttributes()); } } else if (isPlainObject(otherValue)) { this[$attributes][key] = JSON.parse(JSON.stringify(otherValue)); } else if (Array.isArray(otherValue) || otherValue instanceof ArrayBuffer || ArrayBuffer.isView(otherValue)) { // biome-ignore lint/suspicious/noExplicitAny: TODO this[$attributes][key] = otherValue.slice(); } else { this[$attributes][key] = otherValue; } } return this; } /** * Returns true if two properties are deeply equivalent, recursively comparing the attributes * of the properties. Optionally, a 'skip' set may be included, specifying attributes whose * values should not be considered in the comparison. * * Example: Two {@link Primitive Primitives} are equivalent if they have accessors and * materials with equivalent content — but not necessarily the same specific accessors * and materials. */ equals(other, skip = EMPTY_SET) { if (this === other) return true; if (this.propertyType !== other.propertyType) return false; for (const key in this[$attributes]) { if (skip.has(key)) continue; const a = this[$attributes][key]; const b = other[$attributes][key]; if (a instanceof GraphEdge || b instanceof GraphEdge) { if (!equalsRef(a, b)) { return false; } } else if (a instanceof RefSet || b instanceof RefSet || a instanceof RefList || b instanceof RefList) { if (!equalsRefSet(a, b)) { return false; } } else if (a instanceof RefMap || b instanceof RefMap) { if (!equalsRefMap(a, b)) { return false; } } else if (isPlainObject(a) || isPlainObject(b)) { if (!equalsObject(a, b)) return false; } else if (isArray(a) || isArray(b)) { if (!equalsArray(a, b)) return false; } else { // Literal. if (a !== b) return false; } } return true; } detach() { // Detaching should keep properties in the same Document, and attached to its root. this.graph.disconnectParents(this, n => n.propertyType !== 'Root'); return this; } /** * Returns a list of all properties that hold a reference to this property. For example, a * material may hold references to various textures, but a texture does not hold references * to the materials that use it. * * It is often necessary to filter the results for a particular type: some resources, like * {@link Accessor}s, may be referenced by different types of properties. Most properties * include the {@link Root} as a parent, which is usually not of interest. * * Usage: * * ```ts * const materials = texture * .listParents() * .filter((p) => p instanceof Material) * ``` */ listParents() { return this.graph.listParents(this); } } /** * *A {@link Property} that can have {@link ExtensionProperty} instances attached.* * * Most properties are extensible. See the {@link Extension} documentation for information about * how to use extensions. * * @category Properties */ class ExtensibleProperty extends Property { getDefaults() { return Object.assign(super.getDefaults(), { extensions: new RefMap() }); } /** Returns an {@link ExtensionProperty} attached to this Property, if any. */ getExtension(name) { return this.getRefMap('extensions', name); } /** * Attaches the given {@link ExtensionProperty} to this Property. For a given extension, only * one ExtensionProperty may be attached to any one Property at a time. */ setExtension(name, extensionProperty) { if (extensionProperty) extensionProperty._validateParent(this); return this.setRefMap('extensions', name, extensionProperty); } /** Lists all {@link ExtensionProperty} instances attached to this Property. */ listExtensions() { return this.listRefMapValues('extensions'); } } /** * *Accessors store lists of numeric, vector, or matrix elements in a typed array.* * * All large data for {@link Mesh}, {@link Skin}, and {@link Animation} properties is stored in * {@link Accessor}s, organized into one or more {@link Buffer}s. Each accessor provides data in * typed arrays, with two abstractions: * * *Elements* are the logical divisions of the data into useful types: `"SCALAR"`, `"VEC2"`, * `"VEC3"`, `"VEC4"`, `"MAT3"`, or `"MAT4"`. The element type can be determined with the * {@link Accessor.getType getType}() method, and the number of elements in the accessor determine its * {@link Accessor.getCount getCount}(). The number of components in an element — e.g. 9 for `"MAT3"` — are its * {@link Accessor.getElementSize getElementSize}(). See {@link Accessor.Type}. * * *Components* are the numeric values within an element — e.g. `.x` and `.y` for `"VEC2"`. Various * component types are available: `BYTE`, `UNSIGNED_BYTE`, `SHORT`, `UNSIGNED_SHORT`, * `UNSIGNED_INT`, and `FLOAT`. The component type can be determined with the * {@link Accessor.getComponentType getComponentType} method, and the number of bytes in each component determine its * {@link Accessor.getComponentSize getComponentSize}. See {@link Accessor.ComponentType}. * * Usage: * * ```typescript * const accessor = doc.createAccessor('myData') * .setArray(new Float32Array([1,2,3,4,5,6,7,8,9,10,11,12])) * .setType(Accessor.Type.VEC3) * .setBuffer(doc.getRoot().listBuffers()[0]); * * accessor.getCount(); // → 4 * accessor.getElementSize(); // → 3 * accessor.getByteLength(); // → 48 * accessor.getElement(1, []); // → [4, 5, 6] * * accessor.setElement(0, [10, 20, 30]); * ``` * * Data access through the {@link Accessor.getElement getElement} and {@link Accessor.setElement setElement} * methods reads or overwrites the content of the underlying typed array. These methods use * element arrays intended to be compatible with the [gl-matrix](https://github.com/toji/gl-matrix) * library, or with the `toArray`/`fromArray` methods of libraries like three.js and babylon.js. * * Each Accessor must be assigned to a {@link Buffer}, which determines where the accessor's data * is stored in the final file. Assigning Accessors to different Buffers allows the data to be * written to different `.bin` files. * * glTF Transform does not expose many details of sparse, normalized, or interleaved accessors * through its API. It reads files using those techniques, presents a simplified view of the data * for editing, and attempts to write data back out with optimizations. For example, vertex * attributes will typically be interleaved by default, regardless of the input file. * * References: * - [glTF → Accessors](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#accessors) * * @category Properties */ class Accessor extends ExtensibleProperty { /********************************************************************************************** * Instance. */ init() { this.propertyType = PropertyType.ACCESSOR; } getDefaults() { return Object.assign(super.getDefaults(), { array: null, type: Accessor.Type.SCALAR, componentType: Accessor.ComponentType.FLOAT, normalized: false, sparse: false, buffer: null }); } /********************************************************************************************** * Static. */ /** Returns size of a given element type, in components. */ static getElementSize(type) { switch (type) { case Accessor.Type.SCALAR: return 1; case Accessor.Type.VEC2: return 2; case Accessor.Type.VEC3: return 3; case Accessor.Type.VEC4: return 4; case Accessor.Type.MAT2: return 4; case Accessor.Type.MAT3: return 9; case Accessor.Type.MAT4: return 16; default: throw new Error('Unexpected type: ' + type); } } /** Returns size of a given component type, in bytes. */ static getComponentSize(componentType) { switch (componentType) { case Accessor.ComponentType.BYTE: return 1; case Accessor.ComponentType.UNSIGNED_BYTE: return 1; case Accessor.ComponentType.SHORT: return 2; case Accessor.ComponentType.UNSIGNED_SHORT: return 2; case Accessor.ComponentType.UNSIGNED_INT: return 4; case Accessor.ComponentType.FLOAT: return 4; default: throw new Error('Unexpected component type: ' + componentType); } } /********************************************************************************************** * Min/max bounds. */ /** * Minimum value of each component in this attribute. Unlike in a final glTF file, values * returned by this method will reflect the minimum accounting for {@link .normalized} * state. */ getMinNormalized(target) { const normalized = this.getNormalized(); const elementSize = this.getElementSize(); const componentType = this.getComponentType(); this.getMin(target); if (normalized) { for (let j = 0; j < elementSize; j++) { target[