VertexPackage.js

'use strict';

const COMPONENT_TYPE = 'FLOAT';
const BYTES_PER_COMPONENT = 4;

/**
 * Removes invalid attribute arguments. A valid argument must be an Array of length > 0 key by a string representing an int.
 *
 * @private
 *
 * @param {Object} attributes - The map of vertex attributes.
 *
 * @returns {Array} The valid array of arguments.
 */
function parseAttributeMap(attributes) {
	const goodAttributes = [];
	Object.keys(attributes).forEach(key => {
		const index = parseFloat(key);
		// check that key is an valid integer
		if (!Number.isInteger(index) || index < 0) {
			throw `Attribute index \`${key}\` does not represent a valid integer`;
		}
		const vertices = attributes[key];
		// ensure attribute is valid
		if (Array.isArray(vertices) && vertices.length > 0) {
			// add attribute data and index
			goodAttributes.push({
				index: index,
				data: vertices
			});
		} else {
			throw `Error parsing attribute of index \`${index}\``;
		}
	});
	// sort attributes ascending by index
	goodAttributes.sort((a, b) => {
		return a.index - b.index;
	});
	return goodAttributes;
}

/**
 * Returns a component's byte size.
 *
 * @private
 *
 * @param {Object|Array} component - The component to measure.
 *
 * @returns {number} The byte size of the component.
 */
function getComponentSize(component) {
	// check if array
	if (Array.isArray(component)) {
		return component.length;
	}
	// check if vector
	if (component.x !== undefined) {
		// 1 component vector
		if (component.y !== undefined) {
			// 2 component vector
			if (component.z !== undefined) {
				// 3 component vector
				if (component.w !== undefined) {
					// 4 component vector
					return 4;
				}
				return 3;
			}
			return 2;
		}
		return 1;
	}
	// single component
	return 1;
}

/**
 * Calculates the type, size, and offset for each attribute in the attribute
 * array along with the length and stride of the package.
 *
 * @private
 *
 * @param {VertexPackage} vertexPackage - The VertexPackage object.
 * @param {Array} attributes - The array of vertex attributes.
 */
function setPointersAndStride(vertexPackage, attributes) {
	let shortestArray = Number.MAX_VALUE;
	let offset = 0;
	// for each attribute
	attributes.forEach(vertices => {
		// set size to number of components in the attribute
		const size = getComponentSize(vertices.data[0]);
		// length of the package will be the shortest attribute array length
		shortestArray = Math.min(shortestArray, vertices.data.length);
		// store pointer under index
		vertexPackage.pointers[vertices.index] = {
			type: COMPONENT_TYPE,
			size: size,
			byteOffset: offset * BYTES_PER_COMPONENT
		};
		// accumulate attribute offset
		offset += size;
	});
	// set stride to total offset
	vertexPackage.stride = offset; // not in bytes
	// set length of package to the shortest attribute array length
	vertexPackage.length = shortestArray;
}

/**
 * Fill the arraybuffer with a single component attribute.
 *
 * @private
 *
 * @param {Float32Array} buffer - The arraybuffer to fill.
 * @param {Array} vertices - The vertex attribute array to copy from.
 * @param {number} length - The length of the buffer to copy from.
 * @param {number} offset - The offset to the attribute, not in bytes.
 * @param {number} stride - The stride of the buffer, not in bytes.
 */
function set1ComponentAttr(buffer, vertices, length, offset, stride) {
	for (let i=0; i<length; i++) {
		const vertex = vertices[i];
		// get the index in the buffer to the particular vertex
		const j = offset + (stride * i);
		if (vertex.x !== undefined) {
			buffer[j] = vertex.x;
		} else if (vertex[0] !== undefined) {
			buffer[j] = vertex[0];
		} else {
			buffer[j] = vertex;
		}
	}
}

/**
 * Fill the arraybuffer with a double component attribute.
 *
 * @private
 *
 * @param {Float32Array} buffer - The arraybuffer to fill.
 * @param {Array} vertices - The vertex attribute array to copy from.
 * @param {number} length - The length of the buffer to copy from.
 * @param {number} offset - The offset to the attribute, not in bytes.
 * @param {number} stride - The stride of the buffer, not in bytes.
 */
