# Modeling a control polygon for a piecewise spline curve

## Aim

I'm modeling a control polygon for a piecewise spline curve. Each sub-spline is defined by a location the spline must pass through, as well as a forward tangent, and a backwards tangent. The control points and their tangents can be freely manipulated.

I aim at keeping this flexible, so the forward and backward tangents may or may not be colinear. If the user want to make them colinear, I want to be able to express this as a Constraint, that holds true even when the control points and/or its tangents are manipulated.

Additionally, a control point can be reused in an other Piecewise spline, to make a network of them. Think Train Fever's free-form tracklaying system: ## Here is a usage scenario:

1. Have a start point A with a forward tangent Afwd.
2. Have an end point C with a backward tangent Cbwd.
3. Build a spline out of that control polygon: Awfd---->Cbwd
4. Insert a mid-point B with two tangents (Bfwd/Bbwd)
5. Require B's tangents to be opposite.
6. Build a spline out of that control polygon: Awfd---->[Bbwd|Bfwd]---->Cbwd
7. Move the tangent Bfwd.
-> The tangent Bbwd is automatically altered to the opposite
8. Break the constraint on B's tangents
-> The tangent Bbwd and Bfwd are automatically freed

## Here's how I did it

• A ControlPoint class, which has location, and references two TangencyConstraint objects
• A ControlPolygon class, is a collection of ControlPoints. Has a Builder, and can generate an immutable PiecewiseBezier spline
• A TangencyConstraint interface that provides a tangent vector
• An OppositeTangency which is a TangencyConstraint. Works in pair, one isthe opposite of the other. Is an inner (non-static) class of OppositeTangencyManager
• An OppositeTangencyManager class wich manages two OppositeTangency. Makessure both are opposite, and that if one side wishes to break the constraint, the other is informed and becomes a free tangent.

## Here's the code

ControlPoint.java:

public class ControlPoint {

private final Point2D location;
private final AtomicReference<TangencyConstraint> forwardTangency;
private final AtomicReference<TangencyConstraint> backwardTangency;

public ControlPoint(Point2D location, Point2D backwardTangent, Point2D forwardTangent) {
this.location = new Point2D.Double(location.getX(), location.getY());
this.backwardTangency = new AtomicReference<>(new AbsoluteTangency(forwardTangent));
this.forwardTangency = new AtomicReference<>(new AbsoluteTangency(forwardTangent));
}

public Point2D getLocation() {
return new Point2D.Double(location.getX(), location.getY());
}

public Point2D getForwardTangent() {
return forwardTangency.get().get();
}

public Point2D getBackwardTangent() {
return backwardTangency.get().get();
}

public void moveForwardTangency(Point2D dir) {
forwardTangency.get().move(dir);
}

public void imposeOppositeTangency(Point2D forwardDirection) {
OppositeTangencyManager.lock(forwardTangency, backwardTangency, forwardDirection);
}

public void imposeOppositeTangency(boolean keepForward) {
Point2D direction;
if (keepForward) {
direction = forwardTangency.get().get();
} else {
Point2D opp = backwardTangency.get().get();
direction = new Point2D.Double(-opp.getX(), -opp.getY());
}
OppositeTangencyManager.lock(forwardTangency, backwardTangency, direction);
}

public void freeTangency() {
forwardTangency.get().destroy();
backwardTangency.get().destroy();
}
}


ControlPolygon.java:

