﻿/**
 * Adds nulls to points of selected shape layers or masks.
 * This only works in AE 15.0 and higher!
 */
(function pointsToNulls () {
	var parentToSource = false, // Change this to true to parent the nulls to the layer instead of using toComp()
		comp = app.project.activeItem;

	if (parseInt(app.version) < 15.0) return;

	app.beginUndoGroup('addNullsToPoints');

	var layers = [];
	forAllSelectedLayersElseAll (comp, function(layer) { layers.push(layer); });

	forAllItemsInArray(layers, function (layer) {
		if (isShapeLayer(layer))
			createNullsAtShapeVertices(layer);
		else
			createNullsAtMaskVertices(layer);
	});

	app.endUndoGroup();

	/**
	 * Creates nulls for each point in each shape path on a given layer
	 *
	 * @param {Layer} layer
	 * @returns {Layer[]} Array of created nulls
	 */
	function createNullsAtShapeVertices (layer) {
		var shapeRoot = layer.property('ADBE Root Vectors Group'),
			createdNulls = [];

		recurseFindShapes(shapeRoot);

		return createdNulls;

		/**
		 * Recursively finds shape paths in a given group & creates nulls
		 *
		 * @param {PropertyGroup} shapeGrp Shape group to find paths in
		 */
		function recurseFindShapes (shapeGrp) {
			/**
			* Loops through each shape on the layer, and creates a null for each point in each shape path it finds
			*
			* @param {PropertyGroup} shapeGrp Shape property group to look in
			*/
			forAllPropsInGroup(shapeGrp, function (shapeItem, ii) {
				if (shapeItem.matchName.indexOf('Group') > -1) {
					recurseFindShapes(shapeItem);
				} else if (shapeItem.matchName == 'ADBE Vector Shape') {
					var shapeNamePath = buildShapeNamePath(shapeItem),
						vertices = shapeItem.value.vertices;

					/**
					* For each vertex in the path:
					*	- Create a new null named appropriately
					*	- Set the label colour to the layer's label colour
					*	- Move it right above our layer
					*	- If 'parentToSource' is enabled, then set the layer as the null's parent
					*	- Set up the expression to link the null to the shape vertex
					*
					* @param {Point[]} vertices Array of vertices
					*/
					forAllItemsInArray(vertices, function (vertex, jj) {
						var vertexNull = addNull(layer.containingComp);
							vertexNull.name = layer.name + ' : ' + shapeItem.propertyGroup(3).name + ' : ' + (jj+1) + '/' + vertices.length;
							vertexNull.label = layer.label;
							vertexNull.moveBefore(layer);

						if (parentToSource)
							vertexNull.parent = layer;

						var vertexPos = vertexNull.property('ADBE Transform Group').property('ADBE Position');
							vertexPos.expression =
								'var layer = thisComp.layer("' + layer.name + '");\n' +
								shapeNamePath + '[' + jj + ']' +
								(parentToSource ? '' : ')');

						createdNulls.push(vertexNull);
					});
				}
			});
		}

		/**
		 * Crawls hierarchy from end path, creating an expression path string
		 *
		 * @param {any} shapeItem
		 * @returns {String} Expression body text, crawling hierarchy
		 */
		function buildShapeNamePath (shapeItem) {
			var itemNames = ['.path'],
				parentGrp = shapeItem.propertyGroup(),
				ctr = 1,
				resultStr = "",
				offsetStr = "",
				ii;

			while (parentGrp) {
				if (parentGrp.name !== 'Contents')
					itemNames.push('.content("' + parentGrp.name + '")');

				parentGrp = parentGrp.propertyGroup();
			}

			for (ii = itemNames.length - 2; ii >= 1; ii--)
				resultStr += 'var grp' + ctr + ' = ' + 'grp' + (ctr++ -1) + itemNames[ii] + ';\n';

			for (ii = 1, il = ctr; ii < il - 1; ii++)
				offsetStr += 'grp' + ii + '.transform.position - grp' + ii + '.transform.anchorPoint + ';

			resultStr = resultStr.replace('grp0', 'layer');
			resultStr += parentToSource ? '' : 'layer.toComp(';
			resultStr += offsetStr + 'grp' + (ctr-1) + '.path.points()';

			return resultStr;
		}
	}

	/**
	 * Creates nulls for each point in each mask on a given layer
	 *
	 * @param {Layer} layer
	 * @returns {Layer[]} Array of created nulls
	 */
	function createNullsAtMaskVertices (layer) {
		var maskGrp = layer.property('ADBE Mask Parade'),
			createdNulls = [];

		/**
		* Loops through each mask on the layer, and creates a null for each point in each mask
		*
		* @param {PropertyGroup} maskGrp Mask property group to look in
		*/
		forAllPropsInGroup(maskGrp, function (mask) {
			var path = mask.property('ADBE Mask Shape'),
				vertices = path.value.vertices;

			/**
			* For each vertex in the path:
			*	- Create a new null named appropriately
			*	- Set the label colour to the layer's label colour
			*	- Move it right above our layer
			*	- If 'parentToSource' is enabled, then set the layer as the null's parent
			*	- Set up the expression to link the null to the mask vertex
			*
			* @param {Point[]} vertices Array of vertices
			*/
			forAllItemsInArray(vertices, function (vertex, ii) {
				var vertexNull = addNull(layer.containingComp);
					vertexNull.name = layer.name + ' : ' + mask.name + ' : ' + (ii+1) + '/' + vertices.length;
					vertexNull.label = layer.label;
					vertexNull.moveBefore(layer);

					if (parentToSource)
						vertexNull.parent = layer;

				var vertexPos = vertexNull.property('ADBE Transform Group').property('ADBE Position');
					vertexPos.expression =
						'var layer = thisComp.layer("' + layer.name + '");\n' +
						(parentToSource ? '' : 'layer.toComp(') +
						'layer.mask("' + mask.name + '").maskPath.points()[' + ii + ']' +
						(parentToSource ? '' : ')');

				createdNulls.push(vertexNull);
			});
		});

		return createdNulls;
	}

	/**
	 * Creates a null in given comp
	 *
	 * @param {CompItem} comp Comp to add null to
	 * @returns {Layer} Newly-created null
	 */
	function addNull (comp) {
		return comp.layers.addNull();
	}

	/**
	 * Checks whether item is a comp
	 *
	 * @param {Item} item Project item to check
	 * @returns {Boolean} Whether the item is a comp
	 */
	function isComp (item) {
		return item instanceof CompItem;
	}

	/**
	 * Checks whether layer is a solid
	 *
	 * @param {Layer} layer Layer to check
	 * @returns {Boolean} Whether the layer is a Solid
	 */
	function isSolidLayer (layer) {
		if (!layer.source) return false;
		return layer.source.mainSource instanceof SolidSource;
	}

	/**
	 * Checks whether layer is a shape layer
	 *
	 * @param {Layer} layer Layer to check
	 * @returns {Boolean} Whether the layer is a shape layer
	 */
	function isShapeLayer (layer) {
		return layer.matchName == 'ADBE Vector Layer';
	}

	/**
	 * Returns active comp, or null if none
	 *
	 * @returns {CompItem} Active Comp
	 */
	 function getActiveComp () {
		var comp = app.project.activeItem;
		if (comp === null || !(isComp(comp))) {
			alert('Please select a composition!');
			return null;
		}
		return comp;
	}

	/**
	 * Iterates through all selected layers, or all layers if none selected
	 *
	 * @param {CompItem} comp Comp to iterate through
	 * @param {callback} func Callback function
	 * @param {{backwards: Boolean}} [options] Options object to recurse forward or backward
	 */
	function forAllSelectedLayersElseAll (comp, func, options) {
		if (comp.selectedLayers.length === 0)
			forAllLayersOfComp(comp, func, options);
		else
			forAllItemsInArray(comp.selectedLayers, func, options);
	}

	/**
	 * Iterates through array items
	 *
	 * @param {Array} array Array to iterate through
	 * @param {callback} func Callback function
	 * @param {{backwards: Boolean}} [options] Options object to recurse forward or backward
	 */
	function forAllItemsInArray (array, func, options) {
		var ii, il;
		options = options || {};

		if (options.backwards === true)
			for (ii = array.length - 1; ii >= 0; ii--)
				func(array[ii], ii);
		else
			for (ii = 0, il = array.length; ii < il; ii++)
				func(array[ii], ii);
	}

	/**
	 * Iterates through property group properties
	 *
	 * @param {PropertyGroup} group Group to iterate through
	 * @param {callback} func Callback function
	 * @param {{backwards: Boolean}} [options] Options object to recurse forward or backward
	 */
	function forAllPropsInGroup (group, func, options) {
		var ii, il;
		options = options || {};

		if (options.backwards === true)
			for (ii = group.numProperties; ii > 0; ii--)
				func(group.property(ii), ii);
		else
			for (ii = 1, il = group.numProperties; ii <= il; ii++)
				func(group.property(ii), ii);
	}

	/**
	 * Iterates through layers of a given comp
	 *
	 * @param {CompItem} comp Comp to iterate through
	 * @param {callback} func Callback function
	 * @param {{backwards: Boolean}} [options] Options object to recurse forward or backward
	 */
	function forAllLayersOfComp (comp, func, options) {
		var ii, il;
		options = options || {};

		if (options.backwards === true)
			for (ii = comp.layers.length - 1; ii >= 0; ii--)
				func(comp.layers[ii], ii);
		else
			for (ii = 1, il = comp.layers.length; ii <= il; ii++)
				func(comp.layers[ii], ii);
	}
})();
