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:

Train Fever

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


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) {

    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() {


/**  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) {
            controlPolygon.add(new ControlPoint(begin, new Point2D.Double(), forwardTangent));

        public Builder through(Point2D next, Point2D backwardTangent, Point2D forwardTangent) {
            controlPolygon.add(new ControlPoint(next, backwardTangent, forwardTangent));
            return this;

        public Builder endAt(Point2D next, Point2D backwardTangent) {
            controlPolygon.add(new ControlPoint(next, backwardTangent, new Point2D.Double()));
            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());
            builder.addBezier(p1, p2, p3);
            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);


/** 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) {
            this.referenceToMe = referenceToMe;

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

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

        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) {
        } else {
            tangency.setLocation(-dir.getX(), -dir.getY());

    private OppositeTangencyManager(AtomicReference<TangencyConstraint> forward, AtomicReference<TangencyConstraint> backward, Point2D direction) {
        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!

    public void destroy() {
        if (!active) {
        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();
    // 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

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?


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.