View Javadoc
1   package org.newdawn.slick.geom;
2   
3   import javax.annotation.Nonnull;
4   import javax.annotation.Nullable;
5   import java.util.ArrayList;
6   import java.util.List;
7   
8   /**
9    * A set of utilities to play with geometry
10   * 
11   * @author kevin
12   */
13  class GeomUtil {
14      /** The tolerance for determining changes and steps */
15      private static final float EPSILON = 0.0001f;
16      /** The tolerance for determining direction change */
17      private static final float EDGE_SCALE = 1f;
18      /** The maximum number of points returned by an operation - prevents full lockups */
19      private static final int MAX_POINTS = 10000;
20      /** The listener to notify of operations */
21      private GeomUtilListener listener;
22  
23      /**
24       * Subtract one shape from another - note this is experimental and doesn't
25       * currently handle islands
26       *
27       * @param target The target to be subtracted from
28       * @param missing The shape to subtract
29       * @return The newly created shapes
30       */
31      @Nonnull
32      public Shape[] subtract(Shape target, Shape missing) {
33          target = target.transform(new Transform());
34          missing = missing.transform(new Transform());
35  
36          int count = 0;
37          for (int i=0;i<target.getPointCount();i++) {
38              if (missing.contains(target.getPoint(i)[0], target.getPoint(i)[1])) {
39                  count++;
40              }
41          }
42  
43          if (count == target.getPointCount()) {
44              return new Shape[0];
45          }
46  
47          if (!target.intersects(missing)) {
48              return new Shape[] {target};
49          }
50  
51          int found = 0;
52          for (int i=0;i<missing.getPointCount();i++) {
53              if (target.contains(missing.getPoint(i)[0], missing.getPoint(i)[1])) {
54                  if (!onPath(target, missing.getPoint(i)[0], missing.getPoint(i)[1])) {
55                      found++;
56                  }
57              }
58          }
59          for (int i=0;i<target.getPointCount();i++) {
60              if (missing.contains(target.getPoint(i)[0], target.getPoint(i)[1])) {
61                  if (!onPath(missing, target.getPoint(i)[0], target.getPoint(i)[1]))
62                  {
63                      found++;
64                  }
65              }
66          }
67  
68          if (found < 1) {
69              return new Shape[] {target};
70          }
71  
72          return combine(target, missing, true);
73      }
74  
75      /**
76       * Check if the given point is on the path
77       *
78       * @param path The path to check
79       * @param x The x coordinate of the point to check
80       * @param y The y coordiante of teh point to check
81       * @return True if the point is on the path
82       */
83      private boolean onPath(@Nonnull Shape path, float x, float y) {
84          for (int i=0;i<path.getPointCount()+1;i++) {
85              int n = rationalPoint(path, i+1);
86              Line line = getLine(path, rationalPoint(path, i), n);
87              if (line.distance(new Vector2f(x,y)) < EPSILON*100) {
88                  return true;
89              }
90          }
91  
92          return false;
93      }
94  
95      /**
96       * Set the listener to be notified of geometry based operations
97       *
98       * @param listener The listener to be notified of geometry based operations
99       */
100     public void setListener(GeomUtilListener listener) {
101         this.listener = listener;
102     }
103 
104     /**
105      * Join to shapes together. Note that the shapes must be touching
106      * for this method to work.
107      *
108      * @param target The target shape to union with
109      * @param other The additional shape to union
110      * @return The newly created shapes
111      */
112     @Nonnull
113     public Shape[] union(Shape target, Shape other) {
114         target = target.transform(new Transform());
115         other = other.transform(new Transform());
116 
117         if (!target.intersects(other)) {
118             return new Shape[] {target, other};
119         }
120 
121         // handle the case where intersects is true but really we're talking
122         // about edge points
123         boolean touches = false;
124         int buttCount = 0;
125         for (int i=0;i<target.getPointCount();i++) {
126             if (other.contains(target.getPoint(i)[0], target.getPoint(i)[1])) {
127                 if (!other.hasVertex(target.getPoint(i)[0], target.getPoint(i)[1])) {
128                     touches = true;
129                     break;
130                 }
131             }
132             if (other.hasVertex(target.getPoint(i)[0], target.getPoint(i)[1])) {
133                 buttCount++;
134             }
135         }
136         for (int i=0;i<other.getPointCount();i++) {
137             if (target.contains(other.getPoint(i)[0], other.getPoint(i)[1])) {
138                 if (!target.hasVertex(other.getPoint(i)[0], other.getPoint(i)[1])) {
139                     touches = true;
140                     break;
141                 }
142             }
143         }
144 
145         if ((!touches) && (buttCount < 2)) {
146             return new Shape[] {target, other};
147         }
148 
149         // so they are definitely touching, consider the union
150         return combine(target, other, false);
151     }
152 
153     /**
154      * Perform the combination
155      *
156      * @param target The target shape we're updating
157      * @param other The other shape in the operation
158      * @param subtract True if it's a subtract operation, otherwise it's union
159      * @return The set of shapes produced
160      */
161     @Nonnull
162     private Shape[] combine(@Nonnull Shape target, @Nonnull Shape other, boolean subtract) {
163         if (subtract) {
164             List<Shape> shapes = new ArrayList<>();
165             List<Vector2f> used = new ArrayList<>();
166 
167             // remove any points that are contianed in the shape we're removing, these
168             // are implicitly used
169             for (int i=0;i<target.getPointCount();i++) {
170                 float[] point = target.getPoint(i);
171                 if (other.contains(point[0], point[1])) {
172                     used.add(new Vector2f(point[0], point[1]));
173                     if (listener != null) {
174                         listener.pointExcluded(point[0], point[1]);
175                     }
176                 }
177             }
178 
179             for (int i=0;i<target.getPointCount();i++) {
180                 float[] point = target.getPoint(i);
181                 Vector2f pt = new Vector2f(point[0], point[1]);
182 
183                 if (!used.contains(pt)) {
184                     Shape result = combineSingle(target, other, true, i);
185                     shapes.add(result);
186                     for (int j=0;j<result.getPointCount();j++) {
187                         float[] kpoint = result.getPoint(j);
188                         Vector2f kpt = new Vector2f(kpoint[0], kpoint[1]);
189                         used.add(kpt);
190                     }
191                 }
192             }
193 
194             return (Shape[]) shapes.toArray();
195         } else {
196             for (int i=0;i<target.getPointCount();i++) {
197                 if (!other.contains(target.getPoint(i)[0], target.getPoint(i)[1])) {
198                     if (!other.hasVertex(target.getPoint(i)[0], target.getPoint(i)[1])) {
199                         Shape shape = combineSingle(target, other, false, i);
200                         return new Shape[] {shape};
201                     }
202                 }
203             }
204 
205             return new Shape[] {other};
206         }
207     }
208 
209     /**
210      * Combine two shapes
211      *
212      * @param target The target shape
213      * @param missing The second shape to apply
214      * @param subtract True if we should subtract missing from target, otherwise union
215      * @param start The point to start at
216      * @return The newly created shape
217      */
218     @Nonnull
219     private Shape combineSingle(Shape target, Shape missing, boolean subtract, int start) {
220         Shape current = target;
221         Shape other = missing;
222         int point = start;
223         int dir = 1;
224 
225         Polygon poly = new Polygon();
226         boolean first = true;
227 
228         int loop = 0;
229 
230         // while we've not reached the same point
231         float px = current.getPoint(point)[0];
232         float py = current.getPoint(point)[1];
233 
234         while (!poly.hasVertex(px, py) || (first) || (current != target)) {
235             first = false;
236             loop++;
237             if (loop > MAX_POINTS) {
238                 break;
239             }
240 
241             // add the current point to the result shape
242             poly.addPoint(px,py);
243             if (listener != null) {
244                 listener.pointUsed(px,py);
245             }
246 
247             // if the line between the current point and the next one intersect the
248             // other shape work out where on the other shape and start traversing it's
249             // path instead
250             Line line = getLine(current, px, py, rationalPoint(current, point+dir));
251             HitResult hit = intersect(other, line);
252 
253             if (hit != null) {
254                 Line hitLine = hit.line;
255                 Vector2f pt = hit.pt;
256                 px = pt.x;
257                 py = pt.y;
258 
259                 if (listener != null) {
260                     listener.pointIntersected(px,py);
261                 }
262 
263                 if (other.hasVertex(px, py)) {
264                     point = other.indexOf(pt.x,pt.y);
265                     dir = 1;
266                     px = pt.x;
267                     py = pt.y;
268 
269                     Shape temp = current;
270                     current = other;
271                     other = temp;
272                     continue;
273                 }
274 
275                 float dx = hitLine.getDX() / hitLine.length();
276                 float dy = hitLine.getDY() / hitLine.length();
277                 dx *= EDGE_SCALE;
278                 dy *= EDGE_SCALE;
279 
280                 if (current.contains(pt.x + dx, pt.y + dy)) {
281                     // the point is the next one, we need to take the first and traverse
282                     // the path backwards
283                     if (subtract) {
284                         if (current == missing) {
285                             point = hit.p2;
286                             dir = -1;
287                         } else {
288                             point = hit.p1;
289                             dir = 1;
290                         }
291                     } else {
292                         if (current == target) {
293                             point = hit.p2;
294                             dir = -1;
295                         } else {
296                             point = hit.p2;
297                             dir = -1;
298                         }
299                     }
300 
301                     // swap the shapes over, we'll traverse the other one
302                     Shape temp = current;
303                     current = other;
304                     other = temp;
305                 } else if (current.contains(pt.x - dx, pt.y - dy)) {
306                     if (subtract) {
307                         if (current == target) {
308                             point = hit.p2;
309                             dir = -1;
310                         } else {
311                             point = hit.p1;
312                             dir = 1;
313                         }
314                     } else {
315                         if (current == missing) {
316                             point = hit.p1;
317                             dir = 1;
318                         } else {
319                             point = hit.p1;
320                             dir = 1;
321                         }
322                     }
323 
324                     // swap the shapes over, we'll traverse the other one
325                     Shape temp = current;
326                     current = other;
327                     other = temp;
328                 } else {
329                     // give up
330                     if (subtract) {
331                         break;
332                     } else {
333                         point = hit.p1;
334                         dir = 1;
335                         Shape temp = current;
336                         current = other;
337                         other = temp;
338 
339                         point = rationalPoint(current, point+dir);
340                         px = current.getPoint(point)[0];
341                         py = current.getPoint(point)[1];
342                     }
343                 }
344             } else {
345                 // otherwise just move to the next point in the current shape
346                 point = rationalPoint(current, point+dir);
347                 px = current.getPoint(point)[0];
348                 py = current.getPoint(point)[1];
349             }
350         }
351 
352         poly.addPoint(px,py);
353         if (listener != null) {
354             listener.pointUsed(px,py);
355         }
356 
357         return poly;
358     }
359 
360     /**
361      * Intersect a line with a shape
362      *
363      * @param shape The shape to compare
364      * @param line The line to intersect against the shape
365      * @return The result describing the intersection or null if none
366      */
367     @Nullable
368     HitResult intersect(@Nonnull Shape shape, @Nonnull Line line) {
369         float distance = Float.MAX_VALUE;
370         HitResult hit = null;
371 
372         for (int i=0;i<shape.getPointCount();i++) {
373             int next = rationalPoint(shape, i+1);
374             Line local = getLine(shape, i, next);
375 
376             Vector2f pt = line.intersect(local, true);
377             if (pt != null) {
378                 float newDis = pt.distance(line.getStart());
379                 if ((newDis < distance) && (newDis > EPSILON)) {
380                     hit = new HitResult();
381                     hit.pt = pt;
382                     hit.line = local;
383                     hit.p1 = i;
384                     hit.p2 = next;
385                     distance = newDis;
386                 }
387             }
388         }
389 
390         return hit;
391     }
392 
393     /**
394      * Rationalise a point in terms of a given shape
395      *
396      * @param shape The shape
397      * @param p The index of the point
398      * @return The index that is rational for the shape
399      */
400     private static int rationalPoint(@Nonnull Shape shape, int p) {
401         while (p < 0) {
402             p += shape.getPointCount();
403         }
404         while (p >= shape.getPointCount()) {
405             p -=  shape.getPointCount();
406         }
407 
408         return p;
409     }
410 
411     /**
412      * Get a line between two points in a shape
413      *
414      * @param shape The shape
415      * @param s The index of the start point
416      * @param e The index of the end point
417      * @return The line between the two points
418      */
419     @Nonnull
420     Line getLine(@Nonnull Shape shape, int s, int e) {
421         float[] start = shape.getPoint(s);
422         float[] end = shape.getPoint(e);
423 
424         Line line = new Line(start[0],start[1],end[0],end[1]);
425         return line;
426     }
427 
428     /**
429      * Get a line between two points in a shape
430      *
431      * @param shape The shape
432      * @param sx The x coordinate of the start point
433      * @param sy The y coordinate of the start point
434      * @param e The index of the end point
435      * @return The line between the two points
436      */
437     @Nonnull
438     Line getLine(@Nonnull Shape shape, float sx, float sy, int e) {
439         float[] end = shape.getPoint(e);
440 
441         Line line = new Line(sx,sy,end[0],end[1]);
442         return line;
443     }
444 
445     /**
446      * A lightweigtht description of a intersection between a shape and
447      * line.
448      */
449     public class HitResult {
450         /** The line on the target shape that intersected */
451         public Line line;
452         /** The index of the first point on the target shape that forms the line */
453         public int p1;
454         /** The index of the second point on the target shape that forms the line */
455         public int p2;
456         /** The position of the intersection */
457         @Nullable
458         public Vector2f pt;
459     }
460 }