diff --git a/AC-Vector.cabal b/AC-Vector.cabal
--- a/AC-Vector.cabal
+++ b/AC-Vector.cabal
@@ -1,6 +1,6 @@
 Cabal-Version: >= 1.6
 Name:          AC-Vector
-Version:       2.0.0
+Version:       2.1.0
 Stability:     Experimental
 Synopsis:      Efficient geometric vectors and transformations.
 
@@ -10,10 +10,11 @@
   with @Double@ fields, with seperate types for each size of
   vector, and a type class for handling vectors generally.
 
-  Existing API has been rearranged. Now supports 4D vectors
-  and linear transformations.
+  Now includes \"bounding box\" types, useful for graphical work.
+  (Note that this package itself has no graphics facilities as
+  such. It just provides data structures useful for graphics.)
 
-Category:      Data, Math, Numerical
+Category:      Data, Math, Numerical, Graphics
 License:       BSD3
 License-file:  License.txt
 Author:        Andrew Coppin
@@ -33,6 +34,11 @@
     Data.Vector.Transform.T1,
     Data.Vector.Transform.T2,
     Data.Vector.Transform.T3,
-    Data.Vector.Transform.T4
+    Data.Vector.Transform.T4,
+    Data.BoundingBox.Range,
+    Data.BoundingBox.B1,
+    Data.BoundingBox.B2,
+    Data.BoundingBox.B3,
+    Data.BoundingBox.B4
   Build-Depends:   base >= 4 && < 5
   HS-Source-Dirs:  .
