ianmackenzie / elm-geometry / BoundingBox2d

A BoundingBox2d is a rectangular box in 2D defined by its minimum and maximum X and Y values. It is possible to generate bounding boxes for most geometric objects; for example, Triangle2d.boundingBox takes a Triangle2d and returns a BoundingBox2d that contains that triangle. There are several use cases where it is more efficient to deal with the bounding box of an object than the object itself, such as:


type alias BoundingBox2d units coordinates =
Geometry.Types.BoundingBox2d units coordinates

Constructors

from : Point2d units coordinates -> Point2d units coordinates -> BoundingBox2d units coordinates

Construct a bounding box with the two given points as two of its corners. The points can be given in any order and don't have to represent the 'primary' diagonal of the bounding box.

BoundingBox2d.from
    (Point2d.meters 2 3)
    (Point2d.meters -1 5)
--> BoundingBox2d.fromExtrema
-->     { minX = Length.meters -1
-->     , maxX = Length.meters 2
-->     , minY = Length.meters 3
-->     , maxY = Length.meters 5
-->     }

fromExtrema : { minX : Quantity Basics.Float units, maxX : Quantity Basics.Float units, minY : Quantity Basics.Float units, maxY : Quantity Basics.Float units } -> BoundingBox2d units coordinates

Construct a bounding box from its minimum and maximum X and Y values:

exampleBox =
    BoundingBox2d.fromExtrema
        { minX = Length.meters 3
        , maxX = Length.meters 8
        , minY = Length.meters 2
        , maxY = Length.meters 6
        }

If the minimum and maximum values are provided in the wrong order (for example if minX is greater than maxX), then they will be swapped so that the resulting bounding box is valid.

withDimensions : ( Quantity Basics.Float units, Quantity Basics.Float units ) -> Point2d units coordinates -> BoundingBox2d units coordinates

Construct a bounding box given its overall dimensions (width and height) and center point.

singleton : Point2d units coordinates -> BoundingBox2d units coordinates

Construct a zero-width bounding box containing a single point.

xy : Quantity.Interval.Interval Basics.Float units -> Quantity.Interval.Interval Basics.Float units -> BoundingBox2d units coordinates

Construct a bounding box from separate X and Y intervals.

fromIntervals : ( Quantity.Interval.Interval Basics.Float units, Quantity.Interval.Interval Basics.Float units ) -> BoundingBox2d units coordinates

Construct a bounding box from a pair of X and Y intervals.

Booleans

union : BoundingBox2d units coordinates -> BoundingBox2d units coordinates -> BoundingBox2d units coordinates

Build a bounding box that contains both given bounding boxes.

firstBox =
    BoundingBox2d.from
        (Point2d.meters 1 2)
        (Point2d.meters 4 3)

secondBox =
    BoundingBox2d.from
        (Point2d.meters -2 4)
        (Point2d.meters 2 5)

BoundingBox2d.union firstBox secondBox
--> BoundingBox2d.from
-->     (Point2d.meters -2 2)
-->     (Point2d.meters 4 5)

(Note that this is not strictly speaking a 'union' in the precise mathematical sense.)

intersection : BoundingBox2d units coordinates -> BoundingBox2d units coordinates -> Maybe (BoundingBox2d units coordinates)

Attempt to build a bounding box that contains all points common to both given bounding boxes. If the given boxes do not intersect, returns Nothing.

firstBox =
    BoundingBox2d.from
        (Point2d.meters 1 2)
        (Point2d.meters 4 3)

secondBox =
    BoundingBox2d.from
        (Point2d.meters 2 1)
        (Point2d.meters 5 4)

BoundingBox2d.intersection firstBox secondBox
--> Just <|
-->     BoundingBox2d.from
-->         (Point2d.meters 2 2)
-->         (Point2d.meters 4 3)

If two boxes just touch along an edge or at a corner, they are still considered to have an intersection, even though that intersection will have zero area (at least one of its dimensions will be zero):

firstBox =
    BoundingBox2d.from
        (Point2d.meters 0 0)
        (Point2d.meters 1 2)

secondBox =
    BoundingBox2d.from
        (Point2d.meters 1 1)
        (Point2d.meters 2 3)

BoundingBox2d.intersection firstBox secondBox
--> Just <|
-->     BoundingBox2d.from
-->         (Point2d.meters 1 1)
-->         (Point2d.meters 1 2)

Hull

Functions for building bounding boxes containing several points.

hull : Point2d units coordinates -> List (Point2d units coordinates) -> BoundingBox2d units coordinates

Find the bounding box containing one or more input points:

BoundingBox2d.hull p1 [ p2, p3, p4 ]

Often ends up being used within a case expression:

case points of
    [] ->
        -- some default behavior

    first :: rest ->
        let
            boundingBox =
                BoundingBox2d.hull first rest
        in
        -- normal behavior using 'boundingBox'

See also hullN.

hull3 : Point2d units coordinates -> Point2d units coordinates -> Point2d units coordinates -> BoundingBox2d units coordinates

Build a bounding box that contains all three of the given points;

BoundingBox2d.hull3 p1 p2 p3

