/* * __ .__ .__ ._____. * _/ |_ _______ __|__| ____ | | |__\_ |__ ______ * \ __\/ _ \ \/ / |/ ___\| | | || __ \ / ___/ * | | ( <_> > <| \ \___| |_| || \_\ \\___ \ * |__| \____/__/\_ \__|\___ >____/__||___ /____ > * \/ \/ \/ \/ * * Copyright (c) 2006-2011 Karsten Schmidt * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * http://creativecommons.org/licenses/LGPL/2.1/ * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA */ package toxi.geom; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** *

* This is a generic 3D B-Spline class for curves of arbitrary length, control * handles and patches are created and joined automatically as described here: * ibiblio.org/e-notes/ * Splines/Bint.htm *

* *

* Thanks to a bug report by Aaron Meyers (http://universaloscillation.com) the * {@linkplain #toLineStrip2D(int)} method has a slightly changed behaviour from * version 0014 onwards. In earlier versions erroneous duplicate points would be * added near each given control point, which lead to various weird results. *

* *

* The new behaviour of the curve interpolation/computation is described in the * docs for the {@linkplain #toLineStrip2D(int)} method below. *

* * @version 0014 Added user adjustable curve tightness control * @version 0015 Added JAXB annotations and List support for dynamic building of * spline */ public class Spline2D { /** * */ public static final float DEFAULT_TIGHTNESS = 0.25f; /** * */ public static final int DEFAULT_RES = 16; /** * */ protected Vec2D[] points; /** * */ public List pointList = new ArrayList<>(); /** * */ public BernsteinPolynomial bernstein; /** * */ public Vec2D[] delta; /** * */ public Vec2D[] coeffA; /** * */ public float[] bi; /** * */ protected float tightness; /** * */ protected float invTightness; /** * Constructs an empty spline container with default curve tightness. You * need to populate the spline manually by using {@link #add(ReadonlyVec2D)} * . */ public Spline2D() { setTightness(DEFAULT_TIGHTNESS); } /** * @param rawPoints * list of control point vectors */ public Spline2D(List rawPoints) { this(rawPoints, null, DEFAULT_TIGHTNESS); } /** * @param rawPoints * list of control point vectors * @param b * predefined Bernstein polynomial (good for reusing) * @param tightness * default curve tightness used for the interpolated vertices * {@linkplain #setTightness(float)} */ public Spline2D(List rawPoints, BernsteinPolynomial b, float tightness) { pointList.addAll(rawPoints); bernstein = b; setTightness(tightness); } /** * @param pointArray * array of control point vectors */ public Spline2D(Vec2D[] pointArray) { this(pointArray, null, DEFAULT_TIGHTNESS); } /** * @param pointArray * array of control point vectors * @param b * predefined Bernstein polynomial (good for reusing) * @param tightness * default curve tightness used for the interpolated vertices * {@linkplain #setTightness(float)} */ public Spline2D(Vec2D[] pointArray, BernsteinPolynomial b, float tightness) { this(Arrays.asList(pointArray), b, tightness); } /** * * @param x * @param y * @return */ public Spline2D add(float x, float y) { return add(new Vec2D(x, y)); } /** * Adds a copy of the given point to the list of control points. * * @param p * @return itself */ public Spline2D add(ReadonlyVec2D p) { pointList.add(p.copy()); return this; } /** * */ protected void findCPoints() { bi[1] = -tightness; coeffA[1].set((points[2].x - points[0].x - delta[0].x) * tightness, (points[2].y - points[0].y - delta[0].y) * tightness); final int numP = getNumPoints(); for (int i = 2; i < numP - 1; i++) { bi[i] = -1 / (invTightness + bi[i - 1]); coeffA[i].set( -(points[i + 1].x - points[i - 1].x - coeffA[i - 1].x) * bi[i], -(points[i + 1].y - points[i - 1].y - coeffA[i - 1].y) * bi[i]); } for (int i = numP - 2; i > 0; i--) { delta[i].set(coeffA[i].x + delta[i + 1].x * bi[i], coeffA[i].y + delta[i + 1].y * bi[i]); } } /** * Returns the number of control points. * * @return the numP */ public final int getNumPoints() { return pointList.size(); } /** * @return the pointList */ public List getPointList() { return pointList; } /** * @see #setTightness(float) * @return the spline3d tightness * @since 0014 (rev.216) */ public float getTightness() { return tightness; } /** * Overrides the current control points with the given list. * * @param plist * the pointList to set * @return itself */ public Spline2D setPointList(List plist) { pointList.clear(); plist.forEach((p) -> { pointList.add(p.copy()); }); return this; } /** * Sets the tightness for future curve interpolation calls. Default value is * 0.25. A value of 0.0 equals linear interpolation between the given curve * input points. Curve behaviour for values outside the 0.0 .. 0.5 interval * is unspecified and becomes increasingly less intuitive. Negative values * are possible too and create interesting results (in some cases). * * @param tightness * the tightness value used for the next call to * {@link #toLineStrip2D(int)} * @return * @since 0014 (rev. 216) */ public final Spline2D setTightness(float tightness) { this.tightness = tightness; this.invTightness = 1f / tightness; return this; } /** *

* Computes all curve vertices based on the resolution/number of * subdivisions requested. The higher, the more vertices are computed: *

*

* (number of control points - 1) * resolution + 1 *

*

* Since version 0014 the automatic placement of the curve handles can also * be manipulated via the {@linkplain #setTightness(float)} method. *

* * @param res * the number of vertices to be computed per segment between * original control points (incl. control point always at the * start of each segment) * @return list of Vec2D vertices along the curve */ public LineStrip2D toLineStrip2D(int res) { updateCoefficients(); if (res < 1) { res = 1; } res++; if (bernstein == null || bernstein.resolution != res) { bernstein = new BernsteinPolynomial(res); } findCPoints(); Vec2D deltaP = new Vec2D(); Vec2D deltaQ = new Vec2D(); res--; LineStrip2D strip = new LineStrip2D(); for (int i = 0, numP = getNumPoints(); i < numP - 1; i++) { Vec2D p = points[i]; Vec2D q = points[i + 1]; deltaP.set(delta[i]).addSelf(p); deltaQ.set(q).subSelf(delta[i + 1]); for (int k = 0; k < res; k++) { float x = p.x * bernstein.b0[k] + deltaP.x * bernstein.b1[k] + deltaQ.x * bernstein.b2[k] + q.x * bernstein.b3[k]; float y = p.y * bernstein.b0[k] + deltaP.y * bernstein.b1[k] + deltaQ.y * bernstein.b2[k] + q.y * bernstein.b3[k]; strip.add(new Vec2D(x, y)); } } strip.add(points[points.length - 1].copy()); return strip; } /** * */ public void updateCoefficients() { final int numP = getNumPoints(); if (points == null || (points != null && points.length != numP)) { coeffA = new Vec2D[numP]; delta = new Vec2D[numP]; bi = new float[numP]; for (int i = 0; i < numP; i++) { coeffA[i] = new Vec2D(); delta[i] = new Vec2D(); } } setTightness(tightness); points = pointList.toArray(new Vec2D[numP]); } }