AR Design
UBC EML collab with UBC SALA - visualizing IoT data in AR
InscribedRectangle.cs
Go to the documentation of this file.
1 // Copyright (c) Microsoft Corporation. All rights reserved.
2 // Licensed under the MIT License. See LICENSE in the project root for license information.
3 
4 using System;
5 using System.Collections.Generic;
6 using UnityEngine;
7 
8 namespace HoloToolkit.Unity.Boundary
9 {
10  public class InscribedRectangle
11  {
12  // Total number of starting points randomly generated within the boundary
13  private const int randomPointCount = 30;
14 
15  // A value that is larger than the widest possible room. We use this
16  // to create line segments that are "guaranteed" to hit a piece of the
17  // room boundary
18  private const float largeValue = 10000;
19  private const float smallValue = -largeValue;
20 
21  // The total amount of height we want to gain with each binary search
22  // change before we decide that it's good enough, in meters
23  private const float minimumHeightGain = 0.01f;
24 
25  private float DegreesToRadians(double deg) { return (float)(Math.PI / 180 * deg); }
26 
27  // Parameters for the inscribed rectangle
28  private Vector2 center = new Vector2(float.NegativeInfinity, float.NegativeInfinity);
29  private float angle;
30  private float width;
31  private float height;
32 
36  public InscribedRectangle(IList<Vector3> points) : this(points, (int)DateTime.UtcNow.Ticks) { }
37 
41  public InscribedRectangle(IList<Vector3> points, int randomSeed) : this(EdgeHelpers.ConvertVector3ListToEdgeArray(points), randomSeed) { }
42 
46  public InscribedRectangle(Edge[] edges) : this(edges, (int)DateTime.UtcNow.Ticks) { }
47 
51  public InscribedRectangle(Edge[] edges, int randomSeed)
52  {
53  FindInscribedRectangle(edges, randomSeed, out center, out angle, out width, out height);
54  }
55 
59  public bool IsRectangleValid
60  {
61  get { return EdgeHelpers.IsValidPoint(center); }
62  }
63 
68  public void GetRectangleParams(out Vector2 centerOut, out float angleOut, out float widthOut, out float heightOut)
69  {
70  if (!IsRectangleValid)
71  {
72  throw new InvalidOperationException();
73  }
74 
75  centerOut = center;
76  angleOut = angle;
77  widthOut = width;
78  heightOut = height;
79  }
80 
84  public Vector2[] GetRectanglePoints()
85  {
86  if (!IsRectangleValid)
87  {
88  throw new InvalidOperationException();
89  }
90 
91  var points = new Vector2[4];
92 
93  float x = width / 2.0f;
94  float y = height / 2.0f;
95  points = new Vector2[]
96  {
97  new Vector2(x, y),
98  new Vector2(x, -y),
99  new Vector2(-x, -y),
100  new Vector2(-x, y)
101  };
102 
103  for (int i = 0; i < points.Length; ++i)
104  {
105  points[i] = RotatePoint(Vector2.zero, DegreesToRadians(angle), points[i]);
106  points[i] += center;
107  }
108 
109  return points;
110  }
111 
115  public bool IsPointInRectangleBounds(Vector2 point)
116  {
117  if (!IsRectangleValid)
118  {
119  throw new InvalidOperationException();
120  }
121 
122  point -= center;
123  point = RotatePoint(Vector2.zero, DegreesToRadians(-angle), point);
124  return (Math.Abs(point.x) <= (width / 2.0f)) && (Math.Abs(point.y) <= height / 2.0f);
125  }
126 
130  private Vector2 RotatePoint(Vector2 origin, float angleRad, Vector2 point)
131  {
132  Vector2 retval = point;
133  if (0.0f == angleRad)
134  {
135  return retval;
136  }
137 
138  // Translate to origin of rotation
139  retval.x -= origin.x;
140  retval.y -= origin.y;
141 
142  // Rotate the point
143  float s = (float)Math.Sin(angleRad);
144  float c = (float)Math.Cos(angleRad);
145  float rotatedX = retval.x * c - retval.y * s;
146  float rotatedY = retval.x * s + retval.y * c;
147 
148  // Translate back and return
149  retval.x = rotatedX + origin.x;
150  retval.y = rotatedY + origin.y;
151  return retval;
152  }
153 
159  private bool FindSurroundingCollisionPoints(
160  Edge[] edges,
161  Vector2 point, float angleRad, out Vector2 topCollisionPoint,
162  out Vector2 bottomCollisionPoint, out Vector2 leftCollisionPoint,
163  out Vector2 rightCollisionPoint)
164  {
165  topCollisionPoint = EdgeHelpers.InvalidPoint;
166  bottomCollisionPoint = EdgeHelpers.InvalidPoint;
167  leftCollisionPoint = EdgeHelpers.InvalidPoint;
168  rightCollisionPoint = EdgeHelpers.InvalidPoint;
169 
170  bool isInside = EdgeHelpers.IsInside(edges, point);
171  if (!isInside)
172  {
173  return false;
174  }
175 
176  // Find the top and bottom collision points by creating a large line segment that goes through the point to MAX and MIN values on Y
177  var topEndpoint = new Vector2(point.x, largeValue);
178  var bottomEndpoint = new Vector2(point.x, smallValue);
179  topEndpoint = RotatePoint(point, angleRad, topEndpoint);
180  bottomEndpoint = RotatePoint(point, angleRad, bottomEndpoint);
181  var verticalLine = new Edge(topEndpoint, bottomEndpoint);
182  // Find the left and right collision points by creating a large line segment that goes through the point to MAX and Min values on X
183  var rightEndpoint = new Vector2(largeValue, point.y);
184  var leftEndpoint = new Vector2(smallValue, point.y);
185  rightEndpoint = RotatePoint(point, angleRad, rightEndpoint);
186  leftEndpoint = RotatePoint(point, angleRad, leftEndpoint);
187  var horizontalLine = new Edge(rightEndpoint, leftEndpoint);
188 
189  // Loop the edges and find the nearest intersection point
190  foreach (var edge in edges)
191  {
192  Vector2 verticalIntersection = EdgeHelpers.GetIntersection(edge, verticalLine);
193  if (EdgeHelpers.IsValidPoint(verticalIntersection))
194  {
195  // Is this intersection above or below the point?
196  bool isAbove = RotatePoint(point, -angleRad, verticalIntersection).y > point.y;
197  if (isAbove)
198  {
199  // If this collision point is closer than the previous one
200  if (!EdgeHelpers.IsValidPoint(topCollisionPoint) ||
201  Vector2.SqrMagnitude(point - verticalIntersection) < Vector2.SqrMagnitude(point - topCollisionPoint))
202  {
203  topCollisionPoint = verticalIntersection;
204  }
205  }
206  else
207  {
208  if (!EdgeHelpers.IsValidPoint(bottomCollisionPoint) ||
209  Vector2.SqrMagnitude(point - verticalIntersection) < Vector2.SqrMagnitude(point - bottomCollisionPoint))
210  {
211  bottomCollisionPoint = verticalIntersection;
212  }
213  }
214  } // If vertical intersection found
215 
216  Vector2 horizontalIntersection = EdgeHelpers.GetIntersection(edge, horizontalLine);
217  if (EdgeHelpers.IsValidPoint(horizontalIntersection))
218  {
219  // Is this intersection left or right of the point?
220  bool isLeft = RotatePoint(point, -angleRad, horizontalIntersection).x < point.x;
221  if (isLeft)
222  {
223  // Is it closer than the previous intersection?
224  if (!EdgeHelpers.IsValidPoint(leftCollisionPoint) ||
225  Vector2.SqrMagnitude(point - horizontalIntersection) < Vector2.SqrMagnitude(point - leftCollisionPoint))
226  {
227  leftCollisionPoint = horizontalIntersection;
228  }
229  }
230  else
231  {
232  // Is it closer than the previous intersection?
233  if (!EdgeHelpers.IsValidPoint(rightCollisionPoint) ||
234  Vector2.SqrMagnitude(point - horizontalIntersection) < Vector2.SqrMagnitude(point - rightCollisionPoint))
235  {
236  rightCollisionPoint = horizontalIntersection;
237  }
238  }
239  }
240  }
241 
242  // Assert that any point inside should have intersection points on all sides with the polygon
243  if (!EdgeHelpers.IsValidPoint(topCollisionPoint) || !EdgeHelpers.IsValidPoint(bottomCollisionPoint) ||
244  !EdgeHelpers.IsValidPoint(leftCollisionPoint) || !EdgeHelpers.IsValidPoint(rightCollisionPoint))
245  {
246  return false;
247  }
248  return true;
249  }
250 
258  private bool TryFitMaximumRectangleAtAngle(
259  Edge[] edges,
260  Vector2 center, float angleRad, float minArea,
261  out float aspectRatio, out float width, out float height)
262  {
263  width = 0;
264  height = 0;
265  aspectRatio = 0;
266 
267  float[] aspectRatios = {1, 1.5f, 2, 2.5f, 3, 3.5f, 4, 4.5f, 5, 5.5f, 6, 6.5f, 7, 7.5f,
268  8, 8.5f, 9, 9.5f, 10, 10.5f, 11, 11.5f, 12, 12.5f, 13, 13.5f, 14, 14.5f, 15};
269 
270  // Start by calculating max width and height by ray-casting a cross from the point at the given angle
271  // and taking the shortest leg of each ray. Width is the longest.
272  Vector2 topCollisionPoint;
273  Vector2 bottomCollisionPoint;
274  Vector2 leftCollisionPoint;
275  Vector2 rightCollisionPoint;
276  if (!FindSurroundingCollisionPoints(edges, center, angleRad,
277  out topCollisionPoint, out bottomCollisionPoint, out leftCollisionPoint, out rightCollisionPoint))
278  {
279  return false;
280  }
281 
282  float verticalMinDistanceToEdge =
283  Math.Min(Vector2.Distance(center, topCollisionPoint), Vector2.Distance(center, bottomCollisionPoint));
284  float horizontalMinDistanceToEdge =
285  Math.Min(Vector2.Distance(center, leftCollisionPoint), Vector2.Distance(center, rightCollisionPoint));
286 
287  // Width is the largest of the possible dimensions (doubled of course)
288  float maxWidth = Math.Max(verticalMinDistanceToEdge, horizontalMinDistanceToEdge) * 2.0f;
289  float maxHeight = Math.Min(verticalMinDistanceToEdge, horizontalMinDistanceToEdge) * 2.0f;
290 
291  // For each aspect ratio we do a binary search to find the maximum rectangle that fits, though once we start increasing our area by minimumHeightGain we call it good enough
292  foreach (var candidateAspectRatio in aspectRatios)
293  {
294  // The height is limited by the width. If a height would make our width exceed maxWidth, it can't be used
295  float searchHeightUpperBound = Math.Max(maxHeight, maxWidth / candidateAspectRatio);
296 
297  // Set to the min height that will out perform our previous area at the given aspect ratio. This is 0 the first time.
298  // Derived from biggestAreaSoFar=height*(height*aspctRatio)
299  float searchHeightLowerBound = (float)Math.Sqrt(Math.Max((width * height), minArea) / candidateAspectRatio);
300 
301  // If the lowest value needed to outperform the previous best is greater than our max, this aspect ratio can't outperform what we've already calculated
302  if (searchHeightLowerBound > searchHeightUpperBound || searchHeightLowerBound * candidateAspectRatio > maxWidth)
303  {
304  continue;
305  }
306 
307  float currentTestingHeight = Math.Max(searchHeightLowerBound, maxHeight / 2);
308 
309  // Do the binary search until continuing to search will not give us a significant win
310  do
311  {
312  if (WillRectangleFit(edges, center, angleRad, candidateAspectRatio * currentTestingHeight, currentTestingHeight))
313  {
314  // Binary search up-ward
315 
316  // If the rectangle will fit, increase the lower bounds of our binary search
317  searchHeightLowerBound = currentTestingHeight;
318 
319  width = currentTestingHeight * candidateAspectRatio;
320  height = currentTestingHeight;
321  aspectRatio = candidateAspectRatio;
322  currentTestingHeight = (searchHeightUpperBound + currentTestingHeight) / 2;
323  }
324  else
325  {
326  // If the rectangle won't fit, update our upper bound and lower our binary search
327  searchHeightUpperBound = currentTestingHeight;
328  currentTestingHeight = (currentTestingHeight + searchHeightLowerBound) / 2;
329  }
330  }
331  while ((searchHeightUpperBound - searchHeightLowerBound) > minimumHeightGain);
332  }
333 
334  return aspectRatio > 0;
335  }
336 
341  private bool WillRectangleFit(Edge[] edges, Vector2 center, float angleRad, float width, float height)
342  {
343  float halfWidth = width / 2;
344  float halfHeight = height / 2;
345 
346  var topLeft = new Vector2(center.x - halfWidth, center.y + halfHeight);
347  var topRight = new Vector2(center.x + halfWidth, center.y + halfHeight);
348  var bottomLeft = new Vector2(center.x - halfWidth, center.y - halfHeight);
349  var bottomRight = new Vector2(center.x + halfWidth, center.y - halfHeight);
350 
351  topLeft = RotatePoint(center, angleRad, topLeft);
352  topRight = RotatePoint(center, angleRad, topRight);
353  bottomLeft = RotatePoint(center, angleRad, bottomLeft);
354  bottomRight = RotatePoint(center, angleRad, bottomRight);
355 
356  var top = new Edge(topLeft, topRight);
357  var right = new Edge(topRight, bottomRight);
358  var bottom = new Edge(bottomLeft, bottomRight);
359  var left = new Edge(topLeft, bottomLeft);
360 
361  // If any edges of the polygon intersect with any of our edges, it won't fit
362  foreach (var edge in edges)
363  {
366  {
367  return false;
368  }
369  }
370 
371  return true;
372  }
373 
385  private void FindInscribedRectangle(Edge[] edges, int randomSeed, out Vector2 center, out float angle, out float width, out float height)
386  {
387  center = EdgeHelpers.InvalidPoint;
388  angle = width = height = 0;
389 
390  // Find min x, min y, max x, max y and generate random
391  // points in this range until we have randomPointCount
392  // random starting points
393  float minX = largeValue;
394  float minY = largeValue;
395  float maxX = smallValue;
396  float maxY = smallValue;
397 
398  foreach (var edge in edges)
399  {
400  if (edge.Ax < minX || edge.Bx < minX)
401  {
402  minX = Math.Min(edge.Ax, edge.Bx);
403  }
404  if (edge.Ay < minY || edge.By < minY)
405  {
406  minY = Math.Min(edge.Ay, edge.By);
407  }
408  if (edge.Ax > maxX || edge.Bx > maxX)
409  {
410  maxX = Math.Max(edge.Ax, edge.Bx);
411  }
412  if (edge.Ay > maxY || edge.By > maxY)
413  {
414  maxY = Math.Max(edge.Ay, edge.By);
415  }
416  }
417 
418  // Generate random points
419  Vector2[] startingPoints = new Vector2[randomPointCount];
420  {
421  var random = new System.Random(randomSeed);
422 
423  for (int pointIndex = 0; pointIndex < randomPointCount; ++pointIndex)
424  {
425  Vector2 candidatePoint;
426 
427  do
428  {
429  candidatePoint.x = ((float)random.NextDouble() * (maxX - minX)) + minX;
430  candidatePoint.y = ((float)random.NextDouble() * (maxY - minY)) + minY;
431  }
432  while (!EdgeHelpers.IsInside(edges, candidatePoint));
433 
434  startingPoints[pointIndex] = candidatePoint;
435  }
436  }
437 
438  float[] angles = { 0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165 };
439 
440  foreach (var candidateAngle in angles)
441  {
442  // For each randomly generated starting point
443  foreach (var startingPoint in startingPoints)
444  {
445  // Find the collision point of a cross through the given point at the given angle
446  // Ignore the return value. A return value of false indicates one of the points is bad.
447  // We will check each point's validity ourselves anyway.
448  Vector2 topCollisionPoint;
449  Vector2 bottomCollisionPoint;
450  Vector2 leftCollisionPoint;
451  Vector2 rightCollisionPoint;
452  FindSurroundingCollisionPoints(edges, startingPoint, DegreesToRadians(candidateAngle),
453  out topCollisionPoint, out bottomCollisionPoint, out leftCollisionPoint, out rightCollisionPoint);
454 
455  // Now calculate the midpoint between top and bottom (the "vertical midpoint") and left and right (the "horizontal midpoint")
456  if (EdgeHelpers.IsValidPoint(topCollisionPoint) && EdgeHelpers.IsValidPoint(bottomCollisionPoint))
457  {
458  var verticalMidpoint = new Vector2((topCollisionPoint.x + bottomCollisionPoint.x) / 2,
459  (topCollisionPoint.y + bottomCollisionPoint.y) / 2);
460  float aspectRatio;
461  float w;
462  float h;
463  if (TryFitMaximumRectangleAtAngle(edges, verticalMidpoint, DegreesToRadians(candidateAngle), width * height, out aspectRatio, out w, out h))
464  {
465  center = verticalMidpoint;
466  angle = candidateAngle;
467  width = w;
468  height = h;
469  }
470  }
471 
472  if (EdgeHelpers.IsValidPoint(leftCollisionPoint) && EdgeHelpers.IsValidPoint(rightCollisionPoint))
473  {
474  var horizontalMidpoint = new Vector2((leftCollisionPoint.x + rightCollisionPoint.x) / 2,
475  (leftCollisionPoint.y + rightCollisionPoint.y) / 2);
476  float aspectRatio;
477  float w;
478  float h;
479  if (TryFitMaximumRectangleAtAngle(edges, horizontalMidpoint, DegreesToRadians(candidateAngle), width * height, out aspectRatio, out w, out h))
480  {
481  center = horizontalMidpoint;
482  angle = candidateAngle;
483  width = w;
484  height = h;
485  }
486  }
487  }
488  }
489  }
490  }
491 }
static bool IsInside(Edge[] edges, Vector2 point)
Returns true if the given point is within the boundary.
Definition: EdgeHelpers.cs:48
bool IsPointInRectangleBounds(Vector2 point)
Returns true if the given point is within the inscribed rectangle.
void GetRectangleParams(out Vector2 centerOut, out float angleOut, out float widthOut, out float heightOut)
Retrieves the parameters describing the largest inscribed rectangle. within the bounds ...
InscribedRectangle(IList< Vector3 > points)
Constructor that takes in a list of points and generates a seed.
Vector2 [] GetRectanglePoints()
Returns the four points that make up the inscribed rectangle.
InscribedRectangle(IList< Vector3 > points, int randomSeed)
Constructor that takes in a list of points and sets a fixed random seed to get repeatable results...
static Vector2 GetIntersection(Edge edge1, Edge edge2)
Gets the point where two edges intersect. Value is InvalidPoint if they do not.
Definition: EdgeHelpers.cs:68
Helper struct to hold an edge.
Definition: Edge.cs:11
static readonly Vector2 InvalidPoint
Definition: EdgeHelpers.cs:18
InscribedRectangle(Edge[] edges)
Constructor that takes in an array of Edges and generates a seed.
static bool IsValidPoint(Vector2 point)
Helper to know when a point is invalid.
Definition: EdgeHelpers.cs:40
InscribedRectangle(Edge[] edges, int randomSeed)
Constructor that takes in an array of Edges and sets a fixed random seed to get repeatable results...