/*
* Scriptographer
*
* This file is part of Scriptographer, a Scripting Plugin for Adobe Illustrator
* http://scriptographer.org/
*
* Copyright (c) 2002-2010, Juerg Lehni
* http://scratchdisk.com/
*
* All rights reserved. See LICENSE file for details.
*
* File created on 18.02.2005.
*/
package com.scriptographer.ai;
import java.util.ArrayList;
import com.scratchdisk.script.Callable;
import com.scratchdisk.util.IntMap;
import com.scratchdisk.util.IntegerEnumUtils;
import com.scriptographer.CommitManager;
import com.scriptographer.ScriptographerEngine;
import com.scriptographer.ScriptographerException;
import com.scriptographer.ui.MenuItem;
/**
* Wrapper for Illustrator's LiveEffects. Unfortunately, Illustrator is not
* able to remove once created effects again until the next restart. They can be
* removed from the menu but not from memory. So In order to recycle effects
* with the same settings, e.g. during development, where the code often changes
* but the initial settings maybe not, keep track of all existing effects and
* match against those first before creating a new one. Also, when
* Scriptographer is (re)loaded, the list of existing effects needs to be walked
* through and added to the list of unusedEffects. This is done by calling
* getUnusedEffects.
*
* @author lehni
*
* @jshide
*/
/*
* Re: sdk: Illustrator AILiveEffect questions Datum: 23. Februar 2005 18:53:13
* GMT+01:00
*
* In general, live effects should run on all the art it is given. In the first
* example of running a post effect on a path, the input art is split into two
* objects: one path that contains just the stroke attributes and another path
* that contains just the fill color. Input art is typically split up this way
* when effects are involved.
*
* If the effect is dragged before any of the fill/stroke layers in the
* appearance palette, then the input path the effect will see will just be one
* path, since it has not been split up yet. However, the input path will
* contain no paint, since it has not gone through the fill/stroke layers yet.
*
* In the example of running a post effect on a group with two paths, the input
* art will be split up again in order to go through the fill/stroke layers and
* the "Contents" layer. Thus, you will see three copies of the group: one that
* may just be filled, one that may just be stroked, and one that contains the
* original group unchanged.
*
* Because of this redundancy, it is more optimal to register effects as pre
* effects, if the effect does not care about the paint on the input path. For
* example, the Roughen effect in Illustrator is registered as a pre effect
* since it merely roughens the geometry, regardless of the paint. On the other
* hand, drop shadow is registered as a post effect, since its results depend on
* the paint applied to the input objects.
*
* While there is redundancy, effects really cannot make any assumptions about
* the input art they are given and should thus attempt to operate on all of the
* input art. At the end of executing an entire appearance, Illustrator will
* attempt to "clean up" and remove any unnecessary nested groups and unpainted
* paths.
*
* When creating output art for the go message, the output art must be a child
* of the same parent as the input art. It also must be the only child of this
* parent, so if you create a copy of the input art, work on it and attempt to
* return the copy as the output art, you must make sure to dispose the original
* input art first. It is not legal to create an item in an arbitrary place and
* return that as the output art.
*
* Effects are limited in the kinds of attributes that they can attach to the
* output art. Effects must restrict themselves to using "simple" attributes,
* such as: - 1 fill and 1 stroke (AIPathStyle) - transparency options
* (AIBlendStyle) It is actually not necessary to use the AIArtStyle suite when
* generating output art, and effects should try to avoid it. Effects also
* should avoid putting properties on output art that will generate more styled
* art (ie. nested styled art is not allowed).
*
* I suggest playing around with the TwirlFilterProject in Illustrator and
* expanding the appearance to get a better picture of the live effect
* architecture.
*
* Hope that helps, -Frank
*/
public class LiveEffect extends NativeObject {
// AIStyleFilterPreferredInputArtType
protected static final int
INPUT_DYNAMIC = 0,
INPUT_GROUP = 1 << (Item.TYPE_GROUP - 1),
INPUT_PATH = 1 << (Item.TYPE_PATH - 1),
INPUT_COMPOUNDPATH = 1 << (Item.TYPE_COMPOUNDPATH - 1),
INPUT_PLACED = 1 << (Item.TYPE_PLACED - 1),
INPUT_MYSTERYPATH = 1 << (Item.TYPE_MYSTERYPATH - 1),
INPUT_RASTER = 1 << (Item.TYPE_RASTER - 1),
// If INPUT_PLUGIN is not specified, the filter will receive the result
// group of a plugin group instead of the plugin group itself
INPUT_PLUGIN = 1 << (Item.TYPE_PLUGIN - 1),
INPUT_MESH = 1 << (Item.TYPE_MESH - 1),
INPUT_TEXTFRAME = 1 << (Item.TYPE_TEXTFRAME - 1),
INPUT_SYMBOL = 1 << (Item.TYPE_SYMBOL - 1),
INPUT_FOREIGN = 1 << (Item.TYPE_FOREIGN - 1),
INPUT_LEGACYTEXT = 1 << (Item.TYPE_LEGACYTEXT - 1),
// Indicates that the effect can operate on any input art. */
INPUT_ANY = 0xfff,
// Indicates that the effect can operate on any input art other than
// plugin groups which are replaced by their result art.
INPUT_ANY_BUT_PLUGIN = INPUT_ANY & ~INPUT_PLUGIN,
// Special values that don't correspond to regular art types should be
// in the high half word
// Wants strokes to be converted to outlines before being filtered
// (not currently implemented)
INPUT_OUTLINED_STROKE = 0x10000,
// Doesn't want to take objects that are clipping paths or clipping text
// (because it destroys them, e.g. by rasterizing, or by splitting a
// single path into multiple non-intersecting paths, or by turning it
// into a plugin group, like the brush filter).
// This flag is on for "Not OK" instead of on for "OK" because
// destroying clipping paths is an exceptional condition and we don't
// want to require normal filters to explicitly say they're OK.
// Also, it is not necessary to turn this flag on if you can't take any
// paths at all.
INPUT_NO_CLIPMASKS = 0x20000;
//AIStyleFilterFlags
public static final int
FLAG_NONE = 0,
/* Parameters can be scaled. */
FLAG_HAS_SCALABLE_PARAMS = 1 << 17,
/* Supports automatic rasterization. */
FLAG_USE_AUTO_RASTARIZE = 1 << 18,
/* Supports the generation of an SVG filter. */
FLAG_CAN_GENERATE_SVG_FILTER = 1 << 19,
/* Has parameters that can be modified by a \c
* #kSelectorAILiveEffectAdjustColors message. */
FLAG_HAS_ADJUST_COLOR_HANDLER = 1 << 20,
/* Handles \c #kSelectorAILiveEffectIsCompatible messages.
* If this flag is not set the message will not be sent. */
FLAG_HAS_IS_COMPATIBLE_HANDLER = 1 << 21;
private String name;
private String title;
private LiveEffectPosition position;
private int preferredInput;
private int flags;
private int majorVersion;
private int minorVersion;
private MenuItem menuItem = null;
/**
* effects maps effectHandles to their wrappers.
*/
private static IntMap<LiveEffect> effects = null;
/**
* Called from the native environment.
*/
protected LiveEffect(int handle, String name, String title, int position,
int preferredInput, int flags, int majorVersion, int minorVersion) {
super(handle);
this.name = name;
this.title = title;
this.position = IntegerEnumUtils.get(LiveEffectPosition.class, position);
this.preferredInput = preferredInput;
this.flags = flags;
this.majorVersion = majorVersion;
this.minorVersion = minorVersion;
}
/**
* @param title preferred
* @param preferred a combination of LiveEffect.INPUT_*
* @param flags a combination of LiveEffect.FLAG_*
* @param majorVersion
* @param minorVersion
*/
public LiveEffect(String title, String category,
LiveEffectPosition position, Class preferredInput, int flags,
int majorVersion, int minorVersion) {
this(0, title, title, position != null ? position.value : 0,
getInputType(preferredInput), flags, majorVersion, minorVersion);
IntMap<LiveEffect> effects = getEffects();
// Now see first whether there is an effect already that fits this
// description. Reuse it, as we're probably re-executing a script
// that produces the same effect again.
Integer key = effects.keyOf(this);
if (key != null) {
// Found one, let's reuse it's handle and remove the old effect from
// the list:
LiveEffect effect = effects.get(key);
effect.remove();
handle = effect.handle;
effect.handle = 0;
effects.remove(key);
} else {
// No previously existing effect found, create a new one:
handle = nativeCreate(name, title, this.position.value,
this.preferredInput, flags, majorVersion, minorVersion);
}
if (handle == 0)
throw new ScriptographerException("Unable to create LifeEffect.");
if (category != null)
menuItem = nativeAddMenuItem(name, category, title + "...");
effects.put(handle, this);
}
public LiveEffect(String title, String category,
LiveEffectPosition position, Class preferredInput, int flags) {
this(title, category, position, preferredInput, flags, 1, 0);
}
public LiveEffect(String title, String category,
LiveEffectPosition position, Class preferredInput) {
this(title, category, position, preferredInput, FLAG_NONE, 1, 0);
}
public LiveEffect(String title, String category, LiveEffectPosition position) {
this(title, category, position, null, FLAG_NONE, 1, 0);
}
private native int nativeCreate(String name, String title,
int position, int flags, int preferredInput, int majorVersion,
int minorVersion);
private native MenuItem nativeAddMenuItem(String name, String category,
String title);
/**
* "Removes" the effect. there is no real destroy for LiveEffects in
* Illustrator, so all it really does is remove the effect's menu item, if
* there is one. It keeps the effectHandle and puts itself in the list of
* unused effects
*/
public boolean remove() {
// See whether we're still linked:
if (effects.get(handle) == this) {
if (menuItem != null)
menuItem.remove();
menuItem = null;
return true;
}
return false;
}
public static void removeAll() {
// As remove() modifies the map, using an iterator is not possible here:
if (effects != null)
for (Object effect : effects.values().toArray())
((LiveEffect) effect).remove();
}
public MenuItem getMenuItem() {
return menuItem;
}
/*
* used for unusedEffects.indexOf in the constructor above
*/
public boolean equals(Object obj) {
if (obj instanceof LiveEffect) {
LiveEffect effect = (LiveEffect) obj;
return name.equals(effect.name) &&
title.equals(effect.title) &&
preferredInput == effect.preferredInput &&
position == effect.position &&
flags == effect.flags &&
majorVersion == effect.majorVersion &&
minorVersion == effect.minorVersion;
}
return false;
}
private static IntMap<LiveEffect> getEffects() {
if (effects == null) {
effects = new IntMap<LiveEffect>();
for (LiveEffect effect : nativeGetEffects())
effects.put(effect.handle, effect);
}
return effects;
}
private static native ArrayList<LiveEffect> nativeGetEffects();
// Getters:
public LiveEffectPosition getPosition() {
return position;
}
/**
* @jshide
*/
public String getName() {
return name;
}
/**
* @jshide
*/
public String getTitle() {
return title;
}
/**
* @jshide
*/
public int getFlags() {
return flags;
}
/**
* @jshide
*/
public int getMajorVersion() {
return majorVersion;
}
/**
* @jshide
*/
public int getMinorVersion() {
return minorVersion;
}
// Callback functions:
private Callable onEditParameters = null;
public Callable getOnEditParameters() {
return onEditParameters;
}
public void setOnEditParameters(Callable onEditParameters) {
this.onEditParameters = onEditParameters;
}
protected void onEditParameters(LiveEffectEvent event) {
if (onEditParameters != null)
ScriptographerEngine.invoke(
onEditParameters, this, event);
}
private Callable onCalculate = null;
public Callable getOnCalculate() {
return onCalculate;
}
public void setOnCalculate(Callable onCalculate) {
this.onCalculate = onCalculate;
}
protected void onCalculate(LiveEffectEvent event) {
if (onCalculate != null)
ScriptographerEngine.invoke(onCalculate, this, event);
}
private Callable onGetInputType = null;
public Callable getOnGetInputType() {
return onGetInputType;
}
public void setOnGetInputType(Callable onGetInputType) {
this.onGetInputType = onGetInputType;
}
protected int onGetInputType(LiveEffectEvent event) {
if (onGetInputType != null) {
Object ret = ScriptographerEngine.invoke(
onGetInputType, this, event);
// Determine type from returned class
if (ret instanceof Class)
return getInputType((Class) ret);
}
// Default is INPUT_ANY_BUT_PLUGIN
return INPUT_ANY_BUT_PLUGIN;
}
protected static int getInputType(Class cls) {
// Default setting for effects that provide no input type is
// INPUT_DYNAMIC, so the getInputType handler is asked instead.
int type = INPUT_DYNAMIC;
// Determine type from Item class
if (cls != null && Item.class.isAssignableFrom(cls)) {
type = Item.getItemType(cls);
if (type == Item.TYPE_ANY)
type = INPUT_ANY_BUT_PLUGIN;
else if (type == Item.TYPE_UNKNOWN)
type = INPUT_DYNAMIC;
else {
type = 1 << (type - 1);
if (type == INPUT_ANY)
type = INPUT_ANY_BUT_PLUGIN;
}
}
return type;
}
/**
* To be called from the native environment:
*/
private static void onEditParameters(int handle, int dataHandle) {
LiveEffect effect = getEffect(handle);
if (effect != null) {
effect.onEditParameters(new LiveEffectEvent(0, dataHandle));
}
}
/**
* To be called from the native environment:
*/
private static int onCalculate(int handle, Item item, int dataHandle) {
LiveEffect effect = getEffect(handle);
if (effect != null) {
LiveEffectParameters parameters =
LiveEffectParameters.wrapHandle(dataHandle, item.document);
Item parent = item.getParent();
// Scriptographer's new item recording feature makes
// processing effects extremely convenient. All new items
// are automatically collected, and the right thing is
// done with them at the end. Since doing the wrong
// thing leads to endless crashes, this is the best
// way to handle this anyway.
Item.collectCreatedItems();
ItemList newItems = null;
try {
effect.onCalculate(new LiveEffectEvent(item, parameters));
} finally {
newItems = Item.retreiveCreatedItems();
}
if (newItems.size() > 0) {
boolean changed = false;
Item newItem;
if (newItems.size() == 1) {
newItem = newItems.getFirst();
} else {
// More than one new item was produced. Group them, as
// LiveEffects require one item only.
newItem = new Group(newItems);
changed = true;
}
// "When creating output art for the go message, the output art
// must be a child of the same parent as the input art. It also
// must be the only child of this parent, so if you create a
// copy of the input art, work on it and attempt to return the
// copy as the output art, you must make sure to dispose the
// original input art first. It is not legal to create an item
// in an arbitrary place and return that as the output art."
if (newItem.getParent().equals(parent)
|| parent.appendTop(newItem)) {
item.remove();
item = newItem;
changed = true;
}
// Since we're outside of Scriptographer's script handling, we
// need to take care of committing changes ourselves here before
// returning.
if (changed)
CommitManager.commit();
}
}
// already return the handle to the native environment so it doesn't
// need to access it there...
return item.handle;
}
/**
* To be called from the native environment:
*/
private static int onGetInputType(int handle, int itemHandle,
int parametersHandle) {
// For improved performance of onGetInputType, we do not wrap the handle
// on the native side already, as often it is not even used. Instead
// The LiveEffectEvent takes care of that on demand.
LiveEffect effect = getEffect(handle);
if (effect != null)
return effect.onGetInputType(new LiveEffectEvent(itemHandle,
parametersHandle));
return INPUT_ANY_BUT_PLUGIN;
}
private static LiveEffect getEffect(int handle) {
return effects.get(handle);
}
}