Drawing Paths: The Basics

In my previous blog post, I presented a fairly complete extension panel for InDesign. This time, I’d like to step back a bit and talk about the process of developing an extension for three Creative Suite applications: Illustrator, InDesign, and Photoshop. We’ll be doing something pretty basic–drawing the same shape in all three applications. While we’re at it, we’ll create a framework we can use to draw any valid shape­. This framework will simplify and standardize the process of drawing a shape, and will work for almost any extension that needs to draw shapes.

In this example, we’ll create a generic extension that can run in any of the above applications, and we’ll keep our application specific code isolated from the overall workings of the extension.

It’s All in the (Application-Specific) Details

The trickiest part of this extension, in fact, is the application-specific code. Illustrator, InDesign, and Photoshop can all draw paths, but each of the applications has a slightly different way of performing the task. In all three applications, you can draw paths using the Pen tool, and the resulting shapes, from a scripting point of view, are made up of paths, which are, in turn, made up of path points. The names and the details of the scripting objects differ a bit between products. In Illustrator and Photoshop, for example, the basic drawing object is a pathItem; in InDesign, it’s a pageItem.

Next, the applications have different ways of dealing with measurement. We’ll need to use measurement values to specify horizontal and vertical coordinates if we’re going to position new path points in a document. In InDesign and Photoshop, you can change measurement units at any time; Illustrator, by contrast, always uses points when it’s being driven by a script. In this example, we’ll be using points, but, for your own scripts, you’ll need to convert measurement values to points before sending them to Illustrator.

In InDesign and Photoshop, path point coordinates are specified relative to the ruler zero point; in Illustrator, they’re specified relative to the lower-left corner of the artboard. Since the default location of the ruler zero point is the upper-left corner of the document in all three applications, this means that the paths in Illustrator will be upside down relative to the paths in the other programs, but we don’t need to worry about that right now.

Finally, Illustrator and InDesign have a number of “shortcuts” for drawing paths with specifc arrangements of points–rectangles, ellipses, regular polygons, etc. We’ll ignore those features in favor of a “one size fits all” drawing routine.

In this tutorial, I’ll present support routines that provide a consistent approach to drawing paths in these applications. The idea is to create a generic drawing function that you can use to build new creative effects (ornamental borders for certificates, for example) or wire into your new data driven graphics feature.

You can download the project here (note that this isn’t really a “finished” extension, it’s just a container for the drawing routines):

drawing

Building the Extension

I’ll use the Creative Suite Extension Builder to set up the extension–while you can create the same extension structure without the tooling, I’m lazy, and would prefer to let automation take care of creating the files, adding the required libraries, and editing the XML manifest for the project.

The default panel created by the tooling contains three buttons, one for each application. We’ll edit the XML to use a single button, and we’ll add the AppController and AppModel classes to provide the features and data storage for our extension. Then, we’ll edit the AppController module to take care of making certain that the correct application-specific code gets called. We’ll add a bit of code to the AppModel module to sort out which application is running the extension.

Here’s what main.mxml looks like after we change the user interface.

<?xml version=”1.0” encoding=”utf-8”?>
<mx:Application xmlns:mx=”http://www.adobe.com/2006/mxml” layout=”absolute” historyManagementEnabled=”false” creationComplete=”onCreationComplete()”>
	<mx:Script>
		<![CDATA[
			import com.adobe.csxs.core.CSXSInterface;
			import sample.drawing.AppController;
			import sample.drawing.AppModel;
			[Bindable]
			private var model:AppModel = AppModel.getInstance();
			private var controller:AppController = AppController.getInstance();
			private function onCreationComplete():void
			{
				model.readyState();
			}
			
		]]>
	</mx:Script>
	<mx:VBox height=”100%” width=”100%” verticalAlign=”middle” horizontalAlign=”center”>
		<mx:Button label=”Draw Shape” click=”controller.drawShape()”/>
	</mx:VBox>
</mx:Application>


We’ll add a public property to the AppModel class to store the name of the host application, and a method to determine the host application. Note that determining the host application can be as simple as the following:

HostObject.mainExtension


I’ve used a more complete approach (the same as in MakeSideHeads) to allow for future expansion to other Creative Suite applications.

