/* This file is part of the source code for 3D-XplorMath-J, Version 1.0 (January 2008). * Copyright (c) 2008 The 3D-XplorMath Consortium (http://3d-xplormath.org). * This source code is released under a BSD License, which allows redistribution * in source and binary form, with or without modification, provided copyright * and license information are included, and with no warranty or guarantee of * any kind. For details, see http://3d-xplormath.org/j/source/BSDLicense.txt */ package vmm.planecurve.parametric; import java.awt.Color; import java.awt.Graphics2D; import java.awt.geom.Ellipse2D; import java.awt.geom.Line2D; import vmm.core.Decoration; import vmm.core.Exhibit; import vmm.core.Transform; import vmm.core.VMMSave; import vmm.core.View; /** * A decoration that computes normal lines to points on a ParametricPlaneCurve and * that can also show osculating circles, the evolute of the curve, and one or two parallel curves. * (The points are those that are in the point array that defines the curve.) * Either unit normals or normals whose length is given by the radius of curvature * can be shown, and they can be shown at any number of points, including zero; * this allows a partial normal bundle to be shown, so that the bundle can be built * up gradually on the screen by adding one vector at a time. * The decoration can also show several features that are computed based on * the normal bundle: the evolute of the curve, an osculating circle at one point, * and parallel curves. */ public class NormalBundleDecoration extends Decoration { private PlaneCurveParametric curve; @VMMSave private int pointCt; // How many points are normals shown on? private Line2D.Double[] unitNormals; // Unit normals to the curve private Line2D.Double[] evoluteNormals; // Normals with length equal to radius of osculatting circles private Line2D.Double[] lines; // Actual normals drawn; pointer to either unitNormals or evoluteNormals @VMMSave private boolean useUnitNormals = false; @VMMSave private boolean showEvolute = false; private boolean showEvoluteWithOsculatingCircle = false; @VMMSave private boolean showTwoParallelCurves = true; @VMMSave private double parallelCurveOffset = Double.NaN; @VMMSave private int osculatingCircleIndex = -1; private double maxNormalLength; @VMMSave private Color normalColor = Color.red; @VMMSave private Color evoluteColor = new Color(0,200,0); @VMMSave private Color osculatingCircleColor = Color.blue; @VMMSave private Color parallelCurveColor = new Color(60,180,180); @VMMSave private Color parallelCurveColor2 = new Color(200,100,100); /** * Creates a normal bundle with no associated curve. The curve can be set later with * {@link #setCurve(PlaneCurveParametric)}. This constructor * exists mostly because it is required when the decoration is saved to an XML file. */ public NormalBundleDecoration() { } /** * Create a decoration that can be used on a specified curve. By default, nothing is * actually visible. Use the methods setShowEvolute(), setPointCount(), and setOsculatingCircleIndex() * to make various things visible. * @param curve The curve object to which the decoration applies. (A null value is OK but is unusual.) */ public NormalBundleDecoration(PlaneCurveParametric curve) { this.curve = curve; } /** * Returns the curve pn which the decoration is shown. */ public PlaneCurveParametric getCurve() { return curve; } /** * Set the curve on which this decoration is shown. (Usually, the curve is set in * the constructor, and if it is not, it will be set implicitely when computeDrawData * is called.) */ public void setCurve(PlaneCurveParametric c) { curve = c; } /** * Sets the number of points on the curve at which normals appear. If the * specified number of points is less than zero, it is treated as zero. * In the specified number is greater than the number of points on the curve * (that is, greater than 1+curve.getTResolution()), it is not considered an * error, and all normals are drawn. Thus, setting the number of points * to a very large value will ensure that all normals are drawn. */ public void setPointCount(int pointCt) { if (pointCt < 0) pointCt = 0; if (this.pointCt != pointCt) { this.pointCt = pointCt; fireDecorationChangeEvent(); } } /** * Gets the number of points on which normals appear. * @see #setPointCount(int) */ public int getPointCount() { return pointCt; } /** * Tells whether the evolute is curve is drawn. * @see #setShowEvolute(boolean) */ public boolean getShowEvolute() { return showEvolute; } /** * If set to true, then the evolute curve will be drawn. This is the curve through the * centers of the osculating circles. By default, it is not shown. */ public void setShowEvolute(boolean show) { if (show != showEvolute) { showEvolute = show; fireDecorationChangeEvent(); } } /** * Tells the distance between the curve and any parallel curves that are drawn. * The default value, Double.NaN, indicates that the parallel curves are not drawn. * @see #setParallelCurveOffset(double) */ public double getParallelCurveOffset() { return parallelCurveOffset; } /** * If the parallelCurveOffset is set to Double.NaN, then no parallel curve is drawn; this is * the default. Otherwise, a parallel curve is drawn that connects the endpoints of normal lines * of length given by the offsets. If in addition the value of showTwoParallelCurves is true, then * a second parallel curve is drawn with the negative of the specified offset. * @see #setShowTwoParallelCurves(boolean) */ public void setParallelCurveOffset(double offset) { if ( ! (offset == parallelCurveOffset || (Double.isNaN(offset) && Double.isNaN(parallelCurveOffset))) ) { parallelCurveOffset = offset; fireDecorationChangeEvent(); } } /** * Retruns the color used for drawing normal vectors. * @see #setNormalsColor(Color) */ public Color getNormalsColor() { return normalColor; } /** * Set the color used for the normal lines; the default is red. * @param color The color to be used. If this is null, the default (red) is used. */ public void setNormalsColor(Color color) { if (color == null) color = Color.red; if (!color.equals(normalColor)) { normalColor = color; if (pointCt > 0) fireDecorationChangeEvent(); } } /** * Returns the color used for drawing the evolute. * @see #setEvoluteColor(Color) */ public Color getEvoluteColor() { return evoluteColor; } /** * Set the color used for the evolute; the default is a medium dark green. * @param color The color to be used. If this is null, the default is used. */ public void setEvoluteColor(Color color) { if (color == null) color = new Color(0,200,0); if (!color.equals(evoluteColor)) { evoluteColor = color; if (showEvolute) fireDecorationChangeEvent(); } } /** * Retruns the color used for drawing the osculating circle. * @see #setOsculatingCircleColor(Color) */ public Color getOsculatingCircleColor() { return osculatingCircleColor; } /** * Set the color used for the osculating circle; the default is blue. * @param color The color to be used. If this is null, the default (blue) is used. */ public void setOsculatingCircleColor(Color color) { if (color == null) color = Color.blue; if (!color.equals(osculatingCircleColor)) { osculatingCircleColor = color; if (osculatingCircleIndex >= 0) fireDecorationChangeEvent(); } } /** * Returns the color used for drawing the (first) parallel curve. * @see #setParallelCurveColor(Color) */ public Color getParallelCurveColor() { return parallelCurveColor; } /** * Set the color used for the parallel curve; the default is a dark-ish cyan. * @param color The color to be used. If this is null, the default is used. */ public void setParallelCurveColor(Color color) { if (color == null) color = new Color(60,180,180); if (!color.equals(parallelCurveColor)) { parallelCurveColor = color; if ( ! Double.isNaN(parallelCurveOffset) ) fireDecorationChangeEvent(); } } /** * Returns the color used for drawing the second parallel curve. * @see #setParallelCurveColor2(Color) */ public Color getParallelCurveColor2() { return parallelCurveColor; } /** * Set the color used for the second parallel curve, if there is one; the default is a light red. * @param color The color to be used. If this is null, the default is used. * @see #setShowTwoParallelCurves(boolean) */ public void setParallelCurveColor2(Color color) { if (color == null) color = new Color(200,100,100); if (!color.equals(parallelCurveColor2)) { parallelCurveColor2 = color; if ( ! Double.isNaN(parallelCurveOffset) && showTwoParallelCurves) fireDecorationChangeEvent(); } } /** * Tells whether two parallel curves are drawn, or just one. * @see #setShowTwoParallelCurves(boolean) */ public boolean getShowTwoParallelCurves() { return showTwoParallelCurves; } /** * If set to true, two parallel curves are drawn whenever the parallelCurveOffset * property is set to a real number. The offset for the second curve is the * negative of the value of parallelCurveOffset. The default value is true. * @see #setParallelCurveOffset(double) */ public void setShowTwoParallelCurves(boolean show) { if (show != showTwoParallelCurves) { showTwoParallelCurves = show; if ( ! Double.isNaN(parallelCurveOffset)) fireDecorationChangeEvent(); } } /** * Tells whether unit normals are drawn, or normals of length equal to the radius of the osculatting circle. * @see #setUseUnitNormals(boolean) */ public boolean getUseUnitNormals() { return useUnitNormals; } /** * Tells the index of the point where an osculatting circle is drawn. * @see #setOsculatingCircleIndex(int) */ public int getOsculatingCircleIndex() { return osculatingCircleIndex; } /** * Set the index of the point on the curve where an osculating circle is to be shown. * If the index is less than zero, no circle is shown; this is the default. Otherwise, * the value is clamped to the range 0 to curve.getTResolution(), inclusive, and an * osculating circle appears at the specified point on the curve. When an osculating * circle is drawn, its radius is also drawn, and the evolute curve is also drawn. */ public void setOsculatingCircleIndex(int i) { setOsculatingCircleIndex(i,false); } /** * Set the index of the point on the curve where an osculating circle is to be shown. * If the index is less than zero, no circle is shown; this is the default. Otherwise, * the value is clamped to the range 0 to curve.getTResolution(), inclusive, and an * osculating circle appears at the specified point on the curve. When an osculating * circle is drawn, its radius is also drawn. If showEvoluteSoFar is true, then * the evolute curve is shown from the first point on the curve to the point specified for * the osculating circle. */ public void setOsculatingCircleIndex(int i, boolean showEvoluteSoFar) { if (i < 0) i = -1; if (i != osculatingCircleIndex || showEvoluteSoFar != showEvoluteWithOsculatingCircle) { this.osculatingCircleIndex = i; showEvoluteWithOsculatingCircle = showEvoluteSoFar; fireDecorationChangeEvent(); } } /** * Set whether unit normals are drawn. The default is false, meaning * that the length of a normal will be the radius of the osculating circle * (except that it will be limited to a large maximum length). */ public void setUseUnitNormals(boolean useUnitNormals) { if (useUnitNormals != this.useUnitNormals) { this.useUnitNormals = useUnitNormals; lines = (useUnitNormals? unitNormals : evoluteNormals); fireDecorationChangeEvent(); } } /** * Gets an array containing unit normals for all points on the curve. If this method is * called before the decoration has been drawn, the return value will be null. An elemeent * of the array can be null, if the curve or its derivative is undefined as the point. */ public Line2D[] getUnitNormals() { return unitNormals; } /** * Computes the normal vectors that are needed for drawing the array. */ public void computeDrawData(View view, boolean exhibitNeedsRedraw, Transform previousLimits, Transform newLimits) { if (!(exhibitNeedsRedraw || decorationNeedsRedraw)) return; if (curve == null && view != null) { Exhibit c = view.getExhibit(); if (c instanceof PlaneCurveParametric) // it better be! curve = (PlaneCurveParametric)c; } int points = curve.getTResolution(); unitNormals = new Line2D.Double[points+1]; evoluteNormals = new Line2D.Double[points+1]; maxNormalLength = newLimits.getPixelWidth() * 5000; for (int i = 0; i <= points; i++) { double t = curve.getT(i); double x = curve.xValue(t); double y = curve.yValue(t); double dx = curve.xDerivativeValue(t); double dy = curve.yDerivativeValue(t); double dx2 = curve.x2ndDerivativeValue(t); double dy2 = curve.y2ndDerivativeValue(t); double length = Math.sqrt(dx*dx + dy*dy); double unit_dx = dx/length; double unit_dy = dy/length; if (Double.isNaN(x) || Double.isInfinite(x) || Double.isNaN(y) || Double.isInfinite(y) || Double.isNaN(unit_dx) || Double.isInfinite(unit_dx) || Double.isNaN(unit_dy) || Double.isInfinite(unit_dy)) continue; unitNormals[i] = new Line2D.Double(x, y, x - unit_dy, y + unit_dx); if (Double.isNaN(dx2) || Double.isInfinite(dx2) || Double.isNaN(dy2) || Double.isInfinite(dy2)) continue; double curvature = (dx*dy2 - dy*dx2) / (length*length*length); double radius = 1/curvature; if (Math.abs(radius) > maxNormalLength) radius = (radius > 0)? maxNormalLength : -maxNormalLength; evoluteNormals[i] = new Line2D.Double(x, y, x - unit_dy*radius, y + unit_dx*radius); } lines = (useUnitNormals? unitNormals : evoluteNormals); decorationNeedsRedraw = false; } public void doDraw(Graphics2D g, View view, Transform limits) { Color saveColor = g.getColor(); double maxJump = Math.max(limits.getXmax() - limits.getXmin(), limits.getYmax() - limits.getYmin())/4; int pointsToDraw = pointCt; if (pointsToDraw > curve.getTResolution()) pointsToDraw = curve.getTResolution() + 1; if (pointsToDraw > 0) { g.setColor(normalColor); for (int i = 0; i < pointsToDraw; i++) if (lines[i] != null) g.draw(lines[i]); } int oci = osculatingCircleIndex; if (oci > curve.getTResolution()) oci = curve.getTResolution(); if (showEvolute || oci >= 0 ) { g.setColor(evoluteColor); double maxLength = 5000*limits.getPixelWidth(); double x = Double.NaN; double y = Double.NaN; int points = (oci >= 0)? oci + 1: evoluteNormals.length; for (int i = 0; i < points; i++) { double x1, y1, length = 0; if (evoluteNormals[i] == null) x1 = y1 = Double.NaN; else { x1 = evoluteNormals[i].getX2(); y1 = evoluteNormals[i].getY2(); length = Math.sqrt(Math.pow(x1-evoluteNormals[i].getX1(),2) + Math.pow(y1-evoluteNormals[i].getY1(),2)); } if ( ! (Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(x1) || Double.isNaN(y1)) && Math.sqrt((x-x1)*(x-x1) + (y-y1)*(y-y1)) < maxJump && length <= maxLength) g.draw(new Line2D.Double(x,y,x1,y1)); x = x1; y = y1; } } if (oci >= 0 && evoluteNormals[oci] != null) { g.setColor(osculatingCircleColor); Line2D.Double v = evoluteNormals[oci]; g.draw(v); // The radius of the circle. double x = v.getX1(); double y = v.getY1(); double dx = v.getX2() - v.getX1(); double dy = v.getY2() - v.getY1(); double radius = Math.sqrt(dx*dx + dy*dy); g.draw( new Ellipse2D.Double(x+dx-radius,y+dy-radius,2*radius,2*radius)); } if (! (Double.isNaN(parallelCurveOffset) || Double.isInfinite(parallelCurveOffset)) ) { g.setColor(parallelCurveColor); double x = Double.NaN; double y = Double.NaN; for (int i = 0; i < unitNormals.length; i++) { double x1, y1; if (unitNormals[i] == null) x1 = y1 = Double.NaN; else { Line2D.Double v = unitNormals[i]; x1 = v.getX1() + parallelCurveOffset*(v.getX2() - v.getX1()); y1 = v.getY1() + parallelCurveOffset*(v.getY2() - v.getY1()); } if ( ! (Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(x1) || Double.isNaN(y1)) ) { if (Math.abs(x-x1) + Math.abs(y-y1) <= maxJump) g.draw(new Line2D.Double(x,y,x1,y1)); } x = x1; y = y1; } if (showTwoParallelCurves) { g.setColor(parallelCurveColor2); x = Double.NaN; y = Double.NaN; for (int i = 0; i < unitNormals.length; i++) { double x1, y1; if (unitNormals[i] == null) x1 = y1 = Double.NaN; else { Line2D.Double v = unitNormals[i]; x1 = v.getX1() - parallelCurveOffset*(v.getX2() - v.getX1()); y1 = v.getY1() - parallelCurveOffset*(v.getY2() - v.getY1()); } if ( ! (Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(x1) || Double.isNaN(y1)) ) { if (Math.abs(x-x1) + Math.abs(y-y1) <= maxJump) g.draw(new Line2D.Double(x,y,x1,y1)); } x = x1; y = y1; } } } g.setColor(saveColor); } }