Passed
Push — master ( 0f24f3...f1f647 )
by Seth
03:04
created

rotate(double,double,double)   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
dl 0
loc 2
rs 10
1
package org.gannacademy.cdf.graphics.geom;
2
3
import org.gannacademy.cdf.graphics.Drawable;
4
import org.gannacademy.cdf.graphics.DrawableException;
5
import org.gannacademy.cdf.graphics.ui.DrawingPanel;
6
7
import java.awt.*;
8
import java.awt.geom.AffineTransform;
9
import java.awt.geom.Path2D;
10
11
/**
12
 * <p>Draw an arbitrary path</p>
13
 *
14
 * <p><img src="doc-files/Path.png" alt="Path diagram"></p>
15
 *
16
 * <p>Paths are constructed from an arbitrary number of segments. Each segment is a {@link Line}, {@link QuadCurve}, or
17
 * {@link CubicCurve} drawn between the end point of the prior segment and new point. For example, in the diagram above,
18
 * the path is made of three segments:</p>
19
 *
20
 * <ol>
21
 * <li>A line from Point 1 to Point 2</li>
22
 * <li>A cubic curve from Point 2 to Point 3</li>
23
 * <li>A quadratic curve from Point 3 to Point 4</li>
24
 * </ol>
25
 *
26
 * <p>The path above could be drawn with the following sequence of instructions:</p>
27
 *
28
 * <pre>
29
 *   Path p = new Path();
30
 *   p.moveTo(20, 16); // Point 1
31
 *   p.lineTo(40, 140); // Point 2
32
 *   p.curveTo(
33
 *       100, 140, // first Bézier control point
34
 *       120, 127, // second Bézier control point
35
 *       120, 78 // Point 3
36
 *   );
37
 *   p.quadTo(
38
 *       120, 16; // quadratic control point
39
 *       220, 78 // Point 4
40
 *   );
41
 * </pre>
42
 *
43
 * <p>All paths start with empty geometry and are then built segment by segment using {@link #moveTo(double, double)},
44
 * {@link #lineTo(double, double)}, {@link #quadTo(double, double, double, double)}, and
45
 * {@link #curveTo(double, double, double, double, double, double)} methods to extend the existing geometry.</p>
46
 *
47
 * <p>Note that the first segment added to the path must be a {@link #moveTo(double, double)} instruction, to locate
48
 * the first point in the path. Additional {@link #moveTo(double, double)} calls may be made as the path is defined,
49
 * creating a discontinuous path.</p>
50
 *
51
 * <p>Paths are particularly complex (and therefore flexible and powerful!). As the underlying geometry of this object
52
 * is stored as a {@link Path2D}, it is worth perusing that documentation for information on details like approaches to
53
 * filling, stroking, or transforming paths. More detailed explanations of how the Bézier curve segments are computed
54
 * can be found in {@link QuadCurve} and {@link CubicCurve}.</p>
55
 *
56
 * @author <a href="https://github.com/gann-cdf/graphics/issues">Seth Battis</a>
57
 */
