/*
* 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 14.12.2004.
*/
package com.scriptographer.ai;
import com.scratchdisk.script.ArgumentReader;
import com.scratchdisk.script.ChangeReceiver;
import com.scriptographer.Committable;
import com.scriptographer.CommitManager;
/**
* The Segment object represents a part of a path which is described by the
* {@link Path#getSegments()} array. Every segment of a path corresponds to
* an anchor point (anchor points are the path handles that are visible when the
* path is selected).
*
* @author lehni
*/
public class Segment implements Committable, ChangeReceiver {
protected SegmentList segments;
protected int index;
// The internal points
protected SegmentPoint point;
protected SegmentPoint handleIn;
protected SegmentPoint handleOut;
// Corner (hidden to the API, but needed for AI)
protected boolean corner;
// The selection state is fetched the first time it's used
protected short selectionState = SELECTION_FETCH;
//
protected int version = -1;
protected int selectionVersion = -1;
protected short dirty = DIRTY_NONE;
// Dirty flags, to be combined bitwise
protected final static short
DIRTY_NONE = 0,
DIRTY_POINTS = 1,
DIRTY_SELECTION = 2;
// For selectionState, based on AIPathSegementSelectionState
protected final static short
SELECTION_FETCH = -1,
SELECTION_NONE = 0,
SELECTION_POINT = 1,
SELECTION_HANDLE_IN = 2,
SELECTION_HANDLE_OUT = 3,
SELECTION_HANDLE_BOTH = 4;
public Segment() {
init(0, 0, 0, 0, 0, 0);
}
/**
* Creates a new Segment object.
*
* Sample code:
* <code>
* var handleIn = new Point(-40, -50);
* var handleOut = new Point(40, 50);
*
* var firstPoint = new Point(100, 50);
* var firstSegment = new Segment(firstPoint, null, handleOut);
*
* var secondPoint = new Point(200, 50);
* var secondSegment = new Segment(secondPoint, handleIn, null);
*
* var path = new Path();
* path.segments = [firstSegment, secondSegment];
* </code>
*
* @param pt the anchor point of the segment
* @param handleIn the handle point relative to the anchor point of the
* segment that describes the in tangent of the segment. {@default x:
* 0, y: 0}
* @param handleOut the handle point relative to the anchor point of the
* segment that describes the out tangent of the segment. {@default
* x: 0, y: 0}
*/
public Segment(Point pt, Point handleIn, Point handleOut) {
init(pt, handleIn, handleOut);
}
public Segment(Point pt, Point handleIn) {
init(pt, handleIn, null);
}
public Segment(Point pt) {
init(pt, null, null);
}
/**
* Creates a new Segment object.
*
* Sample code:
* <code>
* var handleIn = new Point(-40, -50);
* var handleOut = new Point(40, 50);
*
* var firstSegment = new Segment(100, 50, 0, 0, handleOut.x, handleOut.y);
* var secondSegment = new Segment(200, 50, handleIn.x, handleIn.y, 0, 0);
*
* var path = new Path();
* path.segments = [firstSegment, secondSegment];
* </code>
*
* @param x the x coordinate of the anchor point of the segment
* @param y the y coordinate of the anchor point of the segment
* @param inX the x coordinate of the handle point relative to the anchor
* point of the segment that describes the in tangent of the segment.
* {@default 0}
* @param inY the y coordinate of the handle point relative to the anchor
* point of the segment that describes the in tangent of the segment.
* {@default 0}
* @param outX the x coordinate of the handle point relative to the anchor
* point of the segment that describes the out tangent of the
* segment. {@default 0}
* @param outY the y coordinate of the handle point relative to the anchor
* point of the segment that describes the out tangent of the
* segment. {@default 0}
*/
public Segment(double x, double y, double inX, double inY, double outX,
double outY) {
init(x, y, inX, inY, outX, outY);
}
public Segment(double x, double y) {
init(x, y, 0, 0, 0, 0);
}
/**
* @jshide
*/
public Segment(ArgumentReader reader) {
// First try reading a point, no matter if it is a hash or a array.
// If that does not work, fall back to other scenarios:
Point point = getPoint(reader, "point", true);
if (point != null) {
init(
point,
getPoint(reader, "handleIn", false),
getPoint(reader, "handleOut", false)
);
} else {
reader.revert();
if (reader.isMap()) {
if (reader.has("x")) {
init(
reader.readDouble("x", 0),
reader.readDouble("y", 0),
0, 0, 0, 0
);
}
} else {
init(
reader.readDouble(0),
reader.readDouble(0),
reader.readDouble(0),
reader.readDouble(0),
reader.readDouble(0),
reader.readDouble(0)
);
}
}
}
private static Point getPoint(ArgumentReader reader, String name, boolean allowNull) {
Point point = reader.readObject(name, Point.class);
return allowNull || point != null ? point : new Point();
}
protected Segment(SegmentList segments, int index) {
this();
this.segments = segments;
this.index = index;
}
/**
* Creates a new Segment object from the specified Segment object.
*
* @param segment
*/
public Segment(Segment segment) {
point = new SegmentPoint(this, 0, segment.point);
handleIn = new SegmentPoint(this, 2, segment.handleIn);
handleOut = new SegmentPoint(this, 4, segment.handleOut);
corner = segment.corner;
}
protected void init(double x, double y, double inX, double inY, double outX,
double outY) {
point = new SegmentPoint(this, 0, x, y);
handleIn = new SegmentPoint(this, 2, inX, inY);
handleOut = new SegmentPoint(this, 4, outX, outY);
// Calculate corner accordingly
corner = !handleIn.isColinear(handleOut);
}
protected void init(Point pt, Point in, Point out) {
point = new SegmentPoint(this, 0, pt);
handleIn = new SegmentPoint(this, 2, in);
handleOut = new SegmentPoint(this, 4, out);
// Calculate corner accordingly
corner = !handleIn.isColinear(handleOut);
}
/**
* Read and write directly to native segment struct, which is represented
* here as a float array. Byte alignment wise this works since all fields
* are floats except the last one which is a boolean, but aligns like float
* too.
* We use double precision for all calculations but still have to store
* as floats, since that's what Illustrator uses. Calculations in Sg
* will be much more precise though.
*
* Warning: This does not call markDirty(). This needs to be taken care of
* after.
*
* @param values
* @param valueIndex
*/
protected void setValues(float[] values, int valueIndex) {
float x = values[valueIndex];
float y = values[valueIndex + 1];
point.x = x;
point.y = y;
handleIn.x = values[valueIndex + 2] - x;
handleIn.y = values[valueIndex + 3] - y;
handleOut.x = values[valueIndex + 4] - x;
handleOut.y = values[valueIndex + 5] - y;
corner = values[valueIndex + 6] != 0;
}
protected void getValues(float[] values, int valueIndex) {
double x = point.x;
double y = point.y;
values[valueIndex] = (float) x;
values[valueIndex + 1] = (float) y;
values[valueIndex + 2] = (float) (handleIn.x + x);
values[valueIndex + 3] = (float) (handleIn.y + y);
values[valueIndex + 4] = (float) (handleOut.x + x);
values[valueIndex + 5] = (float) (handleOut.y + y);
// Don't care about the exact value for true, as long as it's != 0 it
// works:
values[valueIndex + 6] = corner ? 1f : 0f;
}
public void commit(boolean endExecution) {
if (dirty != DIRTY_NONE && segments != null && segments.path != null) {
Path path = segments.path;
path.checkValid();
if ((dirty & DIRTY_POINTS) != 0) {
SegmentList.nativeSet(path.handle, path.document.handle, index,
(float) point.x,
(float) point.y,
(float) (handleIn.x + point.x),
(float) (handleIn.y + point.y),
(float) (handleOut.x + point.x),
(float) (handleOut.y + point.y),
corner);
}
if ((dirty & DIRTY_SELECTION) != 0) {
SegmentList.nativeSetSelectionState(path.handle,
path.document.handle, index, selectionState);
}
dirty = DIRTY_NONE;
// Update to current path version after commit.
version = segments.path.version;
path.setModified();
}
}
/**
* inserts this segment in the underlying AI path at position index
* Only call once, when adding this segment to the segmentList!
*/
protected void insert() {
if (segments != null && segments.path != null) {
Path path = segments.path;
path.checkValid();
SegmentList.nativeInsert(path.handle, path.document.handle, index,
(float) point.x,
(float) point.y,
(float) (handleIn.x + point.x),
(float) (handleIn.y + point.y),
(float) (handleOut.x + point.x),
(float) (handleOut.y + point.y),
corner);
dirty = DIRTY_NONE;
// Update to current version after commit.
version = segments.path.version;
path.setModified();
}
}
protected void markDirty(int dirty) {
// Only mark it as dirty if it's attached to a path already and
// if the given dirty flags are not already set
if ((this.dirty & dirty) != dirty && segments != null
&& segments.path != null) {
CommitManager.markDirty(segments.path, this);
this.dirty |= dirty;
}
}
protected void update(boolean updateSelection) {
if ((dirty & DIRTY_POINTS) == 0 && segments != null
&& segments.path != null && segments.path.needsUpdate(version)) {
// This handles all the updating automatically:
segments.get(index);
// Version has changed, force getting of selection state:
selectionState = SELECTION_FETCH;
} else if ((dirty & DIRTY_SELECTION) == 0 // Only fetch if not dirty
&& selectionVersion != CommitManager.version) {
selectionState = SELECTION_FETCH;
}
if (updateSelection && selectionState == SELECTION_FETCH) {
if (segments != null && segments.path != null) {
segments.path.checkValid();
selectionState = SegmentList.nativeGetSelectionState(
segments.path.handle, index);
} else {
selectionState = SELECTION_NONE;
}
// Selection uses its own version number as it might change
// regardless of whether the path itself changes or not.
selectionVersion = CommitManager.version;
}
}
public String toString() {
StringBuffer buf = new StringBuffer(64);
buf.append("{ point: ").append(point.toString());
if (handleIn.x != 0 || handleIn.y != 0)
buf.append(", handleIn: ").append(handleIn.toString());
if (handleOut.x != 0 || handleOut.y != 0)
buf.append(", handleOut: ").append(handleOut.toString());
buf.append(" }");
return buf.toString();
}
/**
* The anchor point of the segment.
*/
public SegmentPoint getPoint() {
update(false);
return point;
}
public void setPoint(Point pt) {
point.set(pt);
}
/**
* @jshide
*/
public void setPoint(double x, double y) {
point.set(x, y);
}
/**
* The handle point relative to the anchor point of the segment that
* describes the in tangent of the segment.
*/
public SegmentPoint getHandleIn() {
update(false);
return handleIn;
}
public void setHandleIn(Point pt) {
handleIn.set(pt);
// Update corner accordingly
corner = !handleIn.isColinear(handleOut);
}
/**
* @jshide
*/
public void setHandleIn(double x, double y) {
handleIn.set(x, y);
}
/**
* The handle point relative to the anchor point of the segment that
* describes the out tangent of the segment.
*/
public SegmentPoint getHandleOut() {
update(false);
return handleOut;
}
public void setHandleOut(Point pt) {
handleOut.set(pt);
// Update corner accordingly
corner = !handleIn.isColinear(handleOut);
}
/**
* @jshide
*/
public void setHandleOut(double x, double y) {
handleOut.set(x, y);
}
/**
* {@grouptitle Hierarchy}
*
* The index of the segment in the {@link Path#getSegments()} array that the
* segment belongs to.
*/
public int getIndex() {
return index;
}
/**
* The path that the segment belongs to.
*/
public Path getPath() {
return segments != null ? segments.path : null;
}
/**
* The curve that the segment belongs to.
*/
public Curve getCurve() {
if (segments != null && segments.path != null) {
CurveList curves = segments.path.getCurves();
int index = this.index;
// The last segment of an open path belongs to the last curve
if (!segments.path.isClosed() && index == segments.size() - 1)
index--;
// The curves list handles closing curves, so the curves.size
// is adjusted accordingly. just check to be in the boundaries here:
if (index < curves.size())
return curves.get(index);
}
return null;
}
/**
* {@grouptitle Sibling Segments}
*
* The next segment in the {@link Path#getSegments()} array that the segment
* belongs to. If the segments belongs to a closed path, the first segment
* is returned for the last segment of the path.
*/
public Segment getNext() {
if (segments != null) {
if (index + 1 < segments.size()) {
return segments.get(index + 1);
} else {
return segments.path != null && segments.path.isClosed()
? segments.getFirst() : null;
}
}
return null;
}
/**
* The previous segment in the {@link Path#getSegments()} array that the
* segment belongs to. If the segments belongs to a closed path, the last
* segment is returned for the first segment of the path.
*/
public Segment getPrevious() {
if (segments != null) {
if (index > 0) {
return segments.get(index - 1);
} else {
return segments.path != null && segments.path.isClosed()
? segments.getLast() : null;
}
}
return null;
}
protected boolean isSelected(SegmentPoint pt) {
update(true);
if (pt == point) {
return selectionState == SELECTION_POINT;
} else if (pt == handleIn) {
return selectionState == SELECTION_HANDLE_IN ||
selectionState == SELECTION_HANDLE_BOTH;
} else if (pt == handleOut) {
return selectionState == SELECTION_HANDLE_OUT ||
selectionState == SELECTION_HANDLE_BOTH;
}
return false;
}
protected void setSelected(SegmentPoint pt, boolean selected) {
if (segments == null || segments.path == null)
return;
update(true);
// Find the right combination of selection states (SELECTION_*)
boolean pointSelected = selectionState == SELECTION_POINT;
boolean handleInSelected = selectionState == SELECTION_HANDLE_IN
|| selectionState == SELECTION_HANDLE_BOTH;
boolean handleOutSelected = selectionState == SELECTION_HANDLE_OUT
|| selectionState == SELECTION_HANDLE_BOTH;
boolean changed = false;
if (pt == point) {
if (pointSelected != selected) {
if (selected) {
handleInSelected = false;
handleOutSelected = false;
} else {
Segment previous = getPrevious();
Segment next = getNext();
// When deselecting a point, the handles get selected
// instead depending on the selection state of their
// neighbors.
handleInSelected = previous != null
&& (previous.getPoint().isSelected()
|| previous.getHandleOut().isSelected());
handleOutSelected = next != null
&& (next.getPoint().isSelected()
|| next.getHandleIn().isSelected());
}
pointSelected = selected;
changed = true;
}
} else if (pt == handleIn) {
if (handleInSelected != selected) {
// When selecting handles, the point get deselected.
if (selected)
pointSelected = false;
handleInSelected = selected;
changed = true;
}
} else if (pt == handleOut) {
if (handleOutSelected != selected) {
// When selecting handles, the point get deselected.
if (selected)
pointSelected = false;
handleOutSelected = selected;
changed = true;
}
}
if (changed) {
short state;
if (pointSelected) {
state = SELECTION_POINT;
} else if (handleInSelected) {
if (handleOutSelected) {
state = SELECTION_HANDLE_BOTH;
} else {
state = SELECTION_HANDLE_IN;
}
} else if (handleOutSelected) {
state = SELECTION_HANDLE_OUT;
} else {
state = SELECTION_NONE;
}
// Only update if it changed
if (selectionState != state) {
selectionState = state;
markDirty(DIRTY_SELECTION);
}
}
}
public boolean isSelected() {
return isSelected(point);
}
public void setSelected(boolean selected) {
setSelected(point, selected);
}
/**
* Retruns the reversed the curve, without modifying the curve itself.
*/
public Segment reverse() {
update(false);
return new Segment(point, handleOut, handleIn);
}
public Object clone() {
update(false);
return new Segment(this);
}
public boolean equals(Object object) {
if (object instanceof Segment) {
Segment sg = (Segment) object;
return point.equals(sg.point)
&& handleOut.equals(sg.handleOut)
&& handleIn.equals(sg.handleIn)
&& corner == sg.corner;
}
return false;
}
public boolean remove() {
if (segments != null)
return segments.remove(index) != null;
return false;
}
}