﻿ //
 // This function receives device information from Device Central,
 // creates a new (uniquely named) comp for each device, and
 // (if selected by Device Central user) creates a master content comp
 // which is nested and centered within each of the device comps.
 //
 // It also creates preview comps, displaying the device comps in a 3x3
 // grid for easy visual comparison by the user.
 //
 // In addition, for each device comp, a guide layer precomp is added
 // to the master content comp to highlight the portion of the master comp
 // visible in the corresponding device comp. Expressions within the
 // guide layer comps link the guide layers "area of interest" to any 
 // transforms applied to the master content comp within the corresponding
 // device comp. 
 //

 function DeviceCentralImporter(jsonString)
 {

	// function to determine if a particular comp name already exists in the project bin
	//
	function compNameInUse(theName)
	{
		for (var i = 1; i <= app.project.numItems; i++){
			if (app.project.item(i) instanceof CompItem && app.project.item(i).name == theName){
				return true;
			}
		}
		return false;
	}

	// function to ensure comp name is unique, by appending a numeric suffix if necessary 
	//
	function getUniqueCompName(theCompName)
	{
		if (! compNameInUse(theCompName)){
			return theCompName;
		}
		var suffix = 2;

		while (compNameInUse(theCompName + " (" + suffix + ")")){
			suffix++;
		}
		return (theCompName + " (" + suffix + ")");
	}

	// function to create (or find, if it already exists) a folder for the comps created by this script
	//
	function getFolder(theName, theParentFolder)
	{
		for (var i = 1; i <= app.project.numItems; i++){
			if (app.project.item(i) instanceof FolderItem && app.project.item(i).name == theName){
				return app.project.item(i);
			}
		}
		var newFolder = app.project.items.addFolder(theName);
		newFolder.parentFolder = theParentFolder;
		return newFolder;			
	}

	// function to clamp value within limits
	//
	function clampVal (theValue, theLowLim, theHighLim)
	{
		return Math.min(Math.max(theValue, theLowLim), theHighLim);
	}

	// convert JSON string from Device Central to local object containing device info and master comp flag
	//
	var btReq = eval("(" + jsonString + ")");

	defaults = {
		compDur : 10,				// default comp duration
		bgColor : [0,0,0],			// default background color
		PAR : 1.0,				// default pixel aspect ratio
		masterCompName : {en: "Device Master"},	// master comp name
		folderName : {en: "Device Central Comps"},	// folder name for comps created by this script
		previews : {
			compName : {en: "Preview"},	// preview comp name
			horizCells : 3,			// number of horizontal cells in preview
			vertCells : 3,			// number of vertical cells in preview
			left : 21,			// padding on left (pixels)
			right : 21,			// padding on right
			top : 31,			// padding at top
			bottom : 21,			// padding at bottom
			vertPadding : 32,		// horizontal padding between cells
			horizPadding : 12,		// horizontal padding between cells
			bgColor : [0, 0, 0],		// preview comp background color
			strokeColor: [0.5, 0.5, 0.5],	// preview grid layer stroke color
			strokeWidth: 2.0,		// preview grid layer stroke width
			fillColor: [0, 0, 0],		// preview grid layer fill color
			textOffset : [0, -5],		// offset of device name from cell UL corner (pixels)
			textFontName : "MyriadPro-Regular",	// device name font
			textFontSize : 12,		// device name font size
			gridLayerName: {en: "Grid"}	// preview grid layer name
		},
		guides : {
			compSuffix : {en: " - guide"},
			folderName : {en: "Guide Comps"},
			maskLayerName : {en: "Mask Layer"},
			overlayLayerName : {en: "Overlay Layer"},
			color : [0.74, 0.0, 0.0],
			minOpacity: 40,
			blendMode : BlendingMode.NORMAL
		}

	};

	limits = { // limits for range checking
		compSize: {width: {min: 4, max: 30000}, height: {min: 4, max: 30000} },
		PAR: {min: 0.01, max: 100.0},
		FPS: {min: 1.0, max: 99.0}
	};

	// make sure data for at least one device was passed from Device Central
	//		
	if (btReq.devices.length > 0){

		app.beginUndoGroup("create comps"); // create undo group

		var newComp;
		var masterComp;
		var masterCompLayer;
		var s; // scale for master comp layer in device comp
		var compFolder = getFolder(localize(defaults.folderName), app.project.rootFolder);

		// assume minimums for comp size and FPS
		//
		var maxWidth = limits.compSize.width.min;
		var maxHeight = limits.compSize.height.min;
		var maxFPS = limits.FPS.min;

		var deviceComps = [];

		// find maximum device width, height, and frame rate (to use for master and preview comps)
		//   also, limit to maximum allowable value
		//
		for (var i = 0; i < btReq.devices.length; i++){
			maxWidth = clampVal(btReq.devices[i].addressableSize.width, maxWidth, limits.compSize.width.max);
			maxHeight = clampVal(btReq.devices[i].addressableSize.height, maxHeight, limits.compSize.height.max);
			maxFPS = clampVal(btReq.devices[i].frameRateFPS, maxFPS, limits.FPS.max);
		}

		// create master content comp only if requested by user
		//
		if (btReq.createMasterComp){

			// create master content comp
			//
			masterComp = app.project.items.addComp(
				getUniqueCompName(localize(defaults.masterCompName)),
				maxWidth,
				maxHeight,
				defaults.PAR,
				defaults.compDur,
				maxFPS);

			masterComp.bgColor = defaults.bgColor;
			masterComp.parentFolder = compFolder;
		}
						

		// create comp for each device
		//
		for (var i = 0; i < btReq.devices.length; i++){
			newComp = app.project.items.addComp(
				getUniqueCompName(btReq.devices[i].deviceName),
				btReq.devices[i].addressableSize.width,
				btReq.devices[i].addressableSize.height,
				clampVal(btReq.devices[i].PAR, limits.PAR.min, limits.PAR.max),
				defaults.compDur,
				btReq.devices[i].frameRateFPS);

			newComp.bgColor = defaults.bgColor;
			newComp.parentFolder = compFolder;
			deviceComps[i] = newComp;

			if (btReq.createMasterComp){
				// add master comp as layer to each device comp, and scale to maximize content
				//
				masterCompLayer = newComp.layers.add(masterComp, defaults.compDur);
				s = Math.max((newComp.width * newComp.pixelAspect)/masterComp.width, newComp.height/masterComp.height)*100;
				masterCompLayer.property("ADBE Transform Group").property("ADBE Scale").setValue([s, s]);
				masterCompLayer.selected = false;
			}

		}

		// build preview comp(s) if more than one device
		// 
		if (deviceComps.length > 1){
			var previewComp;
			var previewLayer;
			var padLayer;
			var shapeLayer;
			var textLayer;
			var textSource;
			var textDoc;
			var shapeLayerContents;
			var shapeGroup;
			var curRect;
			var pathMerge;
			var shapeFill;
			var shapeStroke;
			var previewWidth = maxWidth * defaults.previews.horizCells + defaults.previews.horizPadding * (defaults.previews.horizCells - 1) + defaults.previews.left + defaults.previews.right;
			var previewHeight = maxHeight * defaults.previews.vertCells + defaults.previews.vertPadding * (defaults.previews.vertCells - 1) + defaults.previews.top + defaults.previews.bottom;
			var numPreviewComps = Math.ceil(deviceComps.length/(defaults.previews.horizCells * defaults.previews.vertCells));
			var origin = [defaults.previews.left + maxWidth/2, defaults.previews.top + maxHeight/2];
			var curPosition;
			var deviceIndex = 0; // running index into deviceComps[]
			var x;
			var y;
			var textX;
			var textY;

			for (var i = 1; i <= numPreviewComps; i++){

				// create preview comp
				//
				previewComp = app.project.items.addComp(
					getUniqueCompName(localize(defaults.previews.compName)),
					previewWidth,
					previewHeight,
					defaults.PAR,
					defaults.compDur,
					maxFPS);

				previewComp.bgColor = defaults.previews.bgColor;
				previewComp.parentFolder = compFolder;

				// add grid shape layer for padding between devices
				//
				shapeLayer = previewComp.layers.addShape();
				shapeLayer.name = localize(defaults.previews.gridLayerName);
				shapeLayer.transform.position.setValue([0, 0]); // move to comp's UL corner for convenience
				shapeLayerContents = shapeLayer.property("ADBE Root Vectors Group");
				shapeGroup = shapeLayerContents.addProperty("ADBE Vector Group");

				// fill the preview comp grid with device comps
				//
				for (var j = 0; j < defaults.previews.horizCells; j++){

					for (var k = 0; k < defaults.previews.vertCells; k++){
						x = origin[0] + (defaults.previews.horizPadding + maxWidth) * k; 
						y = origin[1] + (defaults.previews.vertPadding + maxHeight) * j; 
						if (deviceIndex < deviceComps.length){

							// add text layer with device comp name
							//
							textLayer = previewComp.layers.addText(deviceComps[deviceIndex].name);
							textX = Math.round(x - maxWidth/2 + defaults.previews.textOffset[0]);
							textY = Math.round(y - maxHeight/2 + defaults.previews.textOffset[1]);
							textLayer.property("ADBE Transform Group").property("ADBE Position").setValue([textX,textY]);
							textLayer.moveToEnd();
							sourceText = textLayer.property("ADBE Text Properties").property("ADBE Text Document");
							textDoc = sourceText.value;
							textDoc.resetCharStyle();
							textDoc.resetParagraphStyle();
							textDoc.font = defaults.previews.textFontName;;
							textDoc.fontSize = defaults.previews.textFontSize;
							textDoc.applyStroke = false;
							textDoc.applyFill = true;
							textDoc.justification = ParagraphJustification.LEFT_JUSTIFY;
							sourceText.setValue(textDoc);

							// add device comp
							//
							previewLayer = previewComp.layers.add(deviceComps[deviceIndex]);
							previewLayer.property("ADBE Transform Group").property("ADBE Position").setValue([x,y]);
							previewLayer.property("ADBE Transform Group").property("ADBE Scale").setValue([100/previewLayer.source.pixelAspect,100]);
							previewLayer.moveToEnd();
							previewLayer.selected = false;
							deviceIndex++;
						}

						// add padding grid rectangle for this device comp
						//
						curRect = shapeGroup.property("ADBE Vectors Group").addProperty("ADBE Vector Shape - Rect");
						curRect.property("ADBE Vector Rect Size").setValue([maxWidth, maxHeight]);
						curRect.property("ADBE Vector Rect Position").setValue([x, y]);
					}

				}

				// add stroke and fill to padding grid
				//
				shapeFill = shapeGroup.property("ADBE Vectors Group").addProperty("ADBE Vector Graphic - Fill");
				shapeFill.property("ADBE Vector Fill Color").setValue(defaults.previews.fillColor);
				shapeStroke = shapeGroup.property("ADBE Vectors Group").addProperty("ADBE Vector Graphic - Stroke");
				shapeStroke.property("ADBE Vector Stroke Color").setValue(defaults.previews.strokeColor);
				shapeStroke.property("ADBE Vector Stroke Width").setValue(defaults.previews.strokeWidth);
				shapeLayer.moveToEnd();
				shapeLayer.locked = true; // so it doesn't get selected by mistake
			}
		}

		// create guide layers in master comp for each device 
		//
		if (btReq.createMasterComp && (deviceComps.length > 1)){
			var guideComp;
			var maskLayer;
			var overlayLayer;
			var guideLayer;
			var currExpr;
			var expr1;
			var expr2;

			// following are code pieces used to construct position, scale, and rotation expressions
			//

			var compExpr1 = 'C = comp("';
			// device comp name goes here
			var compExpr2 = '");' + '\r';

			var layerExpr1 = 'L = C.layer("';
			// master device comp name goes here
			var layerExpr2 = '");' + '\r';


			var positionExpr = 
				'try{' +'\r' +
				'  L.fromWorld([C.width,C.height]/2)' + '\r' +
				'}catch (err){' + '\r' +
				'  value' + '\r' +
				'}';
  
			var scaleExpr = 
				'try{' + '\r' +
				'  origin = L.fromWorld([0,0]);' + '\r' +
				'  UR = L.fromWorld([C.width, 0]);' + '\r' +
				'  LL = L.fromWorld([0, C.height]);' + '\r' +
				'  x = length(UR, origin)/width;' + '\r' +
				'  y = length(origin, LL)/height;' + '\r' +
				'  [x, y] * 100' + '\r' +
				'}catch (err){' + '\r' +
				'  value' + '\r' +
				'}';

			var rotationExpr = 
				'try{' + '\r' +
				'  origin = L.fromWorld([0,0]);' + '\r' +
				'  UR = L.fromWorld([C.width, 0]);' + '\r' +
				'  v = UR - origin;' + '\r' +
				'  radiansToDegrees (Math.atan2(v[1], v[0]))' + '\r' +
				'}catch (err){' + '\r' +
				'  value' + '\r' +
				'}';

			// create the guide layer comps
			//
			var guideFolder = getFolder (localize(defaults.guides.folderName), compFolder);
			for (var i = 0; i < deviceComps.length; i++){

				// create the comp
				//
				guideComp = app.project.items.addComp(
					deviceComps[i].name + localize(defaults.guides.compSuffix),
					maxWidth,
					maxHeight,
					defaults.PAR,
					defaults.compDur,
					maxFPS);
				guideComp.bgColor = defaults.bgColor;
				guideComp.parentFolder = guideFolder;

				// add mask layer
				//
				maskLayer = guideComp.layers.addSolid(
					[1.0, 1.0, 1.0], // color doesn't matter - just make it white
					localize(defaults.guides.maskLayerName),
					deviceComps[i].width,
					deviceComps[i].height,
					defaults.PAR,
					defaults.compDur);

				// build and apply expressions for position, scale, and rotation to the mask layer
				//
				expr1 = compExpr1 + deviceComps[i].name + compExpr2;
				expr2 = layerExpr1 + masterComp.name + layerExpr2;
				currExpr = expr1 + expr2 + positionExpr;
				maskLayer.property("ADBE Transform Group").property("ADBE Position").expression = currExpr;
				currExpr = expr1 + expr2 + scaleExpr;
				maskLayer.property("ADBE Transform Group").property("ADBE Scale").expression = currExpr;
				currExpr = expr1 + expr2 + rotationExpr;
				maskLayer.property("ADBE Transform Group").property("ADBE Rotate Z").expression = currExpr;

				// add overlay layer
				//
				overlayLayer = guideComp.layers.addSolid(
					defaults.guides.color,
					localize(defaults.guides.overlayLayerName),
					maxWidth,
					maxHeight,
					defaults.PAR,
					defaults.compDur);

				overlayLayer.moveToEnd();
				overlayLayer.property("ADBE Transform Group").property("ADBE Opacity").setValue(Math.max(100/deviceComps.length, defaults.guides.minOpacity));

				overlayLayer.trackMatteType = TrackMatteType.ALPHA_INVERTED;
				overlayLayer.blendingMode = defaults.guides.blendMode;

				// add new guide comp to master device comp
				//
				guideLayer = masterComp.layers.add(guideComp, masterComp.duration);
				guideLayer.guideLayer = true; // make it a guide layer
				guideLayer.moveToEnd();
				guideLayer.selected = false;
			}
		}

		app.endUndoGroup();
			
	}

	app.activate(); // move AE to front
 }

 app.deviceCentralImporter = DeviceCentralImporter;