1717public class Shape {
1818 protected java .awt .Shape awtShape ;
1919
20+ /**
21+ * The outline flattened to points, and whether it came out convex - worked out
22+ * once and reused, so that {@link #intersects(Shape)} can take the cheap route
23+ * without re-walking the path every call. Both are derived from
24+ * {@link #awtShape} and must be dropped whenever it is replaced.
25+ */
26+ private double [] cachedPoints ;
27+ private boolean cachedConvex ;
28+ private Bounds cachedBounds ;
29+
30+ /**
31+ * Above this many corners the separating-axis test stops being cheaper than
32+ * the general {@link Area} one, so shapes that flatten to a lot of segments
33+ * (circles, ellipses) keep using the general path.
34+ */
35+ private static final int MAX_SAT_POINTS = 8 ;
36+
2037 protected Shape () {
2138 }
2239
2340 private Shape (java .awt .Shape awtShape ) {
2441 this .awtShape = awtShape ;
2542 }
2643
44+ /** Call after replacing {@link #awtShape} so the derived data is rebuilt. */
45+ protected void invalidateCache () {
46+ this .cachedPoints = null ;
47+ this .cachedBounds = null ;
48+ }
49+
2750 /**
2851 * Check if the shape contains a point defined by its x and y coordinates.
2952 *
@@ -44,11 +67,129 @@ public boolean contains(double x, double y) {
4467 * @return true if the shapes intersect, false otherwise
4568 */
4669 public boolean intersects (Shape other ) {
70+ // Sprite hitboxes are almost always convex quads, and building two Areas to
71+ // compare two quads costs more than the rest of a frame's collision work put
72+ // together. Separating axes give the same answer for the convex case.
73+ double [] a = this .points ();
74+ double [] b = other .points ();
75+ if (a != null && b != null && this .cachedConvex && other .cachedConvex ) {
76+ return !separated (a , b ) && !separated (b , a );
77+ }
78+
4779 Area a1 = new Area (awtShape );
4880 a1 .intersect (new Area (other .awtShape ));
4981 return !a1 .isEmpty ();
5082 }
5183
84+ /**
85+ * Checks whether some edge of {@code a} has all of {@code b} on its far side,
86+ * which for convex outlines means the two cannot be touching.
87+ */
88+ private static boolean separated (double [] a , double [] b ) {
89+ for (int i = 0 ; i < a .length ; i += 2 ) {
90+ int j = (i + 2 ) % a .length ;
91+ // Outward normal of the edge from point i to point j.
92+ double axisX = a [j + 1 ] - a [i + 1 ];
93+ double axisY = a [i ] - a [j ];
94+
95+ double aMin = Double .POSITIVE_INFINITY ;
96+ double aMax = Double .NEGATIVE_INFINITY ;
97+ for (int k = 0 ; k < a .length ; k += 2 ) {
98+ double p = a [k ] * axisX + a [k + 1 ] * axisY ;
99+ if (p < aMin )
100+ aMin = p ;
101+ if (p > aMax )
102+ aMax = p ;
103+ }
104+
105+ double bMin = Double .POSITIVE_INFINITY ;
106+ double bMax = Double .NEGATIVE_INFINITY ;
107+ for (int k = 0 ; k < b .length ; k += 2 ) {
108+ double p = b [k ] * axisX + b [k + 1 ] * axisY ;
109+ if (p < bMin )
110+ bMin = p ;
111+ if (p > bMax )
112+ bMax = p ;
113+ }
114+
115+ // Not "<", so that outlines meeting exactly along an edge count as apart,
116+ // the same way the Area path does - their overlap has no area.
117+ if (aMax <= bMin || bMax <= aMin ) {
118+ return true ;
119+ }
120+ }
121+ return false ;
122+ }
123+
124+ /**
125+ * The outline as flat {@code x, y, x, y, ...} pairs, or null if the shape is
126+ * not a single small convex ring that {@link #separated} can handle.
127+ */
128+ private double [] points () {
129+ if (this .cachedPoints != null ) {
130+ return this .cachedPoints ;
131+ }
132+ PathIterator it = this .awtShape .getPathIterator (null , 1 );
133+ double [] coord = new double [6 ];
134+ double [] buffer = new double [2 * MAX_SAT_POINTS ];
135+ int n = 0 ;
136+ boolean seenMoveTo = false ;
137+ while (!it .isDone ()) {
138+ int code = it .currentSegment (coord );
139+ if (code == PathIterator .SEG_CLOSE ) {
140+ it .next ();
141+ continue ;
142+ }
143+ if (code == PathIterator .SEG_MOVETO ) {
144+ if (seenMoveTo ) {
145+ return null ; // more than one ring
146+ }
147+ seenMoveTo = true ;
148+ } else if (code != PathIterator .SEG_LINETO ) {
149+ return null ; // a curve survived flattening
150+ }
151+ if (n + 2 > buffer .length ) {
152+ return null ; // too many corners to be worth the separating-axis test
153+ }
154+ buffer [n ++] = coord [0 ];
155+ buffer [n ++] = coord [1 ];
156+ it .next ();
157+ }
158+ if (n < 6 ) {
159+ return null ; // fewer than three corners is not an area
160+ }
161+ // A flattened ring often repeats its first point at the end; drop it so the
162+ // edge walk does not produce a zero-length axis.
163+ if (n >= 8 && buffer [0 ] == buffer [n - 2 ] && buffer [1 ] == buffer [n - 1 ]) {
164+ n -= 2 ;
165+ }
166+
167+ double [] points = java .util .Arrays .copyOf (buffer , n );
168+ this .cachedConvex = isConvex (points );
169+ this .cachedPoints = points ;
170+ return points ;
171+ }
172+
173+ /** True if every turn along the ring goes the same way. */
174+ private static boolean isConvex (double [] p ) {
175+ int sign = 0 ;
176+ for (int i = 0 ; i < p .length ; i += 2 ) {
177+ int j = (i + 2 ) % p .length ;
178+ int k = (i + 4 ) % p .length ;
179+ double cross = (p [j ] - p [i ]) * (p [k + 1 ] - p [j + 1 ]) - (p [j + 1 ] - p [i + 1 ]) * (p [k ] - p [j ]);
180+ if (cross == 0 ) {
181+ continue ;
182+ }
183+ int s = cross > 0 ? 1 : -1 ;
184+ if (sign == 0 ) {
185+ sign = s ;
186+ } else if (sign != s ) {
187+ return false ;
188+ }
189+ }
190+ return true ;
191+ }
192+
52193 /**
53194 * Check if the shape is empty.
54195 *
@@ -95,8 +236,11 @@ public Shape rotate(double theta, double anchorX, double anchorY) {
95236 * @return a Bounds object representing the bounding box of the shape
96237 */
97238 public Bounds getBounds () {
98- java .awt .geom .Rectangle2D bounds = awtShape .getBounds2D ();
99- return new Bounds (bounds .getX (), bounds .getY (), bounds .getWidth (), bounds .getHeight ());
239+ if (this .cachedBounds == null ) {
240+ java .awt .geom .Rectangle2D bounds = awtShape .getBounds2D ();
241+ this .cachedBounds = new Bounds (bounds .getX (), bounds .getY (), bounds .getWidth (), bounds .getHeight ());
242+ }
243+ return this .cachedBounds ;
100244 }
101245
102246 public void draw (PGraphics buffer ) {
0 commit comments