2
\$\begingroup\$

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:

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

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

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?

\$\endgroup\$

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.