In addition, the AppModel class stores a number of other variables that our drawing routines will call on.

public var hostName:String;
public var shapeType:String = "square";
public var width:Number = 72;
public var height:Number = 72;
public var sampleDocument:Boolean = true;
public var sampleWidth:Number = width*2;
public var sampleHeight:Number = height*2;
public var center:Array = new Array(sampleWidth/2, sampleHeight/2);


AppController, modified to call the application specific code for the host application.

package sample.drawing
{
	public class AppController
	{
		import sample.drawing.drawingIllustrator;
		import sample.drawing.drawingInDesign;
		import sample.drawing.drawingPhotoshop;
		private static var instance:AppController;
		private var model:AppModel = AppModel.getInstance();		
		public function AppController()
		{
		}
		public static function getInstance() : AppController 
		{
			if ( instance == null )
			{
				instance = new AppController();
			}
			return instance;
		}
		public function drawShape():void{
			var array:Array = calculatePoints();
			switch(model.hostName){
				case “indesign”:
					drawingInDesign.drawShape(array);
					break;
				case “illustrator”:
					drawingIllustrator.drawShape(array);
					break;
				case “photoshop”:
					drawingPhotoshop.drawShape(array);
					break;
			}
		}
		private function calculatePoints():Array{
			var array:Array = new Array;
			switch(model.shapeType){
				case “square”:
					var x1:Number = model.center[0]-model.width/2;
					var y1:Number = model.center[1]-model.height/2;
					var x2:Number = model.center[0]+model.width/2;
					var y2:Number = model.center[1]+model.height/2;
					array.push(new Array(x1, y1), new Array(x1, y2), new Array(x2, y2), new Array(x2, y1));
					break;
			}
			return array;
		}
	}
}


Note that I’ve set up the calculatePoints function as a switch statement­­—I did this to make it easy to add other point calculation routines easier in the future.

Application-Specific Code

In the ActionScript modules corresponding to each application, I’ve added routines for taking an array and using the coordinate pairs in the array to construct a path.

Illustrator

package sample.drawing
{
	import com.adobe.csawlib.illustrator.Illustrator;
	import com.adobe.illustrator.*;
	
	import sample.drawing.AppModel;
	
	public class drawingIllustrator
	{
		private static var app:Application = Illustrator.app;
		private static var model:AppModel = AppModel.getInstance();
		public function drawingIllustrator()
		{
		}
		public static function drawShape(array:Array):void 
		{
			var pathItem:PathItem;
			var pathPoint:PathPoint;
			if(model.sampleDocument == true){
				makeSampleDocument();
			}
			var document:Document = app.documents.index(0);
			pathItem = document.pathItems.add();
			pathItem.closed = true;
			if((array[0].length == 2)&&(array.length < 1000)){
				pathItem.setEntirePath(array);
			}
			//The array contains curved paths, or the array contains
			//more than 1000 path points.
			else{
				for(var counter:int = 0; counter < array.length; counter++){
					try{
						pathPoint = pathItem.pathPoints.index(counter);
					}
					catch(error:Error){
						pathPoint = pathItem.pathPoints.add();
					}
					if(array[counter].length == 2){
						pathPoint.leftDirection = array[counter];
						pathPoint.anchor = array[counter];
						pathPoint.rightDirection = array[counter];
					}
					else{
						pathPoint.leftDirection = array[counter][0];
						pathPoint.anchor = array[counter][1];
						pathPoint.rightDirection = array[counter][2];
					}
				}
			}
		}
		private static function makeSampleDocument():void
		{
			var document:Document = app.documents.add();
			model.center = new Array(document.width, document.height);
		}
	}
}


InDesign

