View Javadoc
1   package org.opentrafficsim.base.geometry;
2   
3   import java.util.ArrayList;
4   import java.util.Collections;
5   import java.util.List;
6   
7   import org.djutils.draw.bounds.Bounds2d;
8   import org.djutils.draw.line.Polygon2d;
9   import org.djutils.draw.point.Point2d;
10  import org.djutils.exceptions.Throw;
11  
12  /**
13   * Shape defined by a rounded rectangle.
14   * <p>
15   * Copyright (c) 2024-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
16   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
17   * </p>
18   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
19   */
20  public abstract class RoundedRectangleShape implements OtsShape
21  {
22  
23      /** Half length along y dimension. */
24      private final double dx;
25  
26      /** Half length along y dimension. */
27      private final double dy;
28  
29      /** Rounding radius. */
30      private final double r;
31  
32      /** Number of line segments in polygon representation. */
33      private final int polygonSegments;
34  
35      /** Polygon representation. */
36      private Polygon2d polygon;
37  
38      /** Bounds. */
39      private Bounds2d bounds;
40  
41      /**
42       * Constructor.
43       * @param dx complete length along x dimension.
44       * @param dy complete length along y dimension.
45       * @param r radius of rounding, must be positive.
46       * @throws IllegalArgumentException when r is negative, or so large no net shape remains
47       */
48      public RoundedRectangleShape(final double dx, final double dy, final double r)
49      {
50          this(dx, dy, r, DEFAULT_POLYGON_SEGMENTS);
51      }
52  
53      /**
54       * Constructor.
55       * @param dx complete length along x dimension.
56       * @param dy complete length along y dimension.
57       * @param r radius of rounding, must be positive.
58       * @param polygonSegments number of segments in polygon representation.
59       * @throws IllegalArgumentException when r is negative, or so large no net shape remains
60       */
61      public RoundedRectangleShape(final double dx, final double dy, final double r, final int polygonSegments)
62      {
63          /*-
64           * Equation derived from r^2 = (r-dx)^2 + (r-dy^2)  [note: dx and dy here as half of input values, i.e. this.dx/this.dy]
65           *
66           *                dx
67           *    ___       ______
68           * ^ |   ''--_ |      | dy
69           * | |---------o------
70           * | | r-dx  / |'.
71           * r |      /  |  '.
72           * | |   r/    |    \
73           * | |   /     |     \
74           * | | /   r-dy|      |
75           * v |/________|______|
76           *    <-------r------>
77           */
78          this.dx = Math.abs(dx) / 2.0;
79          this.dy = Math.abs(dy) / 2.0;
80          Throw.when(r >= this.dx + this.dy + Math.sqrt(2.0 * this.dx * this.dy), IllegalArgumentException.class,
81                  "Radius makes rounded rectangle non-existent.");
82          Throw.when(r < 0.0, IllegalArgumentException.class, "Radius must be positive.");
83          this.r = r;
84          double maxX = this.dx - signedDistance(new Point2d(this.dx, 0.0));
85          double maxY = this.dy - signedDistance(new Point2d(0.0, this.dy));
86          this.polygonSegments = polygonSegments;
87          this.bounds = new Bounds2d(-maxX, maxX, -maxY, maxY);
88      }
89  
90      @Override
91      public Bounds2d getRelativeBounds()
92      {
93          return this.bounds;
94      }
95  
96      /**
97       * {@inheritDoc}
98       * @see <a href="https://iquilezles.org/articles/distfunctions/">Signed distance functions by Inigo Quilez</a>
99       */
100     @Override
101     public double signedDistance(final Point2d point)
102     {
103         double qx = Math.abs(point.x) - this.dx + this.r;
104         double qy = Math.abs(point.y) - this.dy + this.r;
105         return Math.hypot(Math.max(qx, 0.0), Math.max(qy, 0.0)) + Math.min(Math.max(qx, qy), 0.0) - this.r;
106     }
107 
108     @Override
109     public Polygon2d getRelativeContour()
110     {
111         if (this.polygon == null)
112         {
113             // calculate for top right quadrant only, others are negative or reversed copies
114             int n = this.polygonSegments / 4;
115             List<Point2d> pq = new ArrayList<>();
116             for (int i = 0; i <= n; i++)
117             {
118                 double ang = (0.5 * Math.PI * i) / n;
119                 double x = this.dx + Math.cos(ang) * this.r - this.r;
120                 double y = this.dy + Math.sin(ang) * this.r - this.r;
121                 if (x >= 0.0 && y >= 0.0) // else, radius larger than at least one of this.dx and this.dy, i.e. no full quarters
122                 {
123                     pq.add(new Point2d(x, y));
124                 }
125             }
126 
127             List<Point2d> pqReversed = new ArrayList<>(pq);
128             Collections.reverse(pqReversed);
129             List<Point2d> points = new ArrayList<>(pq); // top right quadrant (y = up)
130             pqReversed.forEach((p) -> points.add(new Point2d(-p.x, p.y))); // top left quadrant
131             pq.forEach((p) -> points.add(p.neg())); // bottom left quadrant
132             pqReversed.forEach((p) -> points.add(new Point2d(p.x, -p.y))); // bottom right quadrant
133             this.polygon = new Polygon2d(0.0, points);
134         }
135         return this.polygon;
136     }
137 
138     @Override
139     public String toString()
140     {
141         return "RoundedRectangleShape [dx=" + this.dx + ", dy=" + this.dy + ", r=" + this.r + "]";
142     }
143 
144 }