Path(DrawingPanel)   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
c 0
b 0
f 0
dl 0
loc 7
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" target="_blank">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
            System.err.println(e.getMessage());
70
            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...
71
        }
72
    }
73
74
    /**
75
     * <p>Construct a path with empty geometry and a winding rule</p>
76
     *
77
     * @param windingRule  The <a href="https://en.wikipedia.org/wiki/Nonzero-rule#/media/File:Even-odd_and_non-zero_winding_fill_rules.png">
78
     *                     winding rule</a> to determine how to fill the shape
79
     * @param drawingPanel on which to draw
80
     */
81
    public Path(int windingRule, DrawingPanel drawingPanel) {
82
        try {
83
            setShape(new Path2D.Double(windingRule));
84
            setDrawingPanel(drawingPanel);
85
        } catch (DrawableException e) {
86
            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...
87
        }
88
    }
89
90
    /**
91
     * <p>Construct a path with empty geometry, a winding rule and expected number of segments</p>
92
     *
93
     * <p>The path will expand to contain as many segments as are added to it, but setting the initial capacity to your best
94
     * guess gains some small amount of efficiency in reducing resizing operations.</p>
95
     *
96
     * @param windingRule     The <a href="https://en.wikipedia.org/wiki/Nonzero-rule#/media/File:Even-odd_and_non-zero_winding_fill_rules.png">
97
     *                        winding rule</a> to determine how to fill the shape
98
     * @param initialCapacity Anticipated number of segments
99
     * @param drawingPanel    on which to draw
100
     */
101
    public Path(int windingRule, int initialCapacity, DrawingPanel drawingPanel) {
102
        try {
103
            setShape(new Path2D.Double(windingRule, initialCapacity));
104
            setDrawingPanel(drawingPanel);
105
        } catch (DrawableException e) {
106
            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...
107
        }
108
    }
109
110
    /**
111
     * <p>Construct a path from {@link Shape} geometry and a transformation</p>
112
     *
113
     * @param shape          of underlying geometry
114
     * @param transformation to apply {@code shape} (i.e. scale, translation, rotation)
115
     * @param drawingPanel   on which to draw
116
     * @throws DrawableException If {@code shape} cannot be converted to a {@link Path2D} (a highly unlikely eventuality)
117
     */
118
    public Path(Shape shape, AffineTransform transformation, DrawingPanel drawingPanel) throws DrawableException {
119
        setShape(new Path2D.Double(shape, transformation));
120
        setDrawingPanel(drawingPanel);
121
    }
122
123
    /**
124
     * Underlying {@link Path2D} geometry
125
     *
126
     * @return Underlying {@link Path2D} geometry
127
     */
128
    protected Path2D getShapeAsPath() {
129
        return (Path2D) getShape();
130
    }
131
132
    @Override
133
    public void setShape(Shape shape) throws DrawableException {
134
        if (shape instanceof Path2D) {
135
            super.setShape(shape);
136
        } else {
137
            throw new DrawableException("Attempt to set Path's underlying shape to a non-Path2D instance");
138
        }
139
    }
140
141
    @Override
142
    public void setWidth(double width) {
143
        // translate to origin before scaling so that distance from origin is not _also_ scaled!
144
        double x = getX();
145
        transform(AffineTransform.getTranslateInstance(-x, 0));
146
        transform(AffineTransform.getScaleInstance(width / getWidth(), 1));
147
        transform(AffineTransform.getTranslateInstance(x, 0));
148
    }
149
150
    @Override
151
    public void setHeight(double height) {
152
        // translate to origin before scaling so that distance from origin is not _also_ scaled!
153
        double y = getY();
154
        transform(AffineTransform.getTranslateInstance(0, -y));
155
        transform(AffineTransform.getScaleInstance(1, height / getHeight()));
156
        transform(AffineTransform.getTranslateInstance(0, y));
157
    }
158
159
    /**
160
     * <p>Add a cubic curve segment to the path</p>
161
     *
162
     * <p><img src="doc-files/CubicCurve.png" alt="Cubic Curve diagram"></p>
163
     *
164
     * <p>The cubic Bézier curve starts at the current end point of the path and extends through two control points. For
165
     * more details on how cubic Bézier curves are computes, refer to {@link CubicCurve}.</p>
166
     *
167
     * @param ctrlX1 X-coordinate of first control point
168
     * @param ctrlY1 Y-coordinate of first control point
169
     * @param ctrlX2 X-coordinate of second control point
170
     * @param ctrlY2 Y-coordinate of second control point
171
     * @param x3     X-coordinate of end point
172
     * @param y3     Y-coordinate of end point
173
     */
174
    public void curveTo(double ctrlX1, double ctrlY1, double ctrlX2, double ctrlY2, double x3, double y3) {
175
        getShapeAsPath().curveTo(ctrlX1, ctrlY1, ctrlX2, ctrlY2, x3, y3);
176
    }
177
178
    /**
179
     * <p>Add a line segment to the path</p>
180
     *
181
     * <p><img src="doc-files/Line.png" alt="Line diagram"></p>
182
     *
183
     * <p>The line starts at the current end point of the path. For more details on drawing lines, refer to {@link Line}.</p>
184
     *
185
     * @param x coordinate of end point
186
     * @param y coordinate of end point
187
     */