is equivalent to

BoundingBox2d.hull p1 [ p2, p3 ]

but is more efficient.

hullN : List (Point2d units coordinates) -> Maybe (BoundingBox2d units coordinates)

Construct a bounding box containing all N points in the given list. If the list is empty, returns Nothing. If you know you have at least one point, you can use hull instead.

hullOf : (a -> Point2d units coordinates) -> a -> List a -> BoundingBox2d units coordinates

Like hull, but lets you work on any kind of item as long as a point can be extracted from it. For example, if you had

type alias Vertex =
    { id : Int
    , position : Point2d Meters WorldCoordinates
    , color : Color
    }

then you could get the bounding box around several vertices using

BoundingBox2d.hullOf .position
    firstVertex
    [ secondVertex
    , thirdVertex
    , fourthVertex
    ]

hullOfN : (a -> Point2d units coordinates) -> List a -> Maybe (BoundingBox2d units coordinates)

Combination of hullOf and hullN.

Aggregation

Functions for combining several bounding boxes into one bounding box that contains all of the input boxes.

aggregate : BoundingBox2d units coordinates -> List (BoundingBox2d units coordinates) -> BoundingBox2d units coordinates

Find the bounding box containing one or more input boxes; works much like hull. See also aggregateN.

aggregate3 : BoundingBox2d units coordinates -> BoundingBox2d units coordinates -> BoundingBox2d units coordinates -> BoundingBox2d units coordinates

Build a bounding box that contains all three of the given bounding boxes;

BoundingBox2d.aggregate3 b1 b2 b3

is equivalent to

BoundingBox2d.aggregate b1 [ b2, b3 ]

but is more efficient.

aggregateN : List (BoundingBox2d units coordinates) -> Maybe (BoundingBox2d units coordinates)

Construct a bounding box containing all bounding boxes in the given list. If the list is empty, returns Nothing. If you know you have at least one bounding box, you can use aggregate instead.

aggregateOf : (a -> BoundingBox2d units coordinates) -> a -> List a -> BoundingBox2d units coordinates

Like aggregate, but lets you work on any kind of item as long as a bounding box can be extracted from it. For example, to get the bounding box around four triangles:

BoundingBox2d.aggregateOf Triangle2d.boundingBox
    firstTriangle
    [ secondTriangle
    , thirdTriangle
    , fourthTriangle
    ]

aggregateOfN : (a -> BoundingBox2d units coordinates) -> List a -> Maybe (BoundingBox2d units coordinates)

Combination of aggregateOf and aggregateN.

Properties

extrema : BoundingBox2d units coordinates -> { minX : Quantity Basics.Float units, maxX : Quantity Basics.Float units, minY : Quantity Basics.Float units, maxY : Quantity Basics.Float units }

Get the minimum and maximum X and Y values of a bounding box in a single record.

BoundingBox2d.extrema exampleBox
--> { minX = Length.meters 3
--> , maxX = Length.meters 8
--> , minY = Length.meters 2
--> , maxY = Length.meters 6
--> }

Can be useful when combined with record destructuring, e.g.

{ minX, maxX, minY, maxY } =
    BoundingBox2d.extrema exampleBox

minX : BoundingBox2d units coordinates -> Quantity Basics.Float units

maxX : BoundingBox2d units coordinates -> Quantity Basics.Float units

minY : BoundingBox2d units coordinates -> Quantity Basics.Float units

maxY : BoundingBox2d units coordinates -> Quantity Basics.Float units

dimensions : BoundingBox2d units coordinates -> ( Quantity Basics.Float units, Quantity Basics.Float units )

Get the X and Y dimensions (width and height) of a bounding box.

( width, height ) =
    BoundingBox2d.dimensions exampleBox

midX : BoundingBox2d units coordinates -> Quantity Basics.Float units

Get the median (central) X value of a bounding box.

midY : BoundingBox2d units coordinates -> Quantity Basics.Float units

Get the median (central) Y value of a bounding box.

xInterval : BoundingBox2d units coordinates -> Quantity.Interval.Interval Basics.Float units

Get the range of X values contained by a bounding box.

yInterval : BoundingBox2d units coordinates -> Quantity.Interval.Interval Basics.Float units

Get the range of Y values contained by a bounding box.

intervals : BoundingBox2d units coordinates -> ( Quantity.Interval.Interval Basics.Float units, Quantity.Interval.Interval Basics.Float units )

Convert a bounding box to a pair of X and Y intervals.

Queries

contains : Point2d units coordinates -> BoundingBox2d units coordinates -> Basics.Bool

Check if a bounding box contains a particular point.

isContainedIn : BoundingBox2d units coordinates -> BoundingBox2d units coordinates -> Basics.Bool

Test if the second given bounding box is fully contained within the first (is a subset of it).

outerBox =
    BoundingBox2d.from
        (Point2d.meters 0 0)
        (Point2d.meters 10 10)

innerBox =
    BoundingBox2d.from
        (Point2d.meters 1 3)
        (Point2d.meters 5 9)

overlappingBox =
    BoundingBox2d.from
        (Point2d.meters 1 3)
        (Point2d.meters 5 12)