diff --git a/Data/BoundingBox/B1.hs b/Data/BoundingBox/B1.hs
new file mode 100644
--- /dev/null
+++ b/Data/BoundingBox/B1.hs
@@ -0,0 +1,38 @@
+{- |
+  This module provides the 'BBox1' type (mainly for completeness).
+-}
+
+module Data.BoundingBox.B1 where
+
+import Data.Vector.Class
+import Data.Vector.V1
+import Data.BoundingBox.Range as R
+
+-- | The 'BBox1' type is basically a 'Range', but all the operations over it work with 'Vector1' (which is really 'Scalar'). While it's called a bounding /box/, a 1-dimensional box is in truth a simple line interval, just like 'Range'.
+newtype BBox1 = BBox1 {range :: Range} deriving (Eq, Show)
+
+-- | Given two vectors, construct a bounding box (swapping the endpoints if necessary).
+bounds :: Vector1 -> Vector1 -> BBox1
+bounds (Vector1 xa) (Vector1 xb) = BBox1 $ R.bounds xa xb
+
+-- | Test whether a 'Vector1' lies within a 'BBox1'.
+within_bounds :: Vector1 -> BBox1 -> Bool
+within_bounds (Vector1 x) (BBox1 r) = x `R.within_bounds` r
+
+-- | Return the minimum endpoint for a 'BBox1'.
+min_bound :: BBox1 -> Vector1
+min_bound = Vector1 . R.min_bound . range
+
+-- | Return the maximum endpoint for a 'BBox1'.
+max_bound :: BBox1 -> Vector1
+max_bound = Vector1 . R.max_bound . range
+
+-- | Take the union of two 'BBox1' values. The result is a new 'BBox1' that contains all the points the original boxes contained, plus any extra space between them.
+union :: BBox1 -> BBox1 -> BBox1
+union (BBox1 r0) (BBox1 r1) = BBox1 (r0 `R.union` r1)
+
+-- | Take the intersection of two 'BBox1' values. If the boxes do not overlap, return 'Nothing'. Otherwise return a 'BBox1' containing only the points common to both argument boxes.
+isect :: BBox1 -> BBox1 -> Maybe BBox1
+isect (BBox1 r0) (BBox1 r1) = do
+  r <- (r0 `R.isect` r1)
+  return (BBox1 r)
diff --git a/Data/BoundingBox/B2.hs b/Data/BoundingBox/B2.hs
new file mode 100644
--- /dev/null
+++ b/Data/BoundingBox/B2.hs
@@ -0,0 +1,57 @@
+{- |
+  This module provides the 'BBox2' type for 2-dimensional bounding boxes.
+-}
+
+module Data.BoundingBox.B2 where
+
+import Data.Vector.Class
+import Data.Vector.V2
+import Data.BoundingBox.Range as R
+
+-- | A 'BBox2' is a 2D bounding box (aligned to the coordinate axies).
+data BBox2 = BBox2 {minX, minY, maxX, maxY :: {-# UNPACK #-} !Scalar} deriving (Eq, Show)
+
+-- | Return the X-range that this bounding box covers.
+rangeX :: BBox2 -> Range
+rangeX b = Range (minX b) (maxX b)
+
+-- | Return the Y-range that this bounding box covers.
+rangeY :: BBox2 -> Range
+rangeY b = Range (minY b) (maxY b)
+
+-- | Given ranges for each coordinate axis, construct a bounding box.
+rangeXY :: Range -> Range -> BBox2
+rangeXY (Range x0 x1) (Range y0 y1) = BBox2 x0 y0 x1 y1
+
+-- | Given a pair of corner points, construct a bounding box. (The points must be from opposite corners, but it doesn't matter /which/ corners nor which order they are given in.)
+bounds :: Vector2 -> Vector2 -> BBox2
+bounds (Vector2 xa ya) (Vector2 xb yb) = BBox2 (min xa xb) (min ya yb) (max xa xb) (max ya yb)
+
+-- | Test whether a given 2D vector is inside this bounding box.
+within_bounds :: Vector2 -> BBox2 -> Bool
+within_bounds (Vector2 x y) b =
+  x `R.within_bounds` (rangeX b) &&
+  y `R.within_bounds` (rangeY b)
+
+-- | Return the minimum values for both coordinates. (In usual 2D space, the bottom-left corner point.)
+min_bound :: BBox2 -> Vector2
+min_bound (BBox2 x0 y0 x1 y1) = Vector2 x0 y0
+
+-- | Return the maximum values for both coordinates. (In usual 2D space, the top-right corner point.)
+max_bound :: BBox2 -> Vector2
+max_bound (BBox2 x0 y0 x1 y1) = Vector2 x1 y1
+
+-- | Take the union of two bounding boxes. The result is a new bounding box that contains all the points the original boxes contained, plus any extra space between them.
+union :: BBox2 -> BBox2 -> BBox2
+union b0 b1 =
+  let
+    rx = (rangeX b0) `R.union` (rangeX b1)
+    ry = (rangeY b0) `R.union` (rangeY b1)
+  in rangeXY rx ry
+
+-- | Take the intersection of two bounding boxes. If the boxes do not overlap, return 'Nothing'. Otherwise return a new bounding box containing only the points common to both argument boxes.
+isect :: BBox2 -> BBox2 -> Maybe BBox2
+isect b0 b1 = do
+  rx <- (rangeX b0) `R.isect` (rangeX b1)
+  ry <- (rangeY b0) `R.isect` (rangeY b1)
+  return (rangeXY rx ry)
diff --git a/Data/BoundingBox/B3.hs b/Data/BoundingBox/B3.hs
new file mode 100644
--- /dev/null
+++ b/Data/BoundingBox/B3.hs
@@ -0,0 +1,64 @@
+{- |
+  This module provides the 'BBox3' type for 3-dimensional bounding boxes (\"bounding volumes\").
+-}
+
+module Data.BoundingBox.B3 where
+
+import Data.Vector.Class
+import Data.Vector.V3
+import Data.BoundingBox.Range as R
+
+-- | A 'BBox3' is a 3D bounding box (aligned to the coordinate axies).
+data BBox3 = BBox3 {minX, minY, minZ, maxX, maxY, maxZ :: {-# UNPACK #-} !Scalar} deriving (Eq, Show)
+
+-- | Return the X-range that this bounding box covers.
+rangeX :: BBox3 -> Range
+rangeX b = Range (minX b) (maxX b)
+
+-- | Return the Y-range that this bounding box covers.
+rangeY :: BBox3 -> Range
+rangeY b = Range (minY b) (maxY b)
+
+-- | Return the Z-range that this bounding box covers.
+rangeZ :: BBox3 -> Range
+rangeZ b = Range (minZ b) (maxZ b)
+
+-- | Given ranges for each coordinate axis, construct a bounding box.
+rangeXYZ :: Range -> Range -> Range -> BBox3
+rangeXYZ (Range x0 x1) (Range y0 y1) (Range z0 z1) = BBox3 x0 y0 z0 x1 y1 z1
+
+-- | Given a pair of corner points, construct a bounding box. (The points must be from opposite corners, but it doesn't matter /which/ corners nor which order they are given in.)
+bounds :: Vector3 -> Vector3 -> BBox3
+bounds (Vector3 xa ya za) (Vector3 xb yb zb) = BBox3 (min xa xb) (min ya yb) (min za zb) (max xa xb) (max ya yb) (max za zb)
+
+-- | Test whether a given 3D vector is inside this bounding box.
+within_bounds :: Vector3 -> BBox3 -> Bool
+within_bounds (Vector3 x y z) b =
+  x `R.within_bounds` (rangeX b) &&
+  y `R.within_bounds` (rangeY b) &&
+  z `R.within_bounds` (rangeZ b)
+
+-- | Return the minimum values for all coordinates.
+min_bound :: BBox3 -> Vector3
+min_bound (BBox3 x0 y0 z0 x1 y1 z1) = Vector3 x0 y0 z0
+
+-- | Return the maximum values for all coordinates.
+max_bound :: BBox3 -> Vector3
+max_bound (BBox3 x0 y0 z0 x1 y1 z1) = Vector3 x1 y1 z1
+
+-- | Take the union of two bounding boxes. The result is a new bounding box that contains all the points the original boxes contained, plus any extra space between them.
+union :: BBox3 -> BBox3 -> BBox3
+union b0 b1 =
+  let
+    rx = (rangeX b0) `R.union` (rangeX b1)
+    ry = (rangeY b0) `R.union` (rangeY b1)
+    rz = (rangeZ b0) `R.union` (rangeZ b1)
+  in rangeXYZ rx ry rz
+
+-- | Take the intersection of two bounding boxes. If the boxes do not overlap, return 'Nothing'. Otherwise return a new bounding box containing only the points common to both argument boxes.
+isect :: BBox3 -> BBox3 -> Maybe BBox3
+isect b0 b1 = do
+  rx <- (rangeX b0) `R.isect` (rangeX b1)
+  ry <- (rangeY b0) `R.isect` (rangeY b1)
+  rz <- (rangeZ b0) `R.isect` (rangeZ b1)
+  return (rangeXYZ rx ry rz)
diff --git a/Data/BoundingBox/B4.hs b/Data/BoundingBox/B4.hs
new file mode 100644
--- /dev/null
+++ b/Data/BoundingBox/B4.hs
@@ -0,0 +1,72 @@
+{- |
+  This module provides the 'BBox4' type for 4-dimensional bounding boxes (bounding hyper-volumes\).
+-}
+
+module Data.BoundingBox.B4 where
+
+import Data.Vector.Class
+import Data.Vector.V4
+import Data.BoundingBox.Range as R
+
+-- | A 'BBox4' is a 4D bounding box (aligned to the coordinate axies).
+data BBox4 = BBox4 {minX, minY, minZ, minW, maxX, maxY, maxZ, maxW :: {-# UNPACK #-} !Scalar} deriving (Eq, Show)
+
+-- | Return the X-range that this bounding box covers.
+rangeX :: BBox4 -> Range
+rangeX b = Range (minX b) (maxX b)
+
+-- | Return the Y-range that this bounding box covers.
+rangeY :: BBox4 -> Range
+rangeY b = Range (minY b) (maxY b)
+
+-- | Return the Z-range that this bounding box covers.
+rangeZ :: BBox4 -> Range
+rangeZ b = Range (minZ b) (maxZ b)
+
+-- | Return the W-range (4th coordinate) that this bounding box covers.
+rangeW :: BBox4 -> Range
+rangeW b = Range (minW b) (maxW b)
+
+-- | Given ranges for each coordinate axis, construct a bounding box.
+rangeXYZW :: Range -> Range -> Range -> Range -> BBox4
+rangeXYZW (Range x0 x1) (Range y0 y1) (Range z0 z1) (Range w0 w1) = BBox4 x0 y0 z0 w0 x1 y1 z1 w1
+
+-- | Given a pair of corner points, construct a bounding box. (The points must be from opposite corners, but it doesn't matter /which/ corners nor which order they are given in.)
+bounds :: Vector4 -> Vector4 -> BBox4
+bounds (Vector4 xa ya za wa) (Vector4 xb yb zb wb) =
+  BBox4 (min xa xb) (min ya yb) (min za zb) (min wa wb) (max xa xb) (max ya yb) (max za zb) (max wa wb)
+
+-- | Test whether a given 4D vector is inside this bounding box.
+within_bounds :: Vector4 -> BBox4 -> Bool
+within_bounds (Vector4 x y z w) b =
+  x `R.within_bounds` (rangeX b) &&
+  y `R.within_bounds` (rangeY b) &&
+  z `R.within_bounds` (rangeZ b) &&
+  w `R.within_bounds` (rangeW b)
+
+-- | Return the minimum values for all coordinates.
+min_bound :: BBox4 -> Vector4
+min_bound (BBox4 x0 y0 z0 w0 x1 y1 z1 w1) = Vector4 x0 y0 z0 w0
+
+-- | Return the maximum values for all coordinates.
+max_bound :: BBox4 -> Vector4
+max_bound (BBox4 x0 y0 z0 w0 x1 y1 z1 w1) = Vector4 x1 y1 z1 w1
+
+-- | Take the union of two bounding boxes. The result is a new bounding box that contains all the points the original boxes contained, plus any extra space between them.
+union :: BBox4 -> BBox4 -> BBox4
+union b0 b1 =
+  let
+    rx = (rangeX b0) `R.union` (rangeX b1)
+    ry = (rangeY b0) `R.union` (rangeY b1)
+    rz = (rangeZ b0) `R.union` (rangeZ b1)
+    rw = (rangeW b0) `R.union` (rangeW b1)
+  in rangeXYZW rx ry rz rw
+
+-- | Take the intersection of two bounding boxes. If the boxes do not overlap, return 'Nothing'. Otherwise return a new bounding box containing only the points common to both argument boxes.
+isect :: BBox4 -> BBox4 -> Maybe BBox4
+isect b0 b1 = do
+  rx <- (rangeX b0) `R.isect` (rangeX b1)
+  ry <- (rangeY b0) `R.isect` (rangeY b1)
+  rz <- (rangeZ b0) `R.isect` (rangeZ b1)
+  rw <- (rangeW b0) `R.isect` (rangeW b1)
+  return (rangeXYZW rx ry rz rw)
diff --git a/Data/BoundingBox/Range.hs b/Data/BoundingBox/Range.hs
new file mode 100644
--- /dev/null
+++ b/Data/BoundingBox/Range.hs
@@ -0,0 +1,31 @@
+{- |
+  This module provides the 'Range' type and several functions for working with ranges.
+-}
+
+module Data.BoundingBox.Range where
+
+import Data.Vector.Class
+
+{- |
+  A 'Range' represents a continuous interval between two 'Scalar' endpoints.
+-}
+data Range = Range {min_bound, max_bound :: {-# UNPACK #-} !Scalar} deriving (Eq, Show)
+
+-- | Given two 'Scalar's, construct a 'Range' (swapping the endpoints if necessary so that they are in the correct order.
+bounds :: Scalar -> Scalar -> Range
+bounds xa xb = Range (min xa xb) (max xa xb)
+
+-- | Test whether a given 'Scalar' falls within a particular 'Range'.
+within_bounds :: Scalar -> Range -> Bool
+within_bounds x (Range x0 x1) = x0 <= x && x <= x1
+
+-- | Take the union of two ranges. The resulting 'Range' contains all points that the original ranges contained, plus any points between them (if the original ranges don't overlap).
+union :: Range -> Range -> Range
+union (Range x00 x01) (Range x10 x11) = Range (min x00 x01) (max x01 x11)
+
+-- | Take the intersection of two ranges. If the ranges do not overlap, the intersection is empty, and 'Nothing' is returned. (This is a good way to check whether two ranges overlap or not.) Otherwise a new 'Range' is returned that contains only the points common to both ranges.
+isect :: Range -> Range -> Maybe Range
+isect (Range x00 x01) (Range x10 x11) =
+  if x00 < x10
+    then if x10 < x01 then Just (Range x10 (min x01 x11)) else Nothing
+    else if x00 < x11 then Just (Range x00 (min x01 x11)) else Nothing
