@gltf-transform/functions
Version:
Functions for common glTF modifications, written using the core API
1,700 lines (1,622 loc) • 277 kB
JavaScript
var core = require('@gltf-transform/core');
var ndarrayPixels = require('ndarray-pixels');
var extensions = require('@gltf-transform/extensions');
var ktxParse = require('ktx-parse');
var ndarray = require('ndarray');
var ndarrayLanczos = require('ndarray-lanczos');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var ndarray__default = /*#__PURE__*/_interopDefaultLegacy(ndarray);
/**
* Maps pixels from source to target textures, with a per-pixel callback.
* @hidden
*/
const rewriteTexture = function (source, target, fn) {
try {
if (!source) return Promise.resolve(null);
const srcImage = source.getImage();
if (!srcImage) return Promise.resolve(null);
return Promise.resolve(ndarrayPixels.getPixels(srcImage, source.getMimeType())).then(function (pixels) {
for (let i = 0; i < pixels.shape[0]; ++i) {
for (let j = 0; j < pixels.shape[1]; ++j) {
fn(pixels, i, j);
}
}
return Promise.resolve(ndarrayPixels.savePixels(pixels, 'image/png')).then(function (dstImage) {
return target.setImage(dstImage).setMimeType('image/png');
});
});
} catch (e) {
return Promise.reject(e);
}
};
/** @hidden */
const {
POINTS: POINTS$1,
LINES: LINES$2,
LINE_STRIP: LINE_STRIP$3,
LINE_LOOP: LINE_LOOP$3,
TRIANGLES: TRIANGLES$2,
TRIANGLE_STRIP: TRIANGLE_STRIP$3,
TRIANGLE_FAN: TRIANGLE_FAN$3
} = core.Primitive.Mode;
/**
* Prepares a function used in an {@link Document#transform} pipeline. Use of this wrapper is
* optional, and plain functions may be used in transform pipelines just as well. The wrapper is
* used internally so earlier pipeline stages can detect and optimize based on later stages.
* @hidden
*/
function createTransform(name, fn) {
Object.defineProperty(fn, 'name', {
value: name
});
return fn;
}
/** @hidden */
function isTransformPending(context, initial, pending) {
if (!context) return false;
const initialIndex = context.stack.lastIndexOf(initial);
const pendingIndex = context.stack.lastIndexOf(pending);
return initialIndex < pendingIndex;
}
/**
* Performs a shallow merge on an 'options' object and a 'defaults' object.
* Equivalent to `{...defaults, ...options}` _except_ that `undefined` values
* in the 'options' object are ignored.
*
* @hidden
*/
function assignDefaults(defaults, options) {
const result = {
...defaults
};
for (const key in options) {
if (options[key] !== undefined) {
// biome-ignore lint/suspicious/noExplicitAny: TODO
result[key] = options[key];
}
}
return result;
}
function getGLPrimitiveCount(prim) {
const indices = prim.getIndices();
const position = prim.getAttribute('POSITION');
// Reference: https://www.khronos.org/opengl/wiki/Primitive
switch (prim.getMode()) {
case core.Primitive.Mode.POINTS:
return indices ? indices.getCount() : position.getCount();
case core.Primitive.Mode.LINES:
return indices ? indices.getCount() / 2 : position.getCount() / 2;
case core.Primitive.Mode.LINE_LOOP:
return indices ? indices.getCount() : position.getCount();
case core.Primitive.Mode.LINE_STRIP:
return indices ? indices.getCount() - 1 : position.getCount() - 1;
case core.Primitive.Mode.TRIANGLES:
return indices ? indices.getCount() / 3 : position.getCount() / 3;
case core.Primitive.Mode.TRIANGLE_STRIP:
case core.Primitive.Mode.TRIANGLE_FAN:
return indices ? indices.getCount() - 2 : position.getCount() - 2;
default:
throw new Error('Unexpected mode: ' + prim.getMode());
}
}
/** @hidden */
class SetMap {
constructor() {
this._map = new Map();
}
get size() {
return this._map.size;
}
has(k) {
return this._map.has(k);
}
add(k, v) {
let entry = this._map.get(k);
if (!entry) {
entry = new Set();
this._map.set(k, entry);
}
entry.add(v);
return this;
}
get(k) {
return this._map.get(k) || new Set();
}
keys() {
return this._map.keys();
}
}
/** @hidden */
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1000;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
const _longFormatter = new Intl.NumberFormat(undefined, {
maximumFractionDigits: 0
});
/** @hidden */
function formatLong(x) {
return _longFormatter.format(x);
}
/** @hidden */
function formatDelta(a, b, decimals = 2) {
const prefix = a > b ? '–' : '+';
const suffix = '%';
return prefix + (Math.abs(a - b) / a * 100).toFixed(decimals) + suffix;
}
/** @hidden */
function formatDeltaOp(a, b) {
return `${formatLong(a)} → ${formatLong(b)} (${formatDelta(a, b)})`;
}
/**
* Returns a list of all unique vertex attributes on the given primitive and
* its morph targets.
* @hidden
*/
function deepListAttributes(prim) {
const accessors = [];
for (const attribute of prim.listAttributes()) {
accessors.push(attribute);
}
for (const target of prim.listTargets()) {
for (const attribute of target.listAttributes()) {
accessors.push(attribute);
}
}
return Array.from(new Set(accessors));
}
/** @hidden */
function deepSwapAttribute(prim, src, dst) {
prim.swap(src, dst);
for (const target of prim.listTargets()) {
target.swap(src, dst);
}
}
/** @hidden */
function shallowEqualsArray(a, b) {
if (a == null && b == null) return true;
if (a == null || b == null) 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;
}
/** Clones an {@link Accessor} without creating a copy of its underlying TypedArray data. */
function shallowCloneAccessor(document, accessor) {
return document.createAccessor(accessor.getName()).setArray(accessor.getArray()).setType(accessor.getType()).setBuffer(accessor.getBuffer()).setNormalized(accessor.getNormalized()).setSparse(accessor.getSparse());
}
/** @hidden */
function createIndices(count, maxIndex = count) {
const array = createIndicesEmpty(count, maxIndex);
for (let i = 0; i < array.length; i++) array[i] = i;
return array;
}
/** @hidden */
function createIndicesEmpty(count, maxIndex = count) {
return maxIndex <= 65534 ? new Uint16Array(count) : new Uint32Array(count);
}
/** @hidden */
function isUsed(prop) {
return prop.listParents().some(parent => parent.propertyType !== core.PropertyType.ROOT);
}
/** @hidden */
function isEmptyObject(object) {
for (const key in object) return false;
return true;
}
/**
* Creates a unique key associated with the structure and draw call characteristics of
* a {@link Primitive}, independent of its vertex content. Helper method, used to
* identify candidate Primitives for joining.
* @hidden
*/
function createPrimGroupKey(prim) {
const document = core.Document.fromGraph(prim.getGraph());
const material = prim.getMaterial();
const materialIndex = document.getRoot().listMaterials().indexOf(material);
const mode = BASIC_MODE_MAPPING[prim.getMode()];
const indices = !!prim.getIndices();
const attributes = prim.listSemantics().sort().map(semantic => {
const attribute = prim.getAttribute(semantic);
const elementSize = attribute.getElementSize();
const componentType = attribute.getComponentType();
return `${semantic}:${elementSize}:${componentType}`;
}).join('+');
const targets = prim.listTargets().map(target => {
return target.listSemantics().sort().map(semantic => {
const attribute = prim.getAttribute(semantic);
const elementSize = attribute.getElementSize();
const componentType = attribute.getComponentType();
return `${semantic}:${elementSize}:${componentType}`;
}).join('+');
}).join('~');
return `${materialIndex}|${mode}|${indices}|${attributes}|${targets}`;
}
/**
* Scales `size` NxN dimensions to fit within `limit` NxN dimensions, without
* changing aspect ratio. If `size` <= `limit` in all dimensions, returns `size`.
* @hidden
*/
function fitWithin(size, limit) {
const [maxWidth, maxHeight] = limit;
const [srcWidth, srcHeight] = size;
if (srcWidth <= maxWidth && srcHeight <= maxHeight) return size;
let dstWidth = srcWidth;
let dstHeight = srcHeight;
if (dstWidth > maxWidth) {
dstHeight = Math.floor(dstHeight * (maxWidth / dstWidth));
dstWidth = maxWidth;
}
if (dstHeight > maxHeight) {
dstWidth = Math.floor(dstWidth * (maxHeight / dstHeight));
dstHeight = maxHeight;
}
return [dstWidth, dstHeight];
}
/**
* Scales `size` NxN dimensions to the specified power of two.
* @hidden
*/
function fitPowerOfTwo(size, method) {
if (isPowerOfTwo(size[0]) && isPowerOfTwo(size[1])) {
return size;
}
switch (method) {
case 'nearest-pot':
return size.map(nearestPowerOfTwo);
case 'ceil-pot':
return size.map(ceilPowerOfTwo$1);
case 'floor-pot':
return size.map(floorPowerOfTwo);
}
}
function isPowerOfTwo(value) {
if (value <= 2) return true;
return (value & value - 1) === 0 && value !== 0;
}
function nearestPowerOfTwo(value) {
if (value <= 4) return 4;
const lo = floorPowerOfTwo(value);
const hi = ceilPowerOfTwo$1(value);
if (hi - value > value - lo) return lo;
return hi;
}
function floorPowerOfTwo(value) {
return Math.pow(2, Math.floor(Math.log(value) / Math.LN2));
}
function ceilPowerOfTwo$1(value) {
return Math.pow(2, Math.ceil(Math.log(value) / Math.LN2));
}
/**
* Mapping from any glTF primitive mode to its equivalent basic mode, as returned by
* {@link convertPrimitiveMode}.
* @hidden
*/
const BASIC_MODE_MAPPING = {
[POINTS$1]: POINTS$1,
[LINES$2]: LINES$2,
[LINE_STRIP$3]: LINES$2,
[LINE_LOOP$3]: LINES$2,
[TRIANGLES$2]: TRIANGLES$2,
[TRIANGLE_STRIP$3]: TRIANGLES$2,
[TRIANGLE_FAN$3]: TRIANGLES$2
};
const NAME$q = 'center';
const CENTER_DEFAULTS = {
pivot: 'center'
};
/**
* Centers the {@link Scene} at the origin, or above/below it. Transformations from animation,
* skinning, and morph targets are not taken into account.
*
* Example:
*
* ```ts
* await document.transform(center({pivot: 'below'}));
* ```
*
* @category Transforms
*/
function center(_options = CENTER_DEFAULTS) {
const options = assignDefaults(CENTER_DEFAULTS, _options);
return createTransform(NAME$q, doc => {
const logger = doc.getLogger();
const root = doc.getRoot();
const isAnimated = root.listAnimations().length > 0 || root.listSkins().length > 0;
doc.getRoot().listScenes().forEach((scene, index) => {
logger.debug(`${NAME$q}: Scene ${index + 1} / ${root.listScenes().length}.`);
let pivot;
if (typeof options.pivot === 'string') {
const bbox = core.getBounds(scene);
pivot = [(bbox.max[0] - bbox.min[0]) / 2 + bbox.min[0], (bbox.max[1] - bbox.min[1]) / 2 + bbox.min[1], (bbox.max[2] - bbox.min[2]) / 2 + bbox.min[2]];
if (options.pivot === 'above') pivot[1] = bbox.max[1];
if (options.pivot === 'below') pivot[1] = bbox.min[1];
} else {
pivot = options.pivot;
}
logger.debug(`${NAME$q}: Pivot "${pivot.join(', ')}".`);
const offset = [-1 * pivot[0], -1 * pivot[1], -1 * pivot[2]];
if (isAnimated) {
logger.debug(`${NAME$q}: Model contains animation or skin. Adding a wrapper node.`);
const offsetNode = doc.createNode('Pivot').setTranslation(offset);
scene.listChildren().forEach(child => offsetNode.addChild(child));
scene.addChild(offsetNode);
} else {
logger.debug(`${NAME$q}: Skipping wrapper, offsetting all root nodes.`);
scene.listChildren().forEach(child => {
const t = child.getTranslation();
child.setTranslation([t[0] + offset[0], t[1] + offset[1], t[2] + offset[2]]);
});
}
});
logger.debug(`${NAME$q}: Complete.`);
});
}
/**
* Finds the parent {@link Scene Scenes} associated with the given {@link Node}.
* In most cases a Node is associated with only one Scene, but it is possible
* for a Node to be located in two or more Scenes, or none at all.
*
* Example:
*
* ```typescript
* import { listNodeScenes } from '@gltf-transform/functions';
*
* const node = document.getRoot().listNodes()
* .find((node) => node.getName() === 'MyNode');
*
* const scenes = listNodeScenes(node);
* ```
*/
function listNodeScenes(node) {
const visited = new Set();
let child = node;
let parent;
while (parent = child.getParentNode()) {
if (visited.has(parent)) {
throw new Error('Circular dependency in scene graph.');
}
visited.add(parent);
child = parent;
}
return child.listParents().filter(parent => parent instanceof core.Scene);
}
/**
* Clears the parent of the given {@link Node}, leaving it attached
* directly to its {@link Scene}. Inherited transforms will be applied
* to the Node. This operation changes the Node's local transform,
* but leaves its world transform unchanged.
*
* Example:
*
* ```typescript
* import { clearNodeParent } from '@gltf-transform/functions';
*
* scene.traverse((node) => { ... }); // Scene → … → Node
*
* clearNodeParent(node);
*
* scene.traverse((node) => { ... }); // Scene → Node
* ```
*
* To clear _all_ transforms of a Node, first clear its inherited transforms with
* {@link clearNodeParent}, then clear the local transform with {@link clearNodeTransform}.
*/
function clearNodeParent(node) {
const scenes = listNodeScenes(node);
const parent = node.getParentNode();
if (!parent) return node;
// Apply inherited transforms to local matrix. Skinned meshes are not affected
// by the node parent's transform, and can be ignored. Updates to IBMs and TRS
// animations are out of scope in this context.
node.setMatrix(node.getWorldMatrix());
// Add to Scene roots.
parent.removeChild(node);
for (const scene of scenes) scene.addChild(node);
return node;
}
/**
* 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);
};
/**
* Inverts a mat4
*
* @param {mat4} out the receiving matrix
* @param {ReadonlyMat4} a the source matrix
* @returns {mat4} out
*/
function invert$1(out, 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
var det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
if (!det) {
return null;
}
det = 1.0 / det;
out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det;
out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det;
out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det;
out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det;
out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det;
out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det;
out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;
return out;
}
/**
* 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$2(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;
}
/**
* Creates a matrix from a vector scaling
* This is equivalent to (but much faster than):
*
* mat4.identity(dest);
* mat4.scale(dest, dest, vec);
*
* @param {mat4} out mat4 receiving operation result
* @param {ReadonlyVec3} v Scaling vector
* @returns {mat4} out
*/
function fromScaling(out, v) {
out[0] = v[0];
out[1] = 0;
out[2] = 0;
out[3] = 0;
out[4] = 0;
out[5] = v[1];
out[6] = 0;
out[7] = 0;
out[8] = 0;
out[9] = 0;
out[10] = v[2];
out[11] = 0;
out[12] = 0;
out[13] = 0;
out[14] = 0;
out[15] = 1;
return out;
}
/**
* Creates a matrix from a quaternion rotation, vector translation and vector scale
* This is equivalent to (but much faster than):
*
* mat4.identity(dest);
* mat4.translate(dest, vec);
* let quatMat = mat4.create();
* quat4.toMat4(quat, quatMat);
* mat4.multiply(dest, quatMat);
* mat4.scale(dest, scale)
*
* @param {mat4} out mat4 receiving operation result
* @param {quat4} q Rotation quaternion
* @param {ReadonlyVec3} v Translation vector
* @param {ReadonlyVec3} s Scaling vector
* @returns {mat4} out
*/
function fromRotationTranslationScale(out, q, v, s) {
// Quaternion math
var x = q[0],
y = q[1],
z = q[2],
w = q[3];
var x2 = x + x;
var y2 = y + y;
var z2 = z + z;
var xx = x * x2;
var xy = x * y2;
var xz = x * z2;
var yy = y * y2;
var yz = y * z2;
var zz = z * z2;
var wx = w * x2;
var wy = w * y2;
var wz = w * z2;
var sx = s[0];
var sy = s[1];
var sz = s[2];
out[0] = (1 - (yy + zz)) * sx;
out[1] = (xy + wz) * sx;
out[2] = (xz - wy) * sx;
out[3] = 0;
out[4] = (xy - wz) * sy;
out[5] = (1 - (xx + zz)) * sy;
out[6] = (yz + wx) * sy;
out[7] = 0;
out[8] = (xz + wy) * sz;
out[9] = (yz - wx) * sz;
out[10] = (1 - (xx + yy)) * sz;
out[11] = 0;
out[12] = v[0];
out[13] = v[1];
out[14] = v[2];
out[15] = 1;
return out;
}
/**
* 3x3 Matrix
* @module mat3
*/
/**
* Creates a new identity mat3
*
* @returns {mat3} a new 3x3 matrix
*/
function create$2() {
var out = new ARRAY_TYPE(9);
if (ARRAY_TYPE != Float32Array) {
out[1] = 0;
out[2] = 0;
out[3] = 0;
out[5] = 0;
out[6] = 0;
out[7] = 0;
}
out[0] = 1;
out[4] = 1;
out[8] = 1;
return out;
}
/**
* Copies the upper-left 3x3 values into the given mat3.
*
* @param {mat3} out the receiving 3x3 matrix
* @param {ReadonlyMat4} a the source 4x4 matrix
* @returns {mat3} out
*/
function fromMat4(out, a) {
out[0] = a[0];
out[1] = a[1];
out[2] = a[2];
out[3] = a[4];
out[4] = a[5];
out[5] = a[6];
out[6] = a[8];
out[7] = a[9];
out[8] = a[10];
return out;
}
/**
* Transpose the values of a mat3
*
* @param {mat3} out the receiving matrix
* @param {ReadonlyMat3} a the source matrix
* @returns {mat3} out
*/
function transpose(out, a) {
// If we are transposing ourselves we can skip a few steps but have to cache some values
if (out === a) {
var a01 = a[1],
a02 = a[2],
a12 = a[5];
out[1] = a[3];
out[2] = a[6];
out[3] = a01;
out[5] = a[7];
out[6] = a02;
out[7] = a12;
} else {
out[0] = a[0];
out[1] = a[3];
out[2] = a[6];
out[3] = a[1];
out[4] = a[4];
out[5] = a[7];
out[6] = a[2];
out[7] = a[5];
out[8] = a[8];
}
return out;
}
/**
* Inverts a mat3
*
* @param {mat3} out the receiving matrix
* @param {ReadonlyMat3} a the source matrix
* @returns {mat3} out
*/
function invert(out, a) {
var a00 = a[0],
a01 = a[1],
a02 = a[2];
var a10 = a[3],
a11 = a[4],
a12 = a[5];
var a20 = a[6],
a21 = a[7],
a22 = a[8];
var b01 = a22 * a11 - a12 * a21;
var b11 = -a22 * a10 + a12 * a20;
var b21 = a21 * a10 - a11 * a20; // Calculate the determinant
var det = a00 * b01 + a01 * b11 + a02 * b21;
if (!det) {
return null;
}
det = 1.0 / det;
out[0] = b01 * det;
out[1] = (-a22 * a01 + a02 * a21) * det;
out[2] = (a12 * a01 - a02 * a11) * det;
out[3] = b11 * det;
out[4] = (a22 * a00 - a02 * a20) * det;
out[5] = (-a12 * a00 + a02 * a10) * det;
out[6] = b21 * det;
out[7] = (-a21 * a00 + a01 * a20) * det;
out[8] = (a11 * a00 - a01 * a10) * det;
return out;
}
/**
* 3 Dimensional Vector
* @module vec3
*/
/**
* Creates a new, empty vec3
*
* @returns {vec3} a new 3D vector
*/
function create$1() {
var out = new ARRAY_TYPE(3);
if (ARRAY_TYPE != Float32Array) {
out[0] = 0;
out[1] = 0;
out[2] = 0;
}
return out;
}
/**
* Multiplies two vec3's
*
* @param {vec3} out the receiving vector
* @param {ReadonlyVec3} a the first operand
* @param {ReadonlyVec3} b the second operand
* @returns {vec3} out
*/
function multiply$1(out, a, b) {
out[0] = a[0] * b[0];
out[1] = a[1] * b[1];
out[2] = a[2] * b[2];
return out;
}
/**
* Returns the minimum of two vec3's
*
* @param {vec3} out the receiving vector
* @param {ReadonlyVec3} a the first operand
* @param {ReadonlyVec3} b the second operand
* @returns {vec3} out
*/
function min(out, a, b) {
out[0] = Math.min(a[0], b[0]);
out[1] = Math.min(a[1], b[1]);
out[2] = Math.min(a[2], b[2]);
return out;
}
/**
* Returns the maximum of two vec3's
*
* @param {vec3} out the receiving vector
* @param {ReadonlyVec3} a the first operand
* @param {ReadonlyVec3} b the second operand
* @returns {vec3} out
*/
function max(out, a, b) {
out[0] = Math.max(a[0], b[0]);
out[1] = Math.max(a[1], b[1]);
out[2] = Math.max(a[2], b[2]);
return out;
}
/**
* Scales a vec3 by a scalar number
*
* @param {vec3} out the receiving vector
* @param {ReadonlyVec3} a the vector to scale
* @param {Number} b amount to scale the vector by
* @returns {vec3} out
*/
function scale$1(out, a, b) {
out[0] = a[0] * b;
out[1] = a[1] * b;
out[2] = a[2] * b;
return out;
}
/**
* Normalize a vec3
*
* @param {vec3} out the receiving vector
* @param {ReadonlyVec3} a vector to normalize
* @returns {vec3} out
*/
function normalize(out, a) {
var x = a[0];
var y = a[1];
var z = a[2];
var len = x * x + y * y + z * z;
if (len > 0) {
//TODO: evaluate use of glm_invsqrt here?
len = 1 / Math.sqrt(len);
}
out[0] = a[0] * len;
out[1] = a[1] * len;
out[2] = a[2] * len;
return out;
}
/**
* 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;
}
/**
* Transforms the vec3 with a mat3.
*
* @param {vec3} out the receiving vector
* @param {ReadonlyVec3} a the vector to transform
* @param {ReadonlyMat3} m the 3x3 matrix to transform with
* @returns {vec3} out
*/
function transformMat3(out, a, m) {
var x = a[0],
y = a[1],
z = a[2];
out[0] = x * m[0] + y * m[3] + z * m[6];
out[1] = x * m[1] + y * m[4] + z * m[7];
out[2] = x * m[2] + y * m[5] + z * m[8];
return out;
}
/**
* Alias for {@link vec3.multiply}
* @function
*/
var mul$1 = multiply$1;
/**
* 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$1();
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;
};
})();
const NAME$p = 'dedup';
const DEDUP_DEFAULTS = {
keepUniqueNames: false,
propertyTypes: [core.PropertyType.ACCESSOR, core.PropertyType.MESH, core.PropertyType.TEXTURE, core.PropertyType.MATERIAL, core.PropertyType.SKIN]
};
/**
* Removes duplicate {@link Accessor}, {@link Mesh}, {@link Texture}, and {@link Material}
* properties. Partially based on a
* [gist by mattdesl](https://gist.github.com/mattdesl/aea40285e2d73916b6b9101b36d84da8). Only
* accessors in mesh primitives, morph targets, and animation samplers are processed.
*
* Example:
*
* ```ts
* document.getRoot().listMeshes(); // → [Mesh, Mesh, Mesh]
*
* await document.transform(dedup({propertyTypes: [PropertyType.MESH]}));
*
* document.getRoot().listMeshes(); // → [Mesh]
* ```
*
* @category Transforms
*/
function dedup(_options = DEDUP_DEFAULTS) {
const options = assignDefaults(DEDUP_DEFAULTS, _options);
const propertyTypes = new Set(options.propertyTypes);
for (const propertyType of options.propertyTypes) {
if (!DEDUP_DEFAULTS.propertyTypes.includes(propertyType)) {
throw new Error(`${NAME$p}: Unsupported deduplication on type "${propertyType}".`);
}
}
return createTransform(NAME$p, document => {
const logger = document.getLogger();
if (propertyTypes.has(core.PropertyType.ACCESSOR)) dedupAccessors(document);
if (propertyTypes.has(core.PropertyType.TEXTURE)) dedupImages(document, options);
if (propertyTypes.has(core.PropertyType.MATERIAL)) dedupMaterials(document, options);
if (propertyTypes.has(core.PropertyType.MESH)) dedupMeshes(document, options);
if (propertyTypes.has(core.PropertyType.SKIN)) dedupSkins(document, options);
logger.debug(`${NAME$p}: Complete.`);
});
}
function dedupAccessors(document) {
const logger = document.getLogger();
// Find all accessors used for mesh and animation data.
const indicesMap = new Map();
const attributeMap = new Map();
const inputMap = new Map();
const outputMap = new Map();
const meshes = document.getRoot().listMeshes();
meshes.forEach(mesh => {
mesh.listPrimitives().forEach(primitive => {
primitive.listAttributes().forEach(accessor => hashAccessor(accessor, attributeMap));
hashAccessor(primitive.getIndices(), indicesMap);
});
});
for (const animation of document.getRoot().listAnimations()) {
for (const sampler of animation.listSamplers()) {
hashAccessor(sampler.getInput(), inputMap);
hashAccessor(sampler.getOutput(), outputMap);
}
}
// Add accessor to the appropriate hash group. Hashes are _non-unique_,
// intended to quickly compare everything accept the underlying array.
function hashAccessor(accessor, group) {
if (!accessor) return;
const hash = [accessor.getCount(), accessor.getType(), accessor.getComponentType(), accessor.getNormalized(), accessor.getSparse()].join(':');
let hashSet = group.get(hash);
if (!hashSet) group.set(hash, hashSet = new Set());
hashSet.add(accessor);
}
// Find duplicate accessors of a given type.
function detectDuplicates(accessors, duplicates) {
for (let i = 0; i < accessors.length; i++) {
const a = accessors[i];
const aData = core.BufferUtils.toView(a.getArray());
if (duplicates.has(a)) continue;
for (let j = i + 1; j < accessors.length; j++) {
const b = accessors[j];
if (duplicates.has(b)) continue;
// Just compare the arrays — everything else was covered by the
// hash. Comparing uint8 views is faster than comparing the
// original typed arrays.
if (core.BufferUtils.equals(aData, core.BufferUtils.toView(b.getArray()))) {
duplicates.set(b, a);
}
}
}
}
let total = 0;
const duplicates = new Map();
for (const group of [attributeMap, indicesMap, inputMap, outputMap]) {
for (const hashGroup of group.values()) {
total += hashGroup.size;
detectDuplicates(Array.from(hashGroup), duplicates);
}
}
logger.debug(`${NAME$p}: Merged ${duplicates.size} of ${total} accessors.`);
// Dissolve duplicate vertex attributes and indices.
meshes.forEach(mesh => {
mesh.listPrimitives().forEach(primitive => {
primitive.listAttributes().forEach(accessor => {
if (duplicates.has(accessor)) {
primitive.swap(accessor, duplicates.get(accessor));
}
});
const indices = primitive.getIndices();
if (indices && duplicates.has(indices)) {
primitive.swap(indices, duplicates.get(indices));
}
});
});
// Dissolve duplicate animation sampler inputs and outputs.
for (const animation of document.getRoot().listAnimations()) {
for (const sampler of animation.listSamplers()) {
const input = sampler.getInput();
const output = sampler.getOutput();
if (input && duplicates.has(input)) {
sampler.swap(input, duplicates.get(input));
}
if (output && duplicates.has(output)) {
sampler.swap(output, duplicates.get(output));
}
}
}
Array.from(duplicates.keys()).forEach(accessor => accessor.dispose());
}
function dedupMeshes(document, options) {
const logger = document.getLogger();
const root = document.getRoot();
// Create Reference -> ID lookup table.
const refs = new Map();
root.listAccessors().forEach((accessor, index) => refs.set(accessor, index));
root.listMaterials().forEach((material, index) => refs.set(material, index));
// For each mesh, create a hashkey.
const numMeshes = root.listMeshes().length;
const uniqueMeshes = new Map();
for (const src of root.listMeshes()) {
// For each mesh, create a hashkey.
const srcKeyItems = [];
for (const prim of src.listPrimitives()) {
srcKeyItems.push(createPrimitiveKey(prim, refs));
}
// If another mesh exists with the same key, replace all instances with that, and dispose
// of the duplicate. If not, just cache it.
let meshKey = '';
if (options.keepUniqueNames) meshKey += src.getName() + ';';
meshKey += srcKeyItems.join(';');
if (uniqueMeshes.has(meshKey)) {
const targetMesh = uniqueMeshes.get(meshKey);
src.listParents().forEach(parent => {
if (parent.propertyType !== core.PropertyType.ROOT) {
parent.swap(src, targetMesh);
}
});
src.dispose();
} else {
uniqueMeshes.set(meshKey, src);
}
}
logger.debug(`${NAME$p}: Merged ${numMeshes - uniqueMeshes.size} of ${numMeshes} meshes.`);
}
function dedupImages(document, options) {
const logger = document.getLogger();
const root = document.getRoot();
const textures = root.listTextures();
const duplicates = new Map();
// Compare each texture to every other texture — O(n²) — and mark duplicates for replacement.
for (let i = 0; i < textures.length; i++) {
const a = textures[i];
const aData = a.getImage();
if (duplicates.has(a)) continue;
for (let j = i + 1; j < textures.length; j++) {
const b = textures[j];
const bData = b.getImage();
if (duplicates.has(b)) continue;
// URIs are intentionally not compared.
if (a.getMimeType() !== b.getMimeType()) continue;
if (options.keepUniqueNames && a.getName() !== b.getName()) continue;
const aSize = a.getSize();
const bSize = b.getSize();
if (!aSize || !bSize) continue;
if (aSize[0] !== bSize[0]) continue;
if (aSize[1] !== bSize[1]) continue;
if (!aData || !bData) continue;
if (core.BufferUtils.equals(aData, bData)) {
duplicates.set(b, a);
}
}
}
logger.debug(`${NAME$p}: Merged ${duplicates.size} of ${root.listTextures().length} textures.`);
Array.from(duplicates.entries()).forEach(([src, dst]) => {
src.listParents().forEach(property => {
if (!(property instanceof core.Root)) property.swap(src, dst);
});
src.dispose();
});
}
function dedupMaterials(document, options) {
const logger = document.getLogger();
const root = document.getRoot();
const materials = root.listMaterials();
const duplicates = new Map();
const modifierCache = new Map();
const skip = new Set();
if (!options.keepUniqueNames) {
skip.add('name');
}
// Compare each material to every other material — O(n²) — and mark duplicates for replacement.
for (let i = 0; i < materials.length; i++) {
const a = materials[i];
if (duplicates.has(a)) continue;
if (hasModifier(a, modifierCache)) continue;
for (let j = i + 1; j < materials.length; j++) {
const b = materials[j];
if (duplicates.has(b)) continue;
if (hasModifier(b, modifierCache)) continue;
if (a.equals(b, skip)) {
duplicates.set(b, a);
}
}
}
logger.debug(`${NAME$p}: Merged ${duplicates.size} of ${materials.length} materials.`);
Array.from(duplicates.entries()).forEach(([src, dst]) => {
src.listParents().forEach(property => {
if (!(property instanceof core.Root)) property.swap(src, dst);
});
src.dispose();
});
}
function dedupSkins(document, options) {
const logger = document.getLogger();
const root = document.getRoot();
const skins = root.listSkins();
const duplicates = new Map();
const skip = new Set(['joints']);
if (!options.keepUniqueNames) {
skip.add('name');
}
for (let i = 0; i < skins.length; i++) {
const a = skins[i];
if (duplicates.has(a)) continue;
for (let j = i + 1; j < skins.length; j++) {
const b = skins[j];
if (duplicates.has(b)) continue;
// Check joints with shallow equality, not deep equality.
// See: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/RecursiveSkeletons
if (a.equals(b, skip) && shallowEqualsArray(a.listJoints(), b.listJoints())) {
duplicates.set(b, a);
}
}
}
logger.debug(`${NAME$p}: Merged ${duplicates.size} of ${skins.length} skins.`);
Array.from(duplicates.entries()).forEach(([src, dst]) => {
src.listParents().forEach(property => {
if (!(property instanceof core.Root)) property.swap(src, dst);
});
src.dispose();
});
}
/** Generates a key unique to the content of a primitive or target. */
function createPrimitiveKey(prim, refs) {
const primKeyItems = [];
for (const semantic of prim.listSemantics()) {
const attribute = prim.getAttribute(semantic);
primKeyItems.push(semantic + ':' + refs.get(attribute));
}
if (prim instanceof core.Primitive) {
const indices = prim.getIndices();
if (indices) {
primKeyItems.push('indices:' + refs.get(indices));
}
const material = prim.getMaterial();
if (material) {
primKeyItems.push('material:' + refs.get(material));
}
primKeyItems.push('mode:' + prim.getMode());
for (const target of prim.listTargets()) {
primKeyItems.push('target:' + createPrimitiveKey(target, refs));
}
}
return primKeyItems.join(',');
}
/**
* Detects dependencies modified by a parent reference, to conservatively prevent merging. When
* implementing extensions like KHR_animation_pointer, the 'modifyChild' attribute should be added
* to graph edges connecting the animation channel to the animated target property.
*
* NOTICE: Implementation is conservative, and could prevent merging two materials sharing the
* same animated "Clearcoat" ExtensionProperty. While that scenario is possible for an in-memory
* glTF Transform graph, valid glTF input files do not have that risk.
*/
function hasModifier(prop, cache) {
if (cache.has(prop)) return cache.get(prop);
const graph = prop.getGraph();
const visitedNodes = new Set();
const edgeQueue = graph.listParentEdges(prop);
// Search dependency subtree for 'modifyChild' attribute.
while (edgeQueue.length > 0) {
const edge = edgeQueue.pop();
if (edge.getAttributes().modifyChild === true) {
cache.set(prop, true);
return true;
}
const child = edge.getChild();
if (visitedNodes.has(child)) continue;
for (const childEdge of graph.listChildEdges(child)) {
edgeQueue.push(childEdge);
}
}
cache.set(prop, false);
return false;
}
/**
* 4 Dimensional Vector
* @module vec4
*/
/**
* Creates a new, empty vec4
*
* @returns {vec4} a new 4D vector
*/
function create() {
var out = new ARRAY_TYPE(4);
if (ARRAY_TYPE != Float32Array) {
out[0] = 0;
out[1] = 0;
out[2] = 0;
out[3] = 0;
}
return out;
}
/**
* Adds two vec4's
*
* @param {vec4} out the receiving vector
* @param {ReadonlyVec4} a the first operand
* @param {ReadonlyVec4} b the second operand
* @returns {vec4} out
*/
function add(out, a, b) {
out[0] = a[0] + b[0];
out[1] = a[1] + b[1];
out[2] = a[2] + b[2];
out[3] = a[3] + b[3];
return out;
}
/**
* Subtracts vector b from vector a
*
* @param {vec4} out the receiving vector
* @param {ReadonlyVec4} a the first operand
* @param {ReadonlyVec4} b the second operand
* @returns {vec4} out
*/
function subtract(out, a, b) {
out[0] = a[0] - b[0];
out[1] = a[1] - b[1];
out[2] = a[2] - b[2];
out[3] = a[3] - b[3];
return out;
}
/**
* Multiplies two vec4's
*
* @param {vec4} out the receiving vector
* @param {ReadonlyVec4} a the first operand
* @param {ReadonlyVec4} b the second operand
* @returns {vec4} out
*/
function multiply(out, a, b) {
out[0] = a[0] * b[0];
out[1] = a[1] * b[1];
out[2] = a[2] * b[2];
out[3] = a[3] * b[3];
return out;
}
/**
* Scales a vec4 by a scalar number
*
* @param {vec4} out the receiving vector
* @param {ReadonlyVec4} a the vector to scale
* @param {Number} b amount to scale the vector by
* @returns {vec4} out
*/
function scale(out, a, b) {
out[0] = a[0] * b;
out[1] = a[1] * b;
out[2] = a[2] * b;
out[3] = a[3] * b;
return out;
}
/**
* Calculates the length of a vec4
*
* @param {ReadonlyVec4} 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];
var w = a[3];
return Math.hypot(x, y, z, w);
}
/**
* Alias for {@link vec4.subtract}
* @function
*/
var sub = subtract;
/**
* Alias for {@link vec4.multiply}
* @function
*/
var mul = multiply;
/**
* Alias for {@link vec4.length}
* @function
*/
var len = length;
/**
* Perform some operation over an array of vec4s.
*
* @param {Array} a the array of vectors to iterate over
* @param {Number} stride Number of elements between the start of each vec4. If 0 assumes tightly packed
* @param {Number} offset Number of elements to skip at the beginning of the array
* @param {Number} count Number of vec4s 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 = 4;
}
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];
vec[3] = a[i + 3];
fn(vec, vec, arg);
a[i] = vec[0];
a[i + 1] = vec[1];
a[i + 2] = vec[2];
a[i + 3] = vec[3];
}
return a;
};
})();
const SRGB_PATTERN = /color|emissive|diffuse/i;
/**
* Returns the color space (if any) implied by the {@link Material} slots to
* which a texture is assigned, or null for non-color textures. If the texture
* is not connected to any {@link Material}, this function will also return
* null — any metadata in the image file will be ignored.
*
* Under current glTF specifications, only 'srgb' and non-color (null) textures
* are used.
*
* Example:
*
* ```typescript
* import { getTextureColorSpace } from '@gltf-transform/functions';
*
* const baseColorTexture = material.getBaseColorTexture();
* const normalTexture = material.getNormalTexture();
*
* getTextureColorSpace(baseColorTexture); // → 'srgb'
* getTextureColorSpace(normalTexture); // → null
* ```
*/
function getTextureColorSpace(texture) {
const graph = texture.getGraph();
const edges = graph.listParentEdges(texture);
const isSRGB = edges.some(edge => {
return edge.getAttributes().isColor || SRGB_PATTERN.test(edge.getName());
});
return isSRGB ? 'srgb' : null;
}
/**
* Lists all {@link TextureInfo} definitions associated with a given
* {@link Texture}. May be used to determine which UV transforms
* and texCoord indices are applied to the material, without explicitly
* checking the material properties and extensions.
*
* Example:
*
* ```typescript
* // Find TextureInfo instances associated with the texture.
* const results = listTextureInfo(texture);
*
* // Find which UV sets (TEXCOORD_0, TEXCOORD_1, ...) are required.
* const texCoords = results.map((info) => info.getTexCoord());
* // → [0, 1]
* ```
*/
function listTextureInfo(texture) {
const graph = texture.getGraph();
const results = new Set();
for (const textureEdge of graph.listParentEdges(texture)) {
const parent = textureEdge.getParent();
const name = textureEdge.getName() + 'Info';
for (const edge of graph.listChildEdges(parent)) {
const child = edge.getChild();
if (child instanceof core.TextureInfo && edge.getName() === name) {
results.add(child);
}
}
}
return Array.from(results);
}
/**
* Lists all {@link TextureInfo} definitions associated with any {@link Texture}
* on the given {@link Material}. May be used to determine which UV transforms
* and texCoord indices are applied to the material, without explicitly
* checking the material properties and extensions.
*
* Example:
*
* ```typescript
* const results = listTextureInfoByMaterial(material);
*
* const texCoords = results.map((info) => info.getTexCoord());
* // → [0, 1]
* ```
*/
function listTextureInfoByMaterial(material) {
const graph = material.getGraph();
const visited = new Set();
const results = new Set();
function traverse(prop) {
const textureInfoNames = new Set();
for (const edge of graph.listChildEdges(prop)) {
if (edge.getChild() instanceof core.Texture) {
textureInfoNames.add(edge.getName() + 'Info');
}
}
for (const edge of graph.listChildEdges(prop)) {
const child = edge.getChild();
if (visited.has(child)) continue;
visited.add(child);
if (child instanceof core.TextureInfo && textureInfoNames.has(edge.getName())) {
results.add(child);
} else if (child instanceof core.ExtensionProperty) {
traverse(child);
}
}
}
traverse(material);
return Array.from(results);
}
/**
* Returns names of all texture slots using the given texture.
*
* Example:
*
* ```js
* const slots = listTextureSlots(texture);
* // → ['occlusionTexture', 'metallicRoughnessTexture']
* ```
*/
function listTextureSlots(texture) {
const document = core.Document.fromGraph(texture.getGraph());
const root = document.getRoot();
const slots = texture.getGraph().listParentEdges(texture).filter(edge => edge.getParent() !== root).map(edge => edge.getName());
return Array.from(new Set(slots));
}
function _catch(body, recover) {
try {
var result = body();
} catch (e) {
return recover(e);
}
if (result && result.then) {
return result.then(void 0, recover);
}
return result;
}
const maybeGetPixels = function (texture) {
return Promise.resolve(_catch(function () {
return Promise.resolve(ndarrayPixels.getPixels(texture.getImage(), texture.getMimeType()));
}, function () {
return null;
}));
};
const getTextureFactor = function (texture) {
return Promise.resolve(maybeGetPixels(texture)).then(function (pixels) {
if (!pixels) return null;
const min = [Infinity, Infinity, Infinity, Infinity];
const max = [-Infinity, -Infinity, -Infinity, -Infinity];
const target = [0, 0, 0, 0];
const [width, height] = pixels.shape;
for (let i = 0; i < width; i++) {
for (let j = 0; j < height; j++) {
for (let k = 0; k < 4; k++) {
min[k] = Math.min(min[k], pixels.get(i, j, k));
max[k] = Math.max(max[k], pixels.get(i, j, k));
}
}
if (len(sub(target, max, min)) / 255 > EPS) {
return null;
}
}
return scale(target, add(target, max, min), 0.5 / 255);
});
};
/**********************************************************************************************
* Prune solid (single-color) textures.
*/
const pruneSolidTextures = function (document) {
try {
const root = document.getRoot();
const graph = document.getGraph();
const logger = document.getLogger();
const textures = root.listTextures();
const pending = textures.map(function (texture) {
return Promise.resolve(getTextureFactor(texture)).then(function (factor) {
var _texture$getSize;
if (!factor) return;
if (getTextureColorSpace(texture) === 'srgb') {
core.ColorUtils.convertSRGBToLinear(factor, factor);
}
const name = texture.getName() || texture.getURI();
const size = (_texture$getSize = texture.getSize()) == null ? void 0 : _texture$getSize.join('x');
const slots = listTextureSlots(texture);
for (const edge of graph.listParentEdges(texture)) {
const parent = edge.getParent();
if (parent !== root && applyMaterialFactor(parent, factor, edge.getName(), logger)) {
edge.dispose();
}
}
if (texture.listParents().length === 1) {
texture.dispose();
logger.debug(`${NAME$o}: Removed solid-color texture "${name}" (${size}px ${slots.join(', ')})`);
}
});
});
return Promise.resolve(Promise.all(pending)).then(function () {});
} catch (e) {
return Promise.reject(e);
}
};
const NAME$o = 'prune';
const EPS = 3 / 255;
const PRUNE_DEFAULTS = {
propertyTypes: [core.PropertyType.NODE, core.PropertyType.SKIN, core.PropertyType.MESH, core.PropertyType.CAMERA, core.PropertyType.PRIMITIVE, core.PropertyType.PRIMITIVE_TARGET, core.PropertyType.ANIMATION, core.PropertyType.MATERIAL, core.PropertyType.TEXTURE, core.PropertyType.ACCESSOR, core.PropertyType.BUFFER],
keepLeaves: false,
keepAttributes: false,