function set2ComponentAttr(buffer, vertices, length, offset, stride) {
	for (let i=0; i<length; i++) {
		const vertex = vertices[i];
		// get the index in the buffer to the particular vertex
		const j = offset + (stride * i);
		buffer[j] = (vertex.x !== undefined) ? vertex.x : vertex[0];
		buffer[j+1] = (vertex.y !== undefined) ? vertex.y : vertex[1];
	}
}

/**
 * Fill the arraybuffer with a triple component attribute.
 *
 * @private
 *
 * @param {Float32Array} buffer - The arraybuffer to fill.
 * @param {Array} vertices - The vertex attribute array to copy from.
 * @param {number} length - The length of the buffer to copy from.
 * @param {number} offset - The offset to the attribute, not in bytes.
 * @param {number} stride - The stride of the buffer, not in bytes.
 */
function set3ComponentAttr(buffer, vertices, length, offset, stride) {
	for (let i=0; i<length; i++) {
		const vertex = vertices[i];
		// get the index in the buffer to the particular vertex
		const j = offset + (stride * i);
		buffer[j] = (vertex.x !== undefined) ? vertex.x : vertex[0];
		buffer[j+1] = (vertex.y !== undefined) ? vertex.y : vertex[1];
		buffer[j+2] = (vertex.z !== undefined) ? vertex.z : vertex[2];
	}
}

/**
 * Fill the arraybuffer with a quadruple component attribute.
 *
 * @private
 *
 * @param {Float32Array} buffer - The arraybuffer to fill.
 * @param {Array} vertices - The vertex attribute array to copy from.
 * @param {number} length - The length of the buffer to copy from.
 * @param {number} offset - The offset to the attribute, not in bytes.
 * @param {number} stride - The stride of the buffer, not in bytes.
 */
function set4ComponentAttr(buffer, vertices, length, offset, stride) {
	for (let i=0; i<length; i++) {
		const vertex = vertices[i];
		// get the index in the buffer to the particular vertex
		const j = offset + (stride * i);
		buffer[j] = (vertex.x !== undefined) ? vertex.x : vertex[0];
		buffer[j+1] = (vertex.y !== undefined) ? vertex.y : vertex[1];
		buffer[j+2] = (vertex.z !== undefined) ? vertex.z : vertex[2];
		buffer[j+3] = (vertex.w !== undefined) ? vertex.w : vertex[3];
	}
}

/**
 * A vertex package to assist in interleaving vertex data and building the
 * associated vertex attribute pointers.
 */
class VertexPackage {

	/**
	 * Instantiates a VertexPackage object.
	  *
	 * @param {Object} attributes - The attributes to interleave keyed by index.
	 */
	constructor(attributes) {
		this.stride = 0;
		this.length = 0;
		this.buffer = null;
		this.pointers = {};
		if (attributes) {
			this.set(attributes);
		}
	}

	/**
	 * Set the data to be interleaved inside the package. This clears any
	 * previously existing data.
	 *
	 * @param {Object} attributes - The attributes to interleaved, keyed by index.
	 *
	 * @returns {VertexPackage} The vertex package object, for chaining.
	 */
	set(attributes) {
		// remove bad attributes
		attributes = parseAttributeMap(attributes);
		// set attribute pointers and stride
		setPointersAndStride(this, attributes);
		// set size of data vector
		const length = this.length;
		const stride = this.stride; // not in bytes
		const pointers = this.pointers;
		const buffer = this.buffer = new Float32Array(length * stride);
		// for each vertex attribute array
		attributes.forEach(vertices => {
			// get the pointer
			const pointer = pointers[vertices.index];
			// get the pointers offset
			const offset = pointer.byteOffset / BYTES_PER_COMPONENT;
			// copy vertex data into arraybuffer
			switch (pointer.size) {
				case 2:
					set2ComponentAttr(buffer, vertices.data, length, offset, stride);
					break;
				case 3:
					set3ComponentAttr(buffer, vertices.data, length, offset, stride);
					break;
				case 4:
					set4ComponentAttr(buffer, vertices.data, length, offset, stride);
					break;
				default:
					set1ComponentAttr(buffer, vertices.data, length, offset, stride);
					break;
			}
		});
		return this;
	}
}

module.exports = VertexPackage;