Envelopes

Consider first the idea of a bounding box. A bounding box
expresses the distance to a bounding plane in every direction
parallel to an axis. That is, a bounding box can be thought of
as the intersection of a collection of half-planes, two
perpendicular to each axis.

More generally, the intersection of half-planes in every
direction would give a tight "bounding region", or convex hull.
However, representing such a thing intensionally would be
impossible; hence bounding boxes are often used as an
approximation.

An envelope is an extensional representation of such a
"bounding region". Instead of storing some sort of direct
representation, we store a function which takes a direction as
input and gives a distance to a bounding half-plane as output.
The important point is that envelopes can be composed, and
transformed by any affine transformation.

Formally, given a vector v, the envelope computes a scalar s such
that

for every point u inside the diagram,
if the projection of (u - origin) onto v is s' *^ v, then s' <= s.

Compute the envelope of an object. For types with an intrinsic
notion of "local origin", the envelope will be based there.
Other types (e.g. Trail) may have some other default
reference point at which the envelope will be based; their
instances should document what it is.

Compute the range of an enveloped object along a certain
direction. Returns a pair of scalars (lo,hi) such that the
object extends from (lo *^ v) to (hi *^ v). Returns Nothing
for objects with an empty envelope.

Note that the envelopeVMay / envelopePMay functions above should be
preferred, as this requires a call to magnitude. However, it is more
efficient than calling magnitude on the results of those functions.