188
    public void lineTo(double x, double y) {
189
        getShapeAsPath().lineTo(x, y);
190
    }
191
192
    @Override
193
    public void setLocation(double x, double y) {
194
        translate(x - getX(), y - getY());
195
    }
196
197
    /**
198
     * <p>Select a new starting point for subsequent path segments</p>
199
     *
200
     * <p>Path segments are defined relative to the end point of the previous segment. {@code moveTo()}
201
     * must be the first instruction to the path to set a starting point for following segments. This method can also
202
     * be used to define a discontinuous path.</p>
203
     *
204
     * @param x coordinate
205
     * @param y coordinate
206
     */
207
    public void moveTo(double x, double y) {
208
        getShapeAsPath().moveTo(x, y);
209
    }
210
211
212
    /**
213
     * <p>Add a quadratic Bézier curve segment to the path</p>
214
     *
215
     * <p><img src="doc-files/QuadCurve.png" alt="Quad Curve diagram"></p>
216
     *
217
     * <p>The quadratic Bézier curve segments starts at the end point of the previous path segment, through a control
218
     * point to the end point. For more information on computing quadratic Bézier curves, refer to {@link QuadCurve}.</p>
219
     *
220
     * @param ctrlX1 X-coordinate of control point
221
     * @param ctrlY1 Y-coordinate of control point
222
     * @param x2     X-coordinate of end point
223
     * @param y2     Y-coordinate of end point
224
     */
225
    public void quadTo(double ctrlX1, double ctrlY1, double x2, double y2) {
226
        getShapeAsPath().quadTo(ctrlX1, ctrlY1, x2, y2);
227
    }
228
229
    /**
230
     * <p>Close the path by drawing a straight line back to the starting point</p>
231
     */
232
    public void closePath() {
233
        getShapeAsPath().closePath();
234
    }
235
236
    /**
237
     * <p>Transform the path</p>
238
     *
239
     * <p>An "affine transformation" is one in which the spatial relationships of the points of the path are not changed
240
     * relative to each other &mdash; scale, translation, and rotation. Refer to {@link AffineTransform} for more
241
     * information.</p>
242
     *
243
     * @param transformation to be applied to the path
244
     */
245
    public void transform(AffineTransform transformation) {
246
        getShapeAsPath().transform(transformation);
247
    }
248
249
    /**
250
     * <p>Translate the shape from one location to another</p>
251
     *
252
     * <p><img src="doc-files/Path-translate.png" alt="Translation diagram"></p>
253
     *
254
     * <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>
255
     *
256
     * @param dx Change in X-coordinates
257
     * @param dy Change in Y-coordinates
258
     */
259
    @Override
260
    public void translate(double dx, double dy) {
261
        transform(AffineTransform.getTranslateInstance(dx, dy));
262
    }
263
264
    /**
265
     * <p>Rotate the shape around an anchor point</p>
266
     *
267
     * <p><img src="doc-files/Path-rotate.png" alt="Rotation diagram"></p>
268
     *
269
     * @param theta   Angle (in radians) to rotate the shape
270
     * @param anchorX X-coordinate of anchor point
271
     * @param anchorY Y-coordinate of anchor point
272
     */
273
    public void rotate(double theta, double anchorX, double anchorY) {
274
        transform(AffineTransform.getRotateInstance(theta, anchorX, anchorY));
275
    }
276
277
    /**
278
     * <p>Rescale the shape by a factor along the X and Y axes</p>
279
     *
280
     * <p><img src="doc-files/Path-scale.png" alt="Scaling diagram"></p>
281
     *
282
     * <p>Note that scaling a shape scales not only its width and height dimensions, but also its position relative to
283
     * the origin: that is, its X and Y-coordinates are also scaled. This means that a shape whose top-left bounding
284
     * box X and Y-coordinates are not at the origin will have its location changed by the scaling transformation.</p>
285
     *
286
     * @param scaleFactorX Factor by which to scale the shape in the X direction (as a percentage of the width)
287
     * @param scaleFactorY Factor by which to scale the shape in the Y direction (as a percentage of the height);
288
     */
289
    public void scale(double scaleFactorX, double scaleFactorY) {
290
        transform(AffineTransform.getScaleInstance(scaleFactorX, scaleFactorY));
291
    }
292
293
    /**
294
     * <p>Shear the shape by a factor along the X and Y axes</p>
295
     *
296
     * <p><img src="doc-files/Path-shear.png" alt="Shearing diagram"></p>
297
     *
298
     * <p>Note that shearing a shape shears not only the relative positions of the shape vertices to each other, but
299
     * also the position of those vertices relative to the origin. This means that a shape whose top-left bounding box
300
     * X and Y-coordinates are not at the origin will have its location changed by the shearing transformation.</p>
301
     *
302
     * @param shearFactorX Factor by which to shear the shape in the X direction (as a percentage of the width)
303
     * @param shearFactorY Factor by which to shear the shape in the Y direction (as a percentage of the height)
304
     */
305
    public void shear(double shearFactorX, double shearFactorY) {
306
        transform(AffineTransform.getShearInstance(shearFactorX, shearFactorY));
307
    }
308
}
309