/* * 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 03.12.2004. */ package com.scriptographer.ai; import java.awt.geom.GeneralPath; import com.scratchdisk.list.ExtendedArrayList; import com.scratchdisk.list.ExtendedList; import com.scratchdisk.list.List; import com.scratchdisk.list.Lists; import com.scratchdisk.list.ReadOnlyList; import com.scriptographer.CommitManager; /** * The Path item represents a path in an Illustrator document. * * @author lehni * * @jsreference {@type constructor} {@name Path.Line} {@reference Document#createLine} {@after Path} * @jsreference {@type constructor} {@name Path.Rectangle} {@reference Document#createRectangle} {@after Path} * @jsreference {@type constructor} {@name Path.RoundRectangle} {@reference Document#createRoundRectangle} {@after Path} * @jsreference {@type constructor} {@name Path.RegularPolygon} {@reference Document#createRegularPolygon} {@after Path} * @jsreference {@type constructor} {@name Path.Star} {@reference Document#createStar} {@after Path} * @jsreference {@type constructor} {@name Path.Spiral} {@reference Document#createSpiral} {@after Path} * @jsreference {@type constructor} {@name Path.Oval} {@reference Document#createOval} {@after Path} * @jsreference {@type constructor} {@name Path.Circle} {@reference Document#createCircle} {@after Path} * @jsreference {@type constructor} {@name Path.Arc} {@reference Document#createArc} {@after Path} */ public class Path extends PathItem { private SegmentList segments = null; private CurveList curves = null; /** * Wraps an AIArtHandle in a Path object */ protected Path(int handle, int docHandle, boolean created) { super(handle, docHandle, created); } /** * Creates a path object of the given type. Used by CompoundPath */ protected Path(short type) { super(type); } public Path() { super(TYPE_PATH); } /** * Creates a new Path Item. * * Sample code: * <code> * var firstSegment = new Segment(30, 30); * var secondSegment = new Segment(100, 100); * var path = new Path([firstSegment, secondSegment]); * </code> * * <code> * var path = new Path(); * path.moveTo(30, 30); * path.lineTo(100, 100); * </code> * * @param segments the segments to be added to the {@link #getSegments()} array * @return the newly created path */ public Path(ReadOnlyList<? extends Segment> segments) { this(); setSegments(segments); } public Path(Segment[] segments) { this(Lists.asList(segments)); } /** * Adds one segment to the end of the segment list of this path. * * @param segment the segment or point to be added. * @return the added segment. This is not necessarily the same object, e.g. * if the segment to be added already belongs to another path. */ public Segment add(Segment segment) { return getSegments().add(segment); } /** * @jshide for now. TODO: Implement varargs in doclet Adds a variable amount * of segments at the end of the segment list of this path. * * @return the added segments. These are not necessarily the same objects, * e.g. if the segments to be added already belongs to another path. */ public ReadOnlyList<? extends Segment> add(Segment... segments) { SegmentList segs = getSegments(); int start = segs.size(); segs.addAll(Lists.asList(segments)); return segs.getSubList(start, segs.size()); } /** * Inserts a segment at a given index in the list of this path's segments. * * @param index the index at which to insert the segment. * @param segment the segment or point to be inserted. * @return the added segment. This is not necessarily the same object, e.g. * if the segment to be added already belongs to another path. */ public Segment insert(int index, Segment segment) { return getSegments().add(index, segment); } /** * @jshide for now. TODO: Implement varargs in doclet Inserts a variable * amount of segment at a given index in the segment list of this * path. * * @param index the index at which to insert the segments. * * @return the added segments. These is not necessarily the same objects, * e.g. if the segments to be added already belongs to another path. */ public ReadOnlyList<? extends Segment> insert(int index, Segment... segments) { SegmentList segs = getSegments(); // Remember previous size so we can find out how many were really added int before = segs.size(); segs.addAll(index, Lists.asList(segments)); return segs.getSubList(index, index + segs.size() - before); } public Segment remove(int index) { return getSegments().remove(index); } /** * @deprecated */ public Segment remove(Segment segment) { return getSegments().remove(segment); } /** * @jshide for now. TODO: Implement varargs in doclet Adds a variable amount * of segments at the end of the segment list of this path. * * @return the added segments. These are not necessarily the same objects, * e.g. if the segments to be added already belongs to another path. */ public ReadOnlyList<? extends Segment> remove(Segment... segments) { SegmentList segs = getSegments(); ExtendedArrayList<Segment> removed = new ExtendedArrayList<Segment>(segments); removed.retainAll(segs); if (segs.removeAll(Lists.asList(segments))) return removed; return null; } public ReadOnlyList<? extends Segment> remove(int fromIndex, int toIndex) { SegmentList segs = getSegments(); ReadOnlyList<? extends Segment> removed = segs.getSubList(fromIndex, toIndex); segs.remove(fromIndex, toIndex); return removed; } /** * Removes the path item from the document. */ public boolean remove() { boolean ret = super.remove(); // Dereference from path if they're used somewhere else! if (segments != null) segments.path = null; return ret; } public Object clone() { CommitManager.commit(this); return super.clone(); } /** * The segments contained within the path. */ public SegmentList getSegments() { if (segments == null) segments = new SegmentList(this); else segments.update(); return segments; } public void setSegments(ReadOnlyList<? extends Segment> segments) { SegmentList segs = getSegments(); // TODO: Implement SegmentList.setAll so removeAll is not necessary and // nativeCommit is used instead of nativeInsert removeRange would still // be needed in cases the new list is smaller than the old one... segs.removeAll(); segs.addAll(segments); } public void setSegments(Segment[] segments) { setSegments(Lists.asList(segments)); } private void updateSize(int size) { // Increase version as all segments have changed version++; if (segments != null) segments.updateSize(size); } /** * The curves contained within the path. */ public CurveList getCurves() { if (curves == null) curves = new CurveList(this, getSegments()); return curves; } public Segment getFirstSegment() { return segments.getFirst(); } public Segment getLastSegment() { return segments.getLast(); } public Curve getFirstCurve() { return getCurves().getFirst(); } public Curve getLastCurve() { return getCurves().getLast(); } /** * Specifies whether the path is closed. If it is closed, Illustrator * connects the first and last segments. */ public native boolean isClosed(); private native void nativeSetClosed(boolean closed); public void setClosed(boolean closed) { // Amount of curves may change when closed is modified nativeSetClosed(closed); if (curves != null) curves.updateSize(); } /** * Specifies whether the path is used as a guide. */ public native boolean isGuide(); public native void setGuide(boolean guide); /** * The length of the perimeter of the path. */ public native double getLength(); /** * The area of the path in square points. Self-intersecting paths can * contain sub-areas that cancel each other out. */ public native float getArea(); private native void nativeReverse(); /** * Reverses the segments of the path. */ public void reverse() { // First save all changes: CommitManager.commit(this); // Reverse underlying AI structures: nativeReverse(); // Increase version as all segments have changed version++; } private native int nativePointsToCurves(float tolerance, float threshold, int cornerRadius, float scale); /** * Approximates the path by converting the points in the path to curves. It * only uses the {@link Segment#getPoint()} property of each segment and * ignores the {@link Segment#getHandleIn()} and * {@link Segment#getHandleOut()} properties. * * @param tolerance a smaller tolerance gives a more exact fit and more * segments, a larger tolerance gives a less exact fit and fewer * segments. {@default 2.5} * @param threshold {@default 1} * @param cornerRadius if, at any point in the fitted curve, the radius of * an inscribed circle that has the same tangent and curvature is * less than the cornerRadius, a corner point is generated there; * otherwise the path is smooth at that point. {@default 1} * @param scale the scale factor by which the points and other input units * (such as the corner radius) are multiplied. {@default 1} */ public void pointsToCurves(float tolerance, float threshold, int cornerRadius, float scale) { updateSize(nativePointsToCurves(tolerance, threshold, cornerRadius, scale)); } public void pointsToCurves(float tolerance, float threshold, int cornerRadius) { pointsToCurves(tolerance, threshold, cornerRadius, 1f); } public void pointsToCurves(float tolerance, float threshold) { pointsToCurves(tolerance, threshold, 1, 1f); } public void pointsToCurves(float tolerance) { pointsToCurves(tolerance, 1f, 1, 1f); } public void pointsToCurves() { pointsToCurves(2.5f, 1f, 1, 1f); } private native int nativeCurvesToPoints(float maxPointDistance, float flatness); /** * Converts the curves in the path to points. * * @param maxPointDistance the maximum distance between the generated points * {@default 1000} * @param flatness a value which controls the exactness of the algorithm * {@default 0.1} */ public void curvesToPoints(double maxPointDistance, double flatness) { int size = nativeCurvesToPoints((float) maxPointDistance, (float) flatness); updateSize(size); } public void curvesToPoints(double maxPointDistance) { curvesToPoints(maxPointDistance, 0.1f); } public void curvesToPoints() { curvesToPoints(1000f, 0.1f); } private native void nativeReduceSegments(float flatness); /** * Reduces the amount of segments in the path. * * @param flatness a value which controls the exactness of the algorithm * {@default 0.1} */ public void reduceSegments(double flatness) { nativeReduceSegments((float) flatness); updateSize(-1); } public void reduceSegments() { reduceSegments(0.1f); } public Path split(double offset) { return split(getLocation(offset)); } public Path split(CurveLocation location) { return location != null ? split(location.getIndex(), location.getParameter()) : null; } public Path split(int index, double parameter) { if (parameter < 0.0) parameter = 0.0; else if (parameter >= 1.0) { // t = 1 is the same as t = 0 and index ++ index++; parameter = 0.0; } SegmentList segments = getSegments(); CurveList curves = getCurves(); if (index >= 0 && index < curves.size()) { boolean hasTabletData = hasTabletData(); // If there is tablet data, we need to measure the offset of // the split point, as a value between 0 and 1 double length, partLength; if (hasTabletData) { length = getLength(); // Add up length of the new path by getting the curves lengths partLength = 0; for (int i = 0; i < index; i++) partLength += curves.get(i).getLength(); } else { length = partLength = 0; } // Only divide curves if we're not on an existing segment already if (parameter > 0.0) { // Divide the curve with the index at given parameter Curve curve = curves.get(index); curve.divide(parameter); if (hasTabletData) partLength += curve.getLength(); // Dividing adds more segments to the path index++; } // Create the new path with the segments to the right of given parameter ExtendedList<Segment> newSegments = segments.getSubList(index, segments.size()); // If the path was closed, make it an open one and move the segments around, // instead of creating a new path. Otherwise create two paths. if (isClosed()) { // Changing an item's segments also seems to change user attributes, // i.e. selection state, so save them and restore them again. int attributes = getAttributes(); newSegments.addAll(segments.getSubList(0, index + 1)); setSegments(newSegments); setClosed(false); setAttributes(attributes); if (hasTabletData) nativeSwapTabletData(partLength / length); return this; } else if (index > 0) { // Delete the segments from the current path, not including the divided point segments.remove(index + 1, segments.size()); // TODO: Instead of cloning, find a way to copy all necessary attributes over? // AIArtSuite::TransferAttributes? // TODO: Split TabletData arrays as well! kTransferLivePaintPathTags? Path newPath = (Path) clone(); newPath.setSegments(newSegments); nativeSplitTabletData(partLength / length, newPath); return newPath; } } return null; } public Path split(int index) { return split(index, 0); } public Path split(Point point) { return split(getLocation(point)); } public boolean join(Path path) { if (path != null) { SegmentList segments1 = getSegments(); SegmentList segments2 = path.getSegments(); Segment last1 = segments1.getLast(); Segment last2 = segments2.getLast(); if (last1.point.equals(last2.point)) { path.reverse(); } Segment first2 = segments2.getFirst(); if (last1.point.equals(first2.point)) { last1.handleOut.set(first2.handleOut); segments1.addAll(segments2.getSubList(1, segments2.size())); } else { Segment first1 = segments1.getFirst(); if (first1.point.equals(first2.point)) { path.reverse(); } last2 = segments2.getLast(); if (first1.point.equals(last2.point)) { first1.handleIn.set(last2.handleIn); // Prepend all segments from segments2 except last one segments1.addAll(0, segments2.getSubList(0, segments2.size() - 1)); } else { segments1.addAll(segments2); } } // TODO: Tablet data! path.remove(); // Close if they touch in both places Segment first1 = segments1.getFirst(); last1 = segments1.getLast(); if (last1.point.equals(first1.point)) { first1.handleIn.set(last1.handleIn); segments1.remove(segments1.size() - 1); setClosed(true); } return true; } return false; } /** * Smooth bezier curves without changing the amount of segments or their * points, by only smoothing and adjusting their handle points, for both * open ended and closed paths. * * @author Oleg V. Polikarpotchkin */ public void smooth() { getSegments().smooth(isClosed()); } public CurveLocation getLocation(Point point, double precision) { CurveList curves = getCurves(); int length = curves.size(); for (int i = 0; i < length; i++) { Curve curve = curves.get(i); double t = curve.getParameter(point, precision); if (t >= 0) return new CurveLocation(curve, t); } return null; } public CurveLocation getLocation(Point point) { return getLocation(point, Curve.EPSILON); } // TODO: move to CurveList, to make accessible when not using // paths directly too? public CurveLocation getLocation(double offset) { CurveList curves = getCurves(); double currentLength = 0; for (int i = 0, l = curves.size(); i < l; i++) { double startLength = currentLength; Curve curve = curves.get(i); currentLength += curve.getLength(); if (currentLength >= offset) { // found the segment within which the length lies double t = curve.getParameter(offset - startLength); return new CurveLocation(curve, t); } } // it may be that through impreciseness of getLength, that the end of // the curves was missed: if (curves.size() > 0 && offset <= getLength()) { Curve curve = curves.getLast(); return new CurveLocation(curve, 1); } return null; } /** * @deprecated */ public CurveLocation getPositionWithLength(double length) { return getLocation(length); } protected Double getOffset(CurveLocation location) { Integer index = location.getIndex(); if (index != null) { double offset = 0; CurveList curves = getCurves(); for (int i = 0; i < index; i++) offset += curves.get(i).getLength(); Curve curve = curves.get(index); return offset + curve.getLength(0, location.getParameter()); } return null; } /** * @deprecated */ public Double getLengthOfPosition(CurveLocation location) { return getOffset(location); } /** * Returns the point of the path at the given offset. */ public Point getPoint(double offset) { CurveLocation loc = getLocation(offset); if (loc != null) return loc.getPoint(); return null; } /** * Returns the tangential vector to the path at the given offset as a vector * point. */ public Point getTangent(double offset) { CurveLocation loc = getLocation(offset); if (loc != null) return loc.getTangent(); return null; } /** * Returns the normal vector to the path at the given offset as a vector * point. */ public Point getNormal(double offset) { CurveLocation loc = getLocation(offset); if (loc != null) return loc.getNormal(); return null; } /* * Tablet Data Stuff */ private static final int /** Stylus pressure. */ TABLET_PRESSURE = 0, /** Stylus wheel pressure, also called tangential or barrel pressure */ TABLET_BARREL_PRESSURE = 1, /** Tilt, also called altitude. */ TABLET_TILT = 2, /** Bearing, also called azimuth. */ TABLET_BEARING = 3, /** Rotation. */ TABLET_ROTATION = 4; private native float[][] nativeGetTabletData(int type); private native void nativeSetTabletData(int type, float[][] data); private native boolean nativeSplitTabletData(double offset, Path other); private native boolean nativeSwapTabletData(double offset); public native boolean hasTabletData(); /** * {@grouptitle Tablet Data} */ public float[][] getTabletPressure() { return nativeGetTabletData(TABLET_PRESSURE); } public void setTabletPressure(float[][] data) { nativeSetTabletData(TABLET_PRESSURE, data); } public float[][] getTabletWheel() { return nativeGetTabletData(TABLET_BARREL_PRESSURE); } public void setTabletWheel(float[][] data) { nativeSetTabletData(TABLET_BARREL_PRESSURE, data); } public float[][] getTabletTilt() { return nativeGetTabletData(TABLET_TILT); } public void setTabletTilt(float[][] data) { nativeSetTabletData(TABLET_TILT, data); } public float[][] getTabletBearing() { return nativeGetTabletData(TABLET_BEARING); } public void setTabletBearing(float[][] data) { nativeSetTabletData(TABLET_BEARING, data); } public float[][] getTabletRotation() { return nativeGetTabletData(TABLET_ROTATION); } public void setTabletRotation(float[][] data) { nativeSetTabletData(TABLET_ROTATION, data); } /** * @deprecated Use {@link #getTabletPressure())} instead. */ public float[][] getTabletData() { return nativeGetTabletData(TABLET_PRESSURE); } /** * @deprecated Use {@link #setTabletPressure())} instead. */ public void setTabletData(float[][] data) { nativeSetTabletData(TABLET_PRESSURE, data); } @Override public void moveTo(double x, double y) { getSegments().moveTo(x, y); } @Override public void lineTo(double x, double y) { getSegments().lineTo(x, y); } @Override public void cubicCurveTo(double handle1X, double handle1Y, double handle2X, double handle2Y, double toX, double toY) { getSegments().cubicCurveTo(handle1X, handle1Y, handle2X, handle2Y, toX, toY); } @Override public void quadraticCurveTo(double handleX, double handleY, double toX, double toY) { getSegments().quadraticCurveTo(handleX, handleY, toX, toY); } @Override public void curveTo(double throughX, double throughY, double toX, double toY, double parameter) { getSegments().curveTo(throughX, throughY, toX, toY, parameter); } @Override public void arcTo(double x, double y, boolean clockwise) { getSegments().arcTo(x, y, clockwise); } @Override public void arcTo(double throughX, double throughY, double toX, double toY) { getSegments().arcTo(throughX, throughY, toX, toY); } @Override public void lineBy(double x, double y) { getSegments().lineBy(x, y); } @Override public void curveBy(double throughX, double throughY, double toX, double toY, double parameter) { getSegments().curveBy(throughX, throughY, toX, toY, parameter); } @Override public void arcBy(double x, double y, boolean clockwise) { getSegments().arcBy(x, y, clockwise); } @Override public void arcBy(double throughX, double throughY, double toX, double toY) { getSegments().arcBy(throughX, throughY, toX, toY); } @Override public void closePath() { setClosed(true); } /** * Converts to a Java2D shape. * * @jshide */ public GeneralPath toShape() { GeneralPath path = new GeneralPath(); SegmentList segments = getSegments(); Segment first = segments.getFirst(); path.moveTo((float) first.point.x, (float) first.point.y); Segment seg = first; for (int i = 1, l = segments.size(); i < l; i++) { Segment next = segments.get(i); addSegment(path, seg, next); seg = next; } if (isClosed()) { addSegment(path, seg, first); path.closePath(); } path.setWindingRule(getStyle().getWindingRule() == WindingRule.NON_ZERO ? GeneralPath.WIND_NON_ZERO : GeneralPath.WIND_EVEN_ODD); return path; } private static void addSegment(GeneralPath path, Segment current, Segment next) { Point point1 = current.point; Point handle1 = current.handleOut; Point handle2 = next.handleIn; Point point2 = next.point; if (handle1.isZero() && handle2.isZero()) { path.lineTo( (float) point2.x, (float) point2.y ); } else { // TODO: Is there an easy way to detect quads? path.curveTo( (float) (point1.x + handle1.x), (float) (point1.y + handle1.y), (float) (point2.x + handle2.x), (float) (point2.y + handle2.y), (float) point2.x, (float) point2.y ); } } protected List<Curve> getAllCurves() { return getCurves(); } }