/**  A ControlPolygon is a collection of {@link ControlPoint}s. Has a {@link Builder}, and can generate an immutable PiecewiseBezier spline */
public class ControlPolygon {
/** Builds a {@link ControlPolygon} */
public static class Builder {
private final List<ControlPoint> controlPolygon = new ArrayList<>();

public Builder(Point2D begin) {
controlPolygon.add(new ControlPoint(begin, new Point2D.Double(), new Point2D.Double()));
}

public Builder(Point2D begin, Point2D forwardTangent) {
}

public Builder through(Point2D next, Point2D backwardTangent, Point2D forwardTangent) {
return this;
}

public Builder endAt(Point2D next, Point2D backwardTangent) {
return this;
}

public ControlPolygon build() {
return new ControlPolygon(new ArrayList<>(controlPolygon));
}
}

private final List<ControlPoint> polygon;

private ControlPolygon(List<ControlPoint> poly) {
this.polygon = poly;
}

public ControlPoint getControlPoint(int i) {
return polygon.get(i);
}

/** Generates an immutable {@link PiecewiseBezier} curve using this ControlPolygon.
public PiecewiseBezier toBezier() {
Point2D p0 = polygon.get(0).getLocation();
PiecewiseBezier.Builder builder = new PiecewiseBezier.Builder(p0);
for (int i = 0; i < polygon.size() - 1; i++) {
ControlPoint cp1 = polygon.get(i);
ControlPoint cp2 = polygon.get(i + 1);
Point2D t1 = cp1.getForwardTangent();
Point2D t2 = cp2.getBackwardTangent();
Point2D p1 = new Point2D.Double(p0.getX() + t1.getX(), p0.getY() + t1.getY());
Point2D p3 = cp2.getLocation();
Point2D p2 = new Point2D.Double(p3.getX() - t2.getX(), p3.getY() - t2.getY());
p0 = p3; // loop
}
return builder.build();
}
}


TangencyConstraint.java interface :

/** Provides a tangent vector which may or may not be externally constrained. An eventual constrain can be destroyed. */
public interface TangencyConstraint {
Point2D get();
void destroy();
void move(Point2D dir);
}


OppositeTangencyManager.java:

/** Manages two {@link OppositeTangency} objects so they are always opposite. Can be disabled, but never re-enabled. */
public class OppositeTangencyManager {
/** A {@link TangencyConstraint} which is always the opposite of another one. */
public class OppositeTangency implements TangencyConstraint {

private final AtomicReference<TangencyConstraint> referenceToMe;

protected OppositeTangency(AtomicReference<TangencyConstraint> referenceToMe) {
super();
this.referenceToMe = referenceToMe;
}

@Override
public Point2D get() {
return OppositeTangencyManager.this.getTangencyForMe(this); // Don't know the current value, must ask the manager
}

@Override
public void destroy() {
OppositeTangencyManager.this.destroy(); // Can't do this myself, must ask the manager
}

@Override
public void move(Point2D dir) {
OppositeTangencyManager.this.move(this, dir); // Can't do this myself, must ask the manager
}
}

/** Creates a constraint on two references to Tangents so they stay opposite until it is disabled. */
public static void lock(AtomicReference<TangencyConstraint> forward, AtomicReference<TangencyConstraint> backward, Point2D direction) {
new OppositeTangencyManager(forward, backward, direction);
}

private final Point2D tangency; // The only place the shared tangent is stored
private boolean active = true; // Can only become false.
private final OppositeTangency forwardTangency;
private final OppositeTangency backwardTangency;

/** Displace the tangent, but keep the constraint. */
public void move(OppositeTangency oppositeTangency, Point2D dir) {
if (oppositeTangency == forwardTangency) {
tangency.setLocation(dir);
} else {
tangency.setLocation(-dir.getX(), -dir.getY());
}
}

private OppositeTangencyManager(AtomicReference<TangencyConstraint> forward, AtomicReference<TangencyConstraint> backward, Point2D direction) {
super();
this.tangency = direction;
this.forwardTangency = new OppositeTangency(forward);
this.backwardTangency = new OppositeTangency(backward);
forward.get().destroy(); // Must destroy the old ones before!
backward.get().destroy(); // Must destroy the old ones before!
forward.set(forwardTangency);
backward.set(backwardTangency);
}

public void destroy() {
if (!active) {
return;
}
active = false;
forwardTangency.referenceToMe.set(new AbsoluteTangency(new Point2D.Double(tangency.getX(), tangency.getY())));
backwardTangency.referenceToMe.set(new AbsoluteTangency(new Point2D.Double(-tangency.getX(), -tangency.getY())));
}

protected Point2D getTangencyForMe(OppositeTangency requestingTangency) {
if (!active) {
return null; // Should be handled better
}
if (requestingTangency == forwardTangency) {
return new Point2D.Double(tangency.getX(), tangency.getY());
} else {
return new Point2D.Double(-tangency.getX(), -tangency.getY());
}
}
}


PiecewiseBezier.java is way outside the scope. Any usual library works.

Example usage (complies with the scenario above):

public static void main(String[] argc) {
// 1. Have a start point A with a forward tangent Afwd.
Point2D A = new Point2D.Double(0, 0);
Point2D Afwd = new Point2D.Double(0, 1.0);
// 2. Have an end point C with a backward tangent Cbwd.
Point2D C = new Point2D.Double(10, 0);
Point2D Cbwd = new Point2D.Double(-0.5, -0.5);
// 3. Build a spline out of that control polygon:  Awfd---->Cbwd
Builder builder = new ControlPolygon.Builder(A, Afwd);
builder.endAt(C, Cbwd,);
ControlPolygon polyAC = builder.build();
PiecewiseBezier curveAC = polyAC.toBezier();
// 4. Insert a mid-point B with two tangents (Bfwd/Bbwd)
Point2D B = new Point2D.Double(5, 5);
Point2D Bbwd = new Point2D.Double(-1, 0);
Point2D Bfwd = new Point2D.Double(-1, 0.5);
builder.insert(1, B, Bbwd, Bfwd);
// 5. Require B's tangents to be opposite.
ControlPolygon polyABC = builder.build();
polyABC.getControlPoint(1).imposeOppositeTangency(true);
// 6. Build a spline out of that control polygon: Awfd---->[Bbwd|Bfwd]---->Cbwd
PiecewiseBezier bezierABC = polyABC.toBezier();
// 7. Move the tangent Bwfd.
//    -> The tangent Bbwd is automatically altered to the opposite
polyABC.getControlPoint(1).moveForwardTangency(new Point2D.Double(2,3));
// 9. Break the  constraint on B's tangents
//    -> The tangent Bbwd and Bfwd are automatically freed
polyABC.getControlPoint(1).freeTangency();
}


## What I'm looking for

I'm looking for a simpler, more elegant way to activate/deactivate/maintain those constraints than a Manager. I feel like this framework is a sledgehammer to crack a nut.

The builder is annoying. Should I not have made one? How to change the definition of the control polygon easily?

My control points are directed (have a forward, and a backward). This prevents me from defining a polygon as Abwd--->Bbwd at the moment. How can I remedy this? A ControlPointReverser?

I intend to allow making constraints between two distinct control point's tangents (like ctrlA.forceParallel(ctrlB)). I also might introduce constraints on location (like ctrlA.sitOnTopOf(ctrlB)) or even curvature. How do you feel about applying the same manager mechanic throughout all these constraints?

Any complaints on style?