innerBox |> BoundingBox2d.isContainedIn outerBox
--> True

overlappingBox |> BoundingBox2d.isContainedIn outerBox
--> False

intersects : BoundingBox2d units coordinates -> BoundingBox2d units coordinates -> Basics.Bool

Test if two boxes touch or overlap at all (have any points in common);

BoundingBox2d.intersects firstBox secondBox

is equivalent to

BoundingBox2d.intersection firstBox secondBox
    /= Nothing

but is more efficient.

overlappingByAtLeast : Quantity Basics.Float units -> BoundingBox2d units coordinates -> BoundingBox2d units coordinates -> Basics.Bool

Check two boxes overlap by at least the given amount. For example, you could implement a tolerant collision check (one that only returns true if the boxes overlap by at least a millimeter, and ignores boxes that just barely touch each other) as

boxesCollide firstBox secondBox =
    BoundingBox2d.overlappingByAtLeast
        (Length.millimeters 1)
        firstBox
        secondBox

Overlap is defined as the minimum distance one box would have to move so that it did not touch the other. Boxes that just touch are considered to have an overlap of zero, so

BoundingBox2d.overlappingByAtLeast Quantity.zero
    firstBox
    secondBox

will return true even if the two boxes just touch each other.

separatedByAtLeast : Quantity Basics.Float units -> BoundingBox2d units coordinates -> BoundingBox2d units coordinates -> Basics.Bool

Check if two boxes are separated by at least the given amount. For example, to perform clash detection between some objects, you could use separatedBy on those objects' bounding boxes as a quick check to see if the objects had a gap of at least 1 cm between them:

safelySeparated firstBox secondBox =
    BoundingBox2d.separatedByAtLeast
        (Length.centimeters 1)
        firstBox
        secondBox

Separation is defined as the minimum distance one box would have to move so that it touched the other. (Note that this may be a diagonal distance between corners.) Boxes that just touch are considered to have a separation of zero, so

BoundingBox2d.separatedByAtLeast Quantity.zero
    firstBox
    secondBox

will return true even if the two boxes just touch each other.

Interpolation

interpolate : BoundingBox2d units coordinates -> Basics.Float -> Basics.Float -> Point2d units coordinates

Interpolate within a bounding box based on parameter values which range from 0 to 1.

Transformations

scaleAbout : Point2d units coordinates -> Basics.Float -> BoundingBox2d units coordinates -> BoundingBox2d units coordinates

Scale a bounding box about a given point by a given scale.

translateBy : Vector2d units coordinates -> BoundingBox2d units coordinates -> BoundingBox2d units coordinates

Translate a bounding box by a given displacement.

translateIn : Direction2d coordinates -> Quantity Basics.Float units -> BoundingBox2d units coordinates -> BoundingBox2d units coordinates

Translate a bounding box in a given direction by a given distance.

expandBy : Quantity Basics.Float units -> BoundingBox2d units coordinates -> BoundingBox2d units coordinates

Expand the given bounding box in all directions by the given offset:

BoundingBox2d.expandBy (Length.meters 3) exampleBox
--> BoundingBox2d.fromExtrema
-->     { minX = Length.meters 0
-->     , maxX = Length.meters 11
-->     , minY = Length.meters -1
-->     , maxY = Length.meters 9
-->     }

Negative offsets will be treated as positive (the absolute value will be used), so the resulting box will always be at least as large as the original. If you need to be able to contract a bounding box, use offsetBy instead.

offsetBy : Quantity Basics.Float units -> BoundingBox2d units coordinates -> Maybe (BoundingBox2d units coordinates)

Expand or shrink the given bounding box in all the directions by the given distance. A positive offset will cause the bounding box to expand and a negative value will cause it to shrink.

BoundingBox2d.offsetBy (Length.meters -1) exampleBox
--> Just <|
-->     BoundingBox2d.fromExtrema
-->         { minX = Length.meters 4
-->         , maxX = Length.meters 7
-->         , minY = Length.meters 3
-->         , maxY = Length.meters 5
-->         }

Returns Nothing if the offset is negative and large enough to cause the bounding box to vanish (that is, if the offset is larger than half the height or half the width of the bounding box, whichever is less):

BoundingBox2d.offsetBy (Length.meters -3) exampleBox
--> Nothing

If you only want to expand a bounding box, you can use expandBy instead (which does not return a Maybe).

Unit conversions

at : Quantity Basics.Float (Quantity.Rate units2 units1) -> BoundingBox2d units1 coordinates -> BoundingBox2d units2 coordinates

Convert a bounding box from one units type to another, by providing a conversion factor given as a rate of change of destination units with respect to source units.

at_ : Quantity Basics.Float (Quantity.Rate units1 units2) -> BoundingBox2d units1 coordinates -> BoundingBox2d units2 coordinates

Convert a bounding box from one units type to another, by providing an 'inverse' conversion factor given as a rate of change of source units with respect to destination units.

Random point generation

randomPoint : BoundingBox2d units coordinates -> Random.Generator (Point2d units coordinates)

Create a random generator for points within a given bounding box.