package sample.drawing
{
	import com.adobe.csawlib.indesign.InDesign;
	import com.adobe.indesign.*;
	
	import sample.drawing.AppModel;
	
	public class drawingInDesign
	{
		private static var app:Application = InDesign.app;
		private static var model:AppModel = AppModel.getInstance();
		public function drawingInDesign()
		{
		}
		public static function drawShape(array:Array):void 
		{
			if(model.sampleDocument == true){
				makeSampleDocument();
			}
			var document:Document = app.documents.item(0);
			//The active window can be either a layout window or a story window. If the
			//user did not create a sample document, and has a story window open, we 
			//shouldn’t do anything.
			if(app.activeWindow is LayoutWindow){
				var window:LayoutWindow = LayoutWindow(app.activeWindow);
				var page:Page = window.activePage;
				var polygon:Polygon = page.polygons.add();
				polygon.paths.item(0).entirePath = array;
			}
		}
		private static function makeSampleDocument():void
		{
			var document:Document = app.documents.add();
			document.viewPreferences.horizontalMeasurementUnits = MeasurementUnits.POINTS;
			document.viewPreferences.verticalMeasurementUnits = MeasurementUnits.POINTS;
			document.documentPreferences.pageWidth = model.sampleWidth;
			document.documentPreferences.pageHeight = model.sampleHeight;
		}
	}
}


Photoshop

package sample.drawing
{
	import com.adobe.csawlib.photoshop.Photoshop;
	import com.adobe.photoshop.*;
	
	import sample.drawing.drawingPhotoshop;
	
	public class drawingPhotoshop
	{
		private static var app:Application = Photoshop.app;
		private static var model:AppModel = AppModel.getInstance();
		public function drawingPhotoshop()
		{
		}
		public static function drawShape(array:Array):void 
		{
			//Photoshop has trouble if there are other documents
			//open, so close any open documents before proceeding.
			if(app.documents.length > 0){
				if(model.sampleDocument == true){
					makeSampleDocument();
				}
				var document:Document = app.activeDocument;
				var subPathInfo:SubPathInfo = makeSubPathInfo(array);
				var subPathInfoArray:Array = new Array();
				subPathInfoArray.push(subPathInfo);
				var pathItem:PathItem = document.pathItems.add("sample", subPathInfoArray);
			}
			else{
				trace("Please close any open documents and try again.");
			}
		}
		private static function makeSampleDocument():void
		{
			app.preferences.rulerUnits = Units.POINTS;
			app.documents.add(model.sampleWidth, model.sampleHeight);
			//Create an art layer so that the path doesn't end up on the background layer.
			app.documents.index(0).artLayers.add();
		}
		private static function makeSubPathInfo(array:Array):SubPathInfo
		{
			var subPathInfo:SubPathInfo = new SubPathInfo;
			var pathPointInfoArray:Array = new Array;
			//Create an array of path point info objects.
			for(var counter:int = 0; counter < array.length; counter++){
				//If array[counter] has a length of 2, then it's a corner point. If not, it's a smooth point.
				if(array[counter].length == 2){
					pathPointInfoArray.push(makePathPointInfo(PointKind.CORNERPOINT, new Array(array[counter][0], array[counter][1]), new Array(array[counter][0], array[counter][1]), new Array(array[counter][0], array[counter][1])));
				}
				else{
					pathPointInfoArray.push(makePathPointInfo(PointKind.SMOOTHPOINT, new Array(array[counter][0][0], array[counter][0][1]), new Array(array[counter][1][0], array[counter][1][1]), new Array(array[counter][2][0], array[counter][2][1])));
				}
			}
			subPathInfo.entireSubPath = pathPointInfoArray;
			subPathInfo.operation = ShapeOperation.SHAPEADD;
			subPathInfo.closed = true;
			return subPathInfo;
		}
		private static function makePathPointInfo(pointKind:PointKind, leftDirection:Array, anchor:Array, rightDirection:Array):PathPointInfo{
			var pathPointInfo:PathPointInfo = new PathPointInfo;
			pathPointInfo.leftDirection = leftDirection;
			pathPointInfo.anchor = anchor;
			pathPointInfo.rightDirection = rightDirection;
			pathPointInfo.kind = pointKind;
			return pathPointInfo;
		}		
	}
}


Testing

At this point, you can test the extension. When you click the button in the panel’s user interface, it should create a new document and then draw a 72 point square in the center of the document.


Path Construction Details

