Putting It All Together: Path Effects

In these blog posts, I’m trying to create a series of “building blocks” that CS SDK users can put together to make CS Extensions. Need to draw things with your CS Extension? You can pull drawing routines from the “Drawing Paths: The Basics” post. Want to have your CS Extension respond to document open/close events? Grab the DocumentWatcher class from the “Watching the Detections” post.

At the same time, I’m thinking that some CS SDK users are coming to the Creative Suite without deep knowledge of at least some of the applications. A developer who has spent years working with Flash might not know (or want to know) the finer details of drawing paths in Illustrator, or setting type in InDesign.

My goal is to make it possible for these developers to get things done in Illustrator, InDesign, and Photoshop relatively quickly, without having to learn the document object model of each application (each one different). To that end, I’ve provided functions that encapsulate some of the complexity inside applications-specific functions.

Sadly, for this post, we’ll have to leave Photoshop behind, as that application doesn’t really provide a way to tell if a path is selected. (Or, I should say, it doesn’t provide an obvious way—I’m still looking.)

To see how this might work for you, let’s put together a new CS Extension using parts and pieces from my earlier blog posts. We’ll create an extension that applies various effects to the paths of page items selected in the host application, and we’ll get most of the code from the Drawing and DocumentWatcher projects.

This project also gives me a chance to make a point about Creative Suite Extensions in general: I think that we developers often think only in terms of productivity tools—writing XMP metadata or setting up defaults for a workgroup, that sort of thing. We tend to forget that automation can be used to add creative tools and new artistic effects.

The project for this example is here:

PathEffects

Sorting Selections

I think it’s safe to say that most interactive panels created using the CS SDK will need to monitor the user selection and make user interface changes based on changes to the selection. In previous posts, I’ve presented a number of different ways to sort the selection based on the type of the selected objects, but I haven’t really highlighted the code.

The following sections show relevant snippets from the current example extension. All of the applications can return the selection from a variety of container objects—the application, a document, a window, and others—we’ll get the selection from the application (app), because the selection of the application will always be the selection of the active document.

In each example, the extension gets the selection (which is always an array) and gets the type of each object in the array. If the object type matches one of the qualifying type(s), the extension changes the value of a variable in the AppModel class, which, in turn, changes the status of items in the user interface bound to that variable.

Note: All of the following examples rely on a peculiarity of the JavaScript (ActionScript) language: you can use switch(true) to create a switch statement that responds to any number of logical case statements. In my opinion, this use of switch makes a good replacement for multiple, nested if...else statements—if nothing else, it’s easier to read. This use of switch is particularly handy when your code needs to branch based on changes to multiple variables, whereas using switch(variable) can only respond to changes to a specific variable.

Illustrator

public static function getObjects():Array{ 
var array:Array = new Array; 
var selectedObjects:Array = app.activeDocument.selection as Array; 
for(var counter:int = 0; counter < selectedObjects.length; counter++){ 
	switch(true){ 
		case selectedObjects[counter] is PathItem: 
		case selectedObjects[counter] is CompoundPathItem: 
			array.push(selectedObjects[counter]); 
			break; 
	} 
} 
return array; 
}


In Illustrator, the objects we’re looking for in this example are either PathItems or CompoundPathItems. Illustrator also includes the ability to get just the PathPoints in the selection using the PathItem.selectedPathPoints property, though I don’t make use of it in this example.

InDesign

public static function getObjects():Array{ 
var array:Array = new Array; 
for(var counter:int = 0; counter < app.selection.length; counter++){ 
	switch(true){ 
		case app.selection[counter] is Rectangle: 
		case app.selection[counter] is Oval: 
		case app.selection[counter] is Polygon: 
		case app.selection[counter] is GraphicLine:
		case app.selection[counter] is TextFrame: 
			array.push(app.selection[counter]); 
			break; 
	} 
} 
return array; 
}