58
public class Path extends Drawable {
59
    /**
60
     * <p>Construct a path with empty geometry</p>
61
     *
62
     * @param drawingPanel on which to draw
63
     */
64
    public Path(DrawingPanel drawingPanel) {
65
        try {
66
            setShape(new Path2D.Double());
67
            setDrawingPanel(drawingPanel);
68
        } catch (DrawableException e) {
69
            e.printStackTrace();
0 ignored issues
show
Best Practice introduced by
Throwable.printStackTrace writes to the console which might not be available at runtime. Using a logger is preferred.
Loading history...
70
        }
71
    }
72
73
    /**
74
     * <p>Construct a path with empty geometry and a winding rule</p>
75
     *
76
     * @param windingRule  The <a href="https://en.wikipedia.org/wiki/Nonzero-rule#/media/File:Even-odd_and_non-zero_winding_fill_rules.png">
77
     *                     winding rule</a> to determine how to fill the shape
78
     * @param drawingPanel on which to draw
79
     */
80
    public Path(int windingRule, DrawingPanel drawingPanel) {
81
        try {
82
            setShape(new Path2D.Double(windingRule));
83
            setDrawingPanel(drawingPanel);
84
        } catch (DrawableException e) {
85
            e.printStackTrace();
0 ignored issues
show
Best Practice introduced by
Throwable.printStackTrace writes to the console which might not be available at runtime. Using a logger is preferred.
Loading history...
86
        }
87
    }
88
89
    /**
90
     * <p>Construct a path with empty geometry, a winding rule and expected number of segments</p>
91
     *
92
     * <p>The path will expand to contain as many segments as are added to it, but setting the initial capacity to your best
93
     * guess gains some small amount of efficiency in reducing resizing operations.</p>
94
     *
95
     * @param windingRule     The <a href="https://en.wikipedia.org/wiki/Nonzero-rule#/media/File:Even-odd_and_non-zero_winding_fill_rules.png">
96
     *                        winding rule</a> to determine how to fill the shape
97
     * @param initialCapacity Anticipated number of segments
98
     * @param drawingPanel    on which to draw
99
     */
100
    public Path(int windingRule, int initialCapacity, DrawingPanel drawingPanel) {
101
        try {
102
            setShape(new Path2D.Double(windingRule, initialCapacity));
103
            setDrawingPanel(drawingPanel);
104
        } catch (DrawableException e) {
105
            e.printStackTrace();
0 ignored issues
show
Best Practice introduced by
Throwable.printStackTrace writes to the console which might not be available at runtime. Using a logger is preferred.
Loading history...
106
        }
107
    }
108
109
    /**
110
     * <p>Construct a path from {@link Shape} geometry and a transformation</p>
111
     *
112
     * @param shape          of underlying geometry
113
     * @param transformation to apply {@code shape} (i.e. scale, translation, rotation)
114
     * @param drawingPanel   on which to draw
115
     * @throws DrawableException If {@code shape} cannot be converted to a {@link Path2D} (a highly unlikely eventuality)
116
     */
117
    public Path(Shape shape, AffineTransform transformation, DrawingPanel drawingPanel) throws DrawableException {
118
        setShape(new Path2D.Double(shape, transformation));
119
        setDrawingPanel(drawingPanel);
120
    }
121
122
    /**
123
     * Underlying {@link Path2D} geometry
124
     *
125
     * @return Underlying {@link Path2D} geometry
126
     */
127
    protected Path2D getShapeAsPath() {
128
        return (Path2D) getShape();
129
    }
130
131
    @Override
132
    public void setShape(Shape shape) throws DrawableException {
133
        if (shape instanceof Path2D) {
134
            super.setShape(shape);
135
        } else {
136
            throw new DrawableException("Attempt to set Path's underlying shape to a non-Path2D instance");
137
        }
138
    }
139
140
    @Override
141
    public void setWidth(double width) {
142
        // translate to origin before scaling so that distance from origin is not _also_ scaled!
143
        double x = getX();
144
        transform(AffineTransform.getTranslateInstance(-x, 0));
145
        transform(AffineTransform.getScaleInstance(width / getWidth(), 1));
146
        transform(AffineTransform.getTranslateInstance(x, 0));
147
    }
148
149
    @Override
150
    public void setHeight(double height) {
151
        // translate to origin before scaling so that distance from origin is not _also_ scaled!
152
        double y = getY();
153
        transform(AffineTransform.getTranslateInstance(0, -y));
154
        transform(AffineTransform.getScaleInstance(1, height / getHeight()));
155
        transform(AffineTransform.getTranslateInstance(0, y));
156
    }
157
158
    /**
159
     * <p>Add a cubic curve segment to the path</p>
160
     *
161
     * <p><img src="doc-files/CubicCurve.png" alt="Cubic Curve diagram"></p>
162
     *
163
     * <p>The cubic Bézier curve starts at the current end point of the path and extends through two control points. For
164
     * more details on how cubic Bézier curves are computes, refer to {@link CubicCurve}.</p>
165
     *
166
     * @param ctrlX1 X-coordinate of first control point
167
     * @param ctrlY1 Y-coordinate of first control point
168
     * @param ctrlX2 X-coordinate of second control point
169
     * @param ctrlY2 Y-coordinate of second control point
170
     * @param x3     X-coordinate of end point
171
     * @param y3     Y-coordinate of end point
172
     */
173
    public void curveTo(double ctrlX1, double ctrlY1, double ctrlX2, double ctrlY2, double x3, double y3) {
174
        getShapeAsPath().curveTo(ctrlX1, ctrlY1, ctrlX2, ctrlY2, x3, y3);
175
    }
176
177
    /**
178
     * <p>Add a line segment to the path</p>
179
     *
180
     * <p><img src="doc-files/Line.png" alt="Line diagram"></p>
181
     *
182
     * <p>The line starts at the current end point of the path. For more details on drawing lines, refer to {@link Line}.</p>
183
     *
184
     * @param x coordinate of end point
185
     * @param y coordinate of end point
186
     */
187
    public void lineTo(double x, double y) {
188
        getShapeAsPath().lineTo(x, y);
189
    }
190
191
    @Override
192
    public void setLocation(double x, double y) {
193
        translate(x - getX(), y - getY());
194
    }
195
196
    /**
197
     * <p>Select a new starting point for subsequent path segments</p>
198
     *
199
     * <p>Path segments are defined relative to the end point of the previous segment. {@code moveTo()}
200
     * must be the first instruction to the path to set a starting point for following segments. This method can also
201
     * be used to define a discontinuous path.</p>
202
     *
203
     * @param x coordinate
204
     * @param y coordinate
205
     */
206
    public void moveTo(double x, double y) {
207
        getShapeAsPath().moveTo(x, y);
208
    }
209
210
211
    /**
212
     * <p>Add a quadratic Bézier curve segment to the path</p>
213
     *
214
     * <p><img src="doc-files/QuadCurve.png" alt="Quad Curve diagram"></p>
215
     *
216
     * <p>The quadratic Bézier curve segments starts at the end point of the previous path segment, through a control
217
     * point to the end point. For more information on computing quadratic Bézier curves, refer to {@link QuadCurve}.</p>
218
     *
219
     * @param ctrlX1 X-coordinate of control point
220
     * @param ctrlY1 Y-coordinate of control point
221
     * @param x2     X-coordinate of end point
222
     * @param y2     Y-coordinate of end point
223
     */
224
    public void quadTo(double ctrlX1, double ctrlY1, double x2, double y2) {
225
        getShapeAsPath().quadTo(ctrlX1, ctrlY1, x2, y2);
226
    }
227
228
    /**
229
     * <p>Close the path by drawing a straight line back to the starting point</p>
230
     */
231
    public void closePath() {
232
        getShapeAsPath().closePath();
233
    }
234
235
    /**
236
     * <p>Transform the path</p>
237
     *
238
     * <p>An "affine transformation" is one in which the spatial relationships of the points of the path are not changed
239
     * relative to each other &mdash; scale, translation, and rotation. Refer to {@link AffineTransform} for more
240
     * information.</p>
241
     *
242
     * @param transformation to be applied to the path
243
     */
244
    public void transform(AffineTransform transformation) {
245
        getShapeAsPath().transform(transformation);
246
    }
247
248
    /**
249
     * <p>Translate the shape from one location to another</p>
250
     *
251
     * <p><img src="doc-files/Path-translate.png" alt="Translation diagram"></p>
252
     *
253
     * <p>Note that {@code dx} and {@code dy} are the change in in X- and Y- coordinates, and are therefore relative to the current position of the shape, and not an absolute location. (To move a shape to an absolute location, use the {@link #setLocation(double x, double y)} method.)</p>
254
     *
255
     * @param dx Change in X-coordinates
256
     * @param dy Change in Y-coordinates
257
     */
258
    @Override
259
    public void translate(double dx, double dy) {
260
        transform(AffineTransform.getTranslateInstance(dx, dy));
261
    }
262
263
    /**
264
     * <p>Rotate the shape around an anchor point</p>
265
     *
266
     * <p><img src="doc-files/Path-rotate.png" alt="Rotation diagram"></p>
267
     *
268
     * @param theta   Angle (in radians) to rotate the shape
269
     * @param anchorX X-coordinate of anchor point
270
     * @param anchorY Y-coordinate of anchor point
271
     */
272
    public void rotate(double theta, double anchorX, double anchorY) {
273
        transform(AffineTransform.getRotateInstance(theta, anchorX, anchorY));
274
    }
275
276
    /**
277
     * <p>Rescale the shape by a factor along the X and Y axes</p>
278
     *
279
     * <p><img src="doc-files/Path-scale.png" alt="Scaling diagram"></p>
280
     *
281
     * <p>Note that scaling a shape scales not only its width and height dimensions, but also its position relative to
282
     * the origin: that is, its X and Y-coordinates are also scaled. This means that a shape whose top-left bounding
283
     * box X and Y-coordinates are not at the origin will have its location changed by the scaling transformation.</p>
284
     *
285
     * @param scaleFactorX Factor by which to scale the shape in the X direction (as a percentage of the width)
286
     * @param scaleFactorY Factor by which to scale the shape in the Y direction (as a percentage of the height);
287
     */
288
    public void scale(double scaleFactorX, double scaleFactorY) {
289
        transform(AffineTransform.getScaleInstance(scaleFactorX, scaleFactorY));
290
    }
291
292
    /**
293
     * <p>Shear the shape by a factor along the X and Y axes</p>
294
     *
295
     * <p><img src="doc-files/Path-shear.png" alt="Shearing diagram"></p>
296
     *
297
     * <p>Note that shearing a shape shears not only the relative positions of the shape vertices to each other, but
298
     * also the position of those vertices relative to the origin. This means that a shape whose top-left bounding box
299
     * X and Y-coordinates are not at the origin will have its location changed by the shearing transformation.</p>
300
     *
301
     * @param shearFactorX Factor by which to shear the shape in the X direction (as a percentage of the width)
302
     * @param shearFactorY Factor by which to shear the shape in the Y direction (as a percentage of the height)
303
     */
304
    public void shear(double shearFactorX, double shearFactorY) {
305
        transform(AffineTransform.getShearInstance(shearFactorX, shearFactorY));
306
    }
307
}
308