As I mentioned earlier, paths are made up of path points. Path points, in turn, contain an anchor, a left direction, and a right direction–these are coordinate pairs defining the location of the point itself (the anchor) and the control handles associated with the point. The left direction contains a coordinate pair defining the location of the incoming (along the direction of the path) control handle; the right direction contains the coordinates of the outgoing control handle. The location of the left direction defines the curve of the line segment coming into the point; the location of the right direction controls the curve of the line segment following the point.

All three applications have a way of setting all of the points on the path at once. InDesign and Illustrator can take a simple array of coordinate pairs and convert them to path points; Photoshop can take an array of PathPointInfo objects. In InDesign, the array can be either an array of two-element arrays, or it can be an array containing arrays containing three two-element arrays. In the former case, the arrays contain only the anchor points of the path points; in the latter, the arrays contain the left direction and right direction in addition to the anchor.

In this extension, I’ve tried to make Illustrator and Photoshop work the way that InDesign works. This means that the application specific code for Photoshop has to generate an array of PathPointInfo objects, and that the Illustrator version has to handle the fully specified (three element) arrays by drawing point-by-point. The Illustrator version will also draw point-by-point when the incoming array contains more than one thousand elements­­—this is to work around what seems to be a limitation of Illustrator scripting: paths created using the setEntirePath method appear to be truncated when the path contains a large number of points.

Further Development

Assuming that your extension is now working, let’s add the ability to draw rectangles, circles, and ellipses. Return to the panel’s user interface–the mxml file–and add a set of radio buttons to specify different shape types, and a pair of edit fields to enter the width and height of the shape. Then we’ll change the AppController slightly to pass different arrays of coordinates to the application specific modules.

I added a simple user interface to the mxml file. It’s not intended to be fancy; just enough UI to draw our demonstration shapes. You can view it in the attached project, if you want–I don’t see the need to quote it here (it doesn’t take much UI to make for a very long blog post).

All we need to do to support these additional features is add new array-generating code to the AppController module—there’s no need for us to make any changes to our application-specific code.

private function calculatePoints():Array{
	var x1:Number, y1:Number, x2:Number, y2:Number, x3:Number, y3:Number, x4:Number, y4:Number;
	var handleXRadius:Number, handleYRadius:Number;
	var magicNumber:Number = 0.55228474666667;
	var array:Array = new Array;
	switch(model.shapeType){
		case "square":
		case "rectangle":
			x1 = model.center[0]-model.width/2;
			y1 = model.center[1]-model.height/2;
			x2 = model.center[0]+model.width/2;
			y2 = model.center[1]+model.height/2;
			array.push(new Array(x1, y1), new Array(x1, y2), new Array(x2, y2), new Array(x2, y1));
			break;
		case "circle":
		case "ellipse":
			handleXRadius = (model.width/2)*magicNumber;
			handleYRadius = (model.height/2)*magicNumber;
			x1 = model.center[0]-model.width/2;
			y1 = model.center[1];
			x2 = model.center[0];
			y2 = model.center[1]+model.height/2;
			x3 = model.center[0]+model.width/2;
			y3 = model.center[1]-model.height/2;
			array.push(new Array(new Array(x1, y1-handleYRadius), new Array(x1, y1), new Array(x1, y1+handleYRadius)));
			array.push(new Array(new Array(x2-handleXRadius, y2), new Array(x2, y2), new Array(x2+handleXRadius, y2)));
			array.push(new Array(new Array(x3, y1+handleYRadius), new Array(x3, y1), new Array(x3, y1-handleYRadius)));
			array.push(new Array(new Array(x2+handleXRadius, y3), new Array(x2, y3), new Array(x2-handleXRadius, y3)));
			//array.push(new Array(x1, y1), new Array(x2, y2), new Array(x3, y1), new Array(x2, y3));
			break;
	}
	return array;
}


As you can see, there’s no difference between the square/rectangle and circle/ellipse array generation routines—it’s purely a matter of user interface.

More to Come

As you’ve probably guessed, the point of this exercise isn’t to draw squares, rectangles, circles, and ellipses. The point was to come up with a general-purpose way of drawing any shape. We’ve done that. All you need to do is provide a routine that generates an array of coordinate pairs, and hand the array off to the application-specific drawing functions we’ve created.

There’s a lot more that we can do with paths and path points, and I’ll be returning to this topic again in this space the near future.