The type that InDesign returns for a given PageItem or SplineItem depends on the number of paths and the arrangement of points on those paths within the object. Rectangles, for example, contain a single path consisting of four points, with ninety degree angles between all points. GraphicLines are always open paths containing two points. Any object containing more than four points, or more than one path, is a Polygon. Any object with a content type of ContentType.TEXT is a TextFrame. Objects can change type based on the arrangement of paths and points within the object. Whether this is a nuisance or a benefit is entirely up to you—I find it useful.

Setting Up

I’ll set up my project in the usual way—adding the AppController and AppModel classes and a separate class each for Illustrator and InDesign. I’ll also add a couple of classes that contain the path effects provided by the extension. The idea here was to make the effects modular—and make it easier for you to add your own. The modules are very simple—they take an array of path point locations, process them, and return another array. The application-specific classes take care of the work of re-drawing the paths in the selection using the points in the array.

Note: In main.mxml, I’ve moved the addition of the application-specific event listeners out of the function triggered by the CreationComplete event to the function triggered by the ApplicationComplete event. In the DocumentWatcher example, I created both the CSXS event listeners and application-specific event listeners when the CreationComplete event fired. In some cases, and usually when debugging, errors would sometimes occur when adding the application-specific event listeners immediately after the CreationComplete event. We’re investigating this problem—it’s intermittent, and hard to track down. In the meantime, I haven’t seen any errors when adding the event listeners after the ApplicationComplete event.

To do this, I added the CSXS SWC library to the project—that gives me the CSXSWindowedApplication, which, in turn, provides the ApplicationComplete event.

<?xml version="1.0" encoding="utf-8"?>
<csxs:CSXSWindowedApplication
	xmlns:csxs="com.adobe.csxs.core.*" 
	xmlns:mx="http://www.adobe.com/2006/mxml" 
	layout="absolute" 
	historyManagementEnabled="false" 
	creationComplete="onCreationComplete()"
	applicationComplete="onApplicationComplete()" 
	close="onClose()"
	>
	<mx:Script>
		<![CDATA[
			import patheffects.*

			[Bindable]
			private var model:AppModel = null; 
			private var controller:AppController = null;
			private var watcher:DocumentWatcher = null;
			private function onCreationComplete():void{
				model = AppModel.getInstance();				
				model.readyState();
			}
			private function onApplicationComplete():void{
				controller = AppController.getInstance();
				watcher = DocumentWatcher.getInstance();
				controller.attach();
			}
			private function selectEffect(event:Event):void{
				model.effectName = model.effectNames[EffectsComboBox.selectedIndex];
			}
			private function onClose():void{
				controller.detach();
			}
		]]>
	</mx:Script>
	<mx:VBox height="100%" width="100%" verticalAlign="middle" horizontalAlign="center">
		<mx:HBox>
			<mx:Label 
				text="Effects:" 
				width="60" textAlign="right"/>
			<mx:ComboBox 
				enabled="{model.pathItemSelected}"
				width="200"
				id="EffectsComboBox"
				dataProvider="{model.effectNames}" 
				change="selectEffect(event)"/>
		</mx:HBox>
		<mx:HBox>
			<mx:Button label="Apply" enabled="{model.pathItemSelected}" click="controller.apply()"/>
		</mx:HBox>
	</mx:VBox>
</csxs:CSXSWindowedApplication>


I’ve added the host adapter libraries for Illustrator and InDesign so that I can respond to application-specific events. We’ll be watching for different events than we did in the DocumentWatcher example—specifically, we need to respond to changes in the selection. If a page item containing paths is selected, the extension will make its user interface available. Both applications have an event that works well for this purpose: AIEvent.ART_SELECTION_CHANGED in Illustrator and Application.AFTER_SELECTION_CHANGED in InDesign. You can add these libraries using the CS Extension Builder or by adding them to the build path in the “stock” CS SDK.


Illustrator

public static function attach():void{
	AIEventAdapter.getInstance().addEventListener(AIEvent.ART_SELECTION_CHANGED, handleAIEvent);
}
//When you quit with an object selected, this event can throw an error. 
//This is a harmless error, we'll use try...catch to deal with it.
public static function detach():void 
{
	try{
		AIEventAdapter.getInstance().removeEventListener(AIEvent.ART_SELECTION_CHANGED, handleAIEvent);
	}
	catch(error:Error){}
}
public static function handleAIEvent(event:AIEvent):void 
{
	handleEvent();
}		
public static function handleEvent():void 
{
	var result:Boolean = false;
	if(app.documents.length > 0){
		var selectedObjects:Array = app.activeDocument.selection as Array;
		if(selectedObjects.length > 0){
			for(var counter:int = 0; counter < selectedObjects.length; counter++){
				switch(true){
					case selectedObjects[counter] is PathItem:
					case selectedObjects[counter] is CompoundPathItem:
						result = true;
						break;
				}
				if(result == true){
					break;
				}
			}
		}
	}
	model.pathItemSelected = result;
}

}


InDesign

public static function attach():void{
	IDScriptingEventAdapter.getInstance().addEventListener(Application.AFTER_SELECTION_CHANGED, handleIDEvent);
}
//When you quit with an object selected, this event can throw an error. 
//This is a harmless error, we'll use try...catch to deal with it.
public static function detach():void 
{
	try{
		IDScriptingEventAdapter.getInstance().removeEventListener(Application.AFTER_SELECTION_CHANGED, handleIDEvent);
	}
	catch(error:Error){}
}
//InDesign event handlers expect an event object, so we'll 
//create a dummy handler to strip the event and call the generic
//event handler.
public static function handleIDEvent(event:Event):void 
{
	handleEvent();
}		
public static function handleEvent():void 
{
	if(!app.modalState){
		var result:Boolean = false;
		if(app.documents.length > 0){
			if(app.selection.length > 0){
				for(var counter:int = 0; counter < app.selection.length; counter++){
					switch(true){
						case app.selection[counter] is Rectangle:
						case app.selection[counter] is Oval:
						case app.selection[counter] is Polygon:
						case app.selection[counter] is GraphicLine:
						case app.selection[counter] is TextFrame:
							result = true;
							break;
					}
					if(result == true){
						break;
					}
				}
			}
		}
	}
	model.pathItemSelected = result;
}


Program Flow

When you select an effect type and click the Apply button, the extension performs the following steps:

1. Calls an application-specific function (getObjects) to check the page items in the selection, and returns an array of qualifying page items (i.e., page items containing one or more paths).

2. Sends the array of page items to an application-specific function getPaths. This function returns an array of paths.

3. Sends each path to the application-specific function isClosedPath to determine the path state—this comes in handy when processing the paths.

4. Sends each path object to another application-specific function (getPoints), which returns an array of path point coordinates (in the form [[[x1, y1], [x2, y2], [x3, y3]], ...], where each array of three coordinate pairs represents the left direction, anchor, and right direction location).

5. Sends each array, in turn, to the selected effect module, which returns a new array in the same form.

6. Sends the returned array to the application-specific function applyEffect, which changes the location of the path points on the current path to match the coordinates of the processed array (adding or removing path points as necessary).

Snippet from the AppController class (for Illustrator, but all functions names are the same for InDesign):

case "illustrator":
	//Because I started from InDesign, and because InDesign and Illustrator
	//draw their paths in opposite directions, I have to reverse the order of
	//the Illustrator path points, process them, and then return them to
	//their original order to get the effects to look the same in 
	//the two applications.
	//Get the qualifying objects from the selection.
	objectArray = PathEffectsIllustrator.getObjects();
	if(objectArray.length > 0){
		//Iterate through the objects.
		for(counter = 0; counter < objectArray.length; counter++){
			//For each object, get an array of paths.
			pathArray = PathEffectsIllustrator.getPaths(objectArray[counter]);
			for(pathCounter = 0; pathCounter < pathArray.length; pathCounter++){
				//Is it an open path, or a closed path?
				isClosedPath = PathEffectsIllustrator.isClosedPath(pathArray[pathCounter]);
				//Get an array of path points from the path.
				pointArray = PathEffectsIllustrator.getPoints(pathArray[pathCounter]);
				//Reverse the order of the points in the array.
				pointArray = reversePath(pointArray);
				//Invert the Y axis values in the array.
				pointArray = invertYAxis(pointArray);
				//Pass the path point array off to the effect.
				pointArray = calculatePoints(pointArray, isClosedPath);
				//Invert the Y axis values in the array.
				pointArray = invertYAxis(pointArray);
				//Reverse the order of the points in the array.
				pointArray = reversePath(pointArray);
				//Redraw the path based on the modified array of path points.
				PathEffectsIllustrator.applyEffect(pathArray[pathCounter], pointArray);
			}
		}
	}
	break;


As I’ve mentioned before, here’s the beauty of this approach: The effects functions do not need to know anything about the specifics of drawing paths in the host application.

The part of each application-specific module that takes care of drawing the paths came almost unchanged from the Drawing post, but I’ve added a few application-specific functions for getting page items, paths, and path points.

The Effects Modules

I won’t go into too much detail on the effects modules (BasicEffects and FractalEffects) in this example—all they do is apply various geometric/trigonometric functions to the coordinate pairs stored in the arrays they receive from the application-specific code, then pass those arrays back to the AppController class. Here’s an edited snippet from the BasicEffects class:

for(counter = 0; counter < array.length; counter++){
	switch(effectName){
		//The "Punk" effect pushes the control handles to the center of the path.
		case "Punk":
			xOffset = (xCenter - array[counter][1][0]) * distance;
			yOffset = (yCenter - array[counter][1][1]) * distance;
			array[counter][0][0] = xCenter - xOffset;
			array[counter][0][1] = yCenter - yOffset;
			array[counter][2][0] = xCenter - xOffset;
			array[counter][2][1] = yCenter - yOffset;
			break;
		//Other cases omitted.
	}
}


The effect takes an array (in this case calculating the geometric center point of the points in the array—stored in the the xCenter and yCenter variables), then writes new coordinates into the array. At the end of the function, the effect module returns the modified array to the AppController.

One thing I will mention, however, is the yAdjust variable in the AppModel class. I added this because the vertical axis in Illustrator is upside down, relative to InDesign: increasing the vertical coordinate of a point moves the point up in Illustrator; down in InDesign. From the point of view of pure geometry, Illustrator gets it right. The effects use this variable to invert the y (vertical) values in some calculations. If you create your own effects, you’ll probably want to make use of it.

Before:

After (yes, I know that Illustrator has this effect built in—but this is InDesign):

It’s easy to get carried away with path effects—but remember, paths with lots of points can take a long time to draw! Repetitively applying one of the fractal effects can tie up your machine for hours. On the other hand, examples like the one below appear quite quickly.

Not Just Fun and Games

Why have I spent two posts writing about drawing in Creative Suite applications? It’s not just because I like playing with geometry; it’s that I think that there’s an unmet need for publishers (and, hence, a market for Creative Suite developers). Given that graphics created using Excel aren’t of publication quality (web or print) and that Illustrator’s charting features are not accessible to scripting, it seems to me that these posts might be useful to someone out there.

What I hear from many publishers is that they typically copy Excel graphics into Illustrator and then spend hours editing them by hand. To me, that sounds like a developer opportunity.

Am I wrong/misguided/crazy? I’d love to hear feedback from developers on this topic. As always, if you have questions about this post, or if you have ideas for future posts, please let me know!

3 Responses to Putting It All Together: Path Effects

  1. Harbs says:

    Hi Ole,

    Very cool post!

    I haven’t yet done any major work on cross-app path manipulation, but I can see this proving to be very useful when I do!

    I love the way you reverse the switch and use switch(true). Very elegent!

    When stuggling to get switches to work in ActionScript the way I’m used to in ExtendScript (i.e. switch(obj.constructor)), I came up with this:

    switch (getDefinitionByName( getQualifiedClassName(obj) ) as Class){
    case PathItem:
    case CompoundPathItem:
    // do stuff
    break;
    }

  2. Pingback: More Basics: Importing and Exporting Files « Creative Suite SDK Team

  3. Utku says:

    Hi,

    Can you guys post some example about coordinate systems, or specifically converting PlacedItem,s transformation matrix to a local one on artboard coordinates, this part pretty unclear, that matrix is very mysterious, most tasks involving placed items are impossible without that information.