@gltf-transform/core
Version:
glTF 2.0 SDK for JavaScript and TypeScript, on Web and Node.js.
1,563 lines (1,521 loc) • 252 kB
JavaScript
var propertyGraph = require('property-graph');
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n["default"] = e;
return n;
}
/**
* 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. */
exports.PropertyType = void 0;
(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";
})(exports.PropertyType || (exports.PropertyType = {}));
/** Vertex layout method. */
exports.VertexLayout = void 0;
(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";
})(exports.VertexLayout || (exports.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. */
exports.TextureChannel = void 0;
(function (TextureChannel) {
TextureChannel[TextureChannel["R"] = 4096] = "R";
TextureChannel[TextureChannel["G"] = 256] = "G";
TextureChannel[TextureChannel["B"] = 16] = "B";
TextureChannel[TextureChannel["A"] = 1] = "A";
})(exports.TextureChannel || (exports.TextureChannel = {}));
exports.Format = void 0;
(function (Format) {
Format["GLTF"] = "GLTF";
Format["GLB"] = "GLB";
})(exports.Format || (exports.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 === exports.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. */
exports.Verbosity = void 0;
(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";
})(exports.Verbosity || (exports.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 = exports.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 () {
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 () {
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 propertyGraph.GraphNode {
/** @hidden */
constructor(graph, name = '') {
super(graph);
this[propertyGraph.$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[propertyGraph.$attributes]) {
const value = this[propertyGraph.$attributes][key];
if (value instanceof propertyGraph.GraphEdge) {
if (!this[propertyGraph.$immutableKeys].has(key)) {
value.dispose();
}
} else if (value instanceof propertyGraph.RefList || value instanceof propertyGraph.RefSet) {
for (const ref of value.values()) {
ref.dispose();
}
} else if (value instanceof propertyGraph.RefMap) {
for (const ref of value.values()) {
ref.dispose();
}
}
}
// Add new references.
for (const key in other[propertyGraph.$attributes]) {
const thisValue = this[propertyGraph.$attributes][key];
const otherValue = other[propertyGraph.$attributes][key];
if (otherValue instanceof propertyGraph.GraphEdge) {
if (this[propertyGraph.$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 propertyGraph.RefSet || otherValue instanceof propertyGraph.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 propertyGraph.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[propertyGraph.$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[propertyGraph.$attributes][key] = otherValue.slice();
} else {
this[propertyGraph.$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[propertyGraph.$attributes]) {
if (skip.has(key)) continue;
const a = this[propertyGraph.$attributes][key];
const b = other[propertyGraph.$attributes][key];
if (a instanceof propertyGraph.GraphEdge || b instanceof propertyGraph.GraphEdge) {
if (!equalsRef(a, b)) {
return false;
}
} else if (a instanceof propertyGraph.RefSet || b instanceof propertyGraph.RefSet || a instanceof propertyGraph.RefList || b instanceof propertyGraph.RefList) {
if (!equalsRefSet(a, b)) {
return false;
}
} else if (a instanceof propertyGraph.RefMap || b instanceof propertyGraph.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 propertyGraph.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 = exports.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: