This is an automated email from the ASF dual-hosted git repository.

mattjuntunen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git


The following commit(s) were added to refs/heads/master by this push:
     new 27b8d65a GEOMETRY-147: adding linecast and line intersection methods 
to Bounds2D and Bounds3D
27b8d65a is described below

commit 27b8d65ad6900af39b6036d8a00dcd8d448c99ee
Author: Matt <[email protected]>
AuthorDate: Fri Apr 29 23:39:40 2022 -0400

    GEOMETRY-147: adding linecast and line intersection methods to Bounds2D and 
Bounds3D
---
 .../commons/geometry/euclidean/AbstractBounds.java | 225 +++++++
 .../geometry/euclidean/threed/Bounds3D.java        | 172 ++++-
 .../threed/line/EmbeddedTreeLineSubset3D.java      |   7 +
 .../euclidean/threed/line/LineConvexSubset3D.java  |  10 +-
 .../threed/line/LineSpanningSubset3D.java          |  13 +-
 .../euclidean/threed/line/LineSubset3D.java        |   7 +
 .../euclidean/threed/line/LinecastPoint3D.java     |   3 +-
 .../geometry/euclidean/threed/line/Ray3D.java      |  19 +-
 .../euclidean/threed/line/ReverseRay3D.java        |  19 +-
 .../geometry/euclidean/threed/line/Segment3D.java  |  29 +-
 .../commons/geometry/euclidean/twod/Bounds2D.java  | 159 ++++-
 .../euclidean/twod/EmbeddedTreeLineSubset.java     |  12 +-
 .../geometry/euclidean/twod/LineConvexSubset.java  |   7 +
 .../euclidean/twod/LineSpanningSubset.java         |   7 -
 .../geometry/euclidean/twod/LineSubset.java        |   2 +-
 .../commons/geometry/euclidean/twod/Ray.java       |  26 +-
 .../geometry/euclidean/twod/ReverseRay.java        |  26 +-
 .../commons/geometry/euclidean/twod/Segment.java   |  30 +-
 .../geometry/euclidean/threed/Bounds3DTest.java    | 696 ++++++++++++++++++++-
 .../threed/line/EmbeddedTreeLineSubset3DTest.java  |  19 +
 .../geometry/euclidean/twod/Bounds2DTest.java      | 611 ++++++++++++++++++
 .../jmh/euclidean/Bounds3DPerformance.java         | 208 ++++++
 22 files changed, 2213 insertions(+), 94 deletions(-)

diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractBounds.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractBounds.java
index 168fdd8e..6f1fbb8f 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractBounds.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractBounds.java
@@ -16,6 +16,12 @@
  */
 package org.apache.commons.geometry.euclidean;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.ToDoubleFunction;
+
 import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
 import org.apache.commons.numbers.core.Precision;
 
@@ -156,4 +162,223 @@ public abstract class AbstractBounds<
 
         return sb.toString();
     }
+
+    /** Abstract internal class used to perform line convex subset 
intersection operations using the
+     * <a 
href="https://education.siggraph.org/static/HyperGraph/raytrace/rtinter3.htm";>slabs
 algorithm</a>.
+     * Subclasses are expected to reference a line convex subset in their 
target dimension that is being
+     * evaluated against the bounding box. Access to the line and subset 
properties is facilitated through
+     * abstract methods.
+     * @param <S> Line segment type
+     * @param <I> Boundary intersection type
+     */
+    protected abstract class BoundsLinecaster<S, I> {
+
+        /** Precision used for floating point comparisons. */
+        private final Precision.DoubleEquivalence precision;
+
+        /** Near slab intersection abscissa value. */
+        private double near = Double.NEGATIVE_INFINITY;
+
+        /** Far slab intersection abscissa value. */
+        private double far = Double.POSITIVE_INFINITY;
+
+        /** Construct a new instance that uses the given precision instance 
for floating
+         * point comparisons.
+         * @param precision precision instance for floating point comparisons
+         */
+        protected BoundsLinecaster(final Precision.DoubleEquivalence 
precision) {
+            this.precision = precision;
+        }
+
+        /** Return {@code true} if the line convex subset shares any points 
with the
+         * bounding box.
+         * @return {@code true} if the line convex subset shares any points 
with the
+         *      bounding box
+         */
+        public boolean intersectsRegion() {
+            return computeNearFar() &&
+                    precision.gte(getSubspaceEnd(), near) &&
+                    precision.lte(getSubspaceStart(), far);
+        }
+
+        /** Get the segment containing all points shared by the line convex
+         * subset and the bounding box, or {@code null} if no points are 
shared.
+         * @return segment containing all points shared by the line convex
+         *      subset and the bounding box, or {@code null} if no points are 
shared.
+         */
+        public S getRegionIntersection() {
+            if (intersectsRegion()) {
+                final double start = Math.max(near, getSubspaceStart());
+                final double end = Math.min(far, getSubspaceEnd());
+
+                return createSegment(start, end);
+            }
+            return null;
+        }
+
+        /** Get the intersections between the line convex subset and the 
boundaries of the
+         * bounding box. An empty list is returned if there are no 
intersections.
+         * @return intersections between the line convex subset and the 
boundaries of the
+         *      bounding box
+         */
+        public List<I> getBoundaryIntersections() {
+            if (computeNearFar()) {
+                final int maxSize = min.getDimension() * 2;
+                final List<I> results = new ArrayList<>(maxSize);
+
+                addBoundaryIntersections(near, results);
+                if (!precision.eq(near, far)) {
+                    addBoundaryIntersections(far, results);
+                }
+
+                results.sort(getBoundaryIntersectionComparator());
+
+                return results;
+            }
+
+            return Collections.emptyList();
+        }
+
+        /** Get an object representing the <em>first</em> intersection of the 
line convex subset
+         * with the boundaries of the bounding box. Null is returned if no 
such intersection exists.
+         * @return object representing the first intersection of the line 
convex subset with the
+         *      boundaries of the bounding box, or {@code null} if no such 
intersection exists
+         */
+        public I getFirstBoundaryIntersection() {
+            final List<I> results = getBoundaryIntersections();
+            return results.isEmpty() ?
+                    null :
+                    results.get(0);
+        }
+
+        /** Add a boundary intersection to {@code results} if the given point 
lies on
+         * one of the bounding box boundaries orthogonal to {@code dimPlusDir}.
+         * @param pt potential intersection point
+         * @param dimMinusDir minus direction for the dimension being evaluated
+         * @param dimPlusDir plus direction for the dimension being evaluated
+         * @param coordinateFn function used to access point coordinate values 
for
+         *      the dimension being evaluated
+         * @param results list containing intersection results
+         */
+        protected void addBoundaryIntersectionIfPresent(
+                final P pt,
+                final P dimMinusDir,
+                final P dimPlusDir,
+                final ToDoubleFunction<P> coordinateFn,
+                final List<I> results) {
+
+            // only include results for dimensions that are not considered
+            // parallel to the line, according to the precision
+            if (!precision.eqZero(getLineDir().dot(dimPlusDir))) {
+                final double coordinate = coordinateFn.applyAsDouble(pt);
+                final double dimMin = coordinateFn.applyAsDouble(min);
+                final double dimMax = coordinateFn.applyAsDouble(max);
+
+                if (precision.eq(coordinate, dimMin)) {
+                    results.add(createBoundaryIntersection(pt, dimMinusDir));
+                }
+
+                if (precision.eq(coordinate, dimMax)) {
+                    results.add(createBoundaryIntersection(pt, dimPlusDir));
+                }
+            }
+        }
+
+        /** Update the {@code near} and {@code far} slab intersection points 
with the
+         * intersection values for the coordinates returned by {@code 
coordinateFn}, returning
+         * {@code false} if the line is determined to not intersect the 
bounding box.
+         * @param coordinateFn function returning the coordinate for the 
dimension
+         *      being evaluated
+         * @return {@code false} if the line is determined to not intersect 
the bounding
+         *      box
+         */
+        protected boolean updateNearFar(final ToDoubleFunction<P> 
coordinateFn) {
+            final double dir = coordinateFn.applyAsDouble(getLineDir());
+            final double origin = coordinateFn.applyAsDouble(getLineOrigin());
+
+            final double minCoord = coordinateFn.applyAsDouble(min);
+            final double maxCoord = coordinateFn.applyAsDouble(max);
+
+            double t1 = (minCoord - origin) / dir;
+            double t2 = (maxCoord - origin) / dir;
+
+            if (!Double.isFinite(t1) || !Double.isFinite(t2)) {
+                // the line is parallel to this dimension; only continue if the
+                // line origin lies between the min and max for this dimension
+                return precision.gte(origin, minCoord) && 
precision.lte(origin, maxCoord);
+            }
+
+            if (t1 > t2) {
+                final double temp = t1;
+                t1 = t2;
+                t2 = temp;
+            }
+
+            if (t1 > near) {
+                near = t1;
+            }
+
+            if (t2 < far) {
+                far = t2;
+            }
+
+            return precision.lte(near, far);
+        }
+
+        /** Create a line segment with the given start and end abscissas.
+         * @param startAbscissa start abscissa
+         * @param endAbscissa end abscissa
+         * @return line segment with the given start and end abscissas
+         */
+        protected abstract S createSegment(double startAbscissa, double 
endAbscissa);
+
+        /** Construct a new boundary intersection instance.
+         * @param pt boundary intersection point
+         * @param normal boundary normal at the intersection
+         * @return a new boundary intersection instance
+         */
+        protected abstract I createBoundaryIntersection(P pt, P normal);
+
+        /** Add all boundary intersections at the given line abscissa value to 
{@code results}.
+         * Subclasses should call {@link #addBoundaryIntersectionIfPresent} 
for each dimension
+         * in the target space.
+         * @param abscissa intersection abscissa
+         * @param results boundary intersection result list
+         */
+        protected abstract void addBoundaryIntersections(double abscissa, 
List<I> results);
+
+        /** Get the comparator used to produce a standardized ordering of 
boundary intersection
+         * results.
+         * @return comparator used to store boundary intersections
+         */
+        protected abstract Comparator<I> getBoundaryIntersectionComparator();
+
+        /** Compute the {@code near} and {@code far} slab intersection values 
for the
+         * line under test, returning {@code true} if the line intersects the 
bounding
+         * box. This method should call {@link 
#updateNearFar(ToDoubleFunction)} for each
+         * dimension in the space.
+         * @return {@code true} if the line intersects the bounding box
+         */
+        protected abstract boolean computeNearFar();
+
+        /** Get the line direction.
+         * @return line direction
+         */
+        protected abstract P getLineDir();
+
+        /** Get the line origin.
+         * @return line origin
+         */
+        protected abstract P getLineOrigin();
+
+        /** Get the line convex subset start abscissa.
+         * @return line convex subset start abscissa
+         */
+        protected abstract double getSubspaceStart();
+
+        /** Get the line convex subset end abscissa.
+         * @return line convex subset end abscissa
+         */
+        protected abstract double getSubspaceEnd();
+    }
 }
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Bounds3D.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Bounds3D.java
index 69be9f5a..9dcc3e42 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Bounds3D.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Bounds3D.java
@@ -17,9 +17,17 @@
 package org.apache.commons.geometry.euclidean.threed;
 
 import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
 import java.util.Objects;
 
+import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.euclidean.AbstractBounds;
+import org.apache.commons.geometry.euclidean.threed.line.Line3D;
+import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
+import org.apache.commons.geometry.euclidean.threed.line.LinecastPoint3D;
+import org.apache.commons.geometry.euclidean.threed.line.Linecastable3D;
+import org.apache.commons.geometry.euclidean.threed.line.Segment3D;
 import org.apache.commons.geometry.euclidean.threed.shape.Parallelepiped;
 import org.apache.commons.numbers.core.Precision;
 
@@ -29,7 +37,8 @@ import org.apache.commons.numbers.core.Precision;
  *
  * <p>Instances of this class are guaranteed to be immutable.</p>
  */
-public final class Bounds3D extends AbstractBounds<Vector3D, Bounds3D> {
+public final class Bounds3D extends AbstractBounds<Vector3D, Bounds3D>
+    implements Linecastable3D {
 
     /** Simple constructor. Callers are responsible for ensuring the min is 
not greater than max.
      * @param min minimum point
@@ -120,6 +129,66 @@ public final class Bounds3D extends 
AbstractBounds<Vector3D, Bounds3D> {
         return null; // no intersection
     }
 
+    /** Return {@code true} if the region represented by this instance shares 
any points with
+     * the given line. Floating point comparisons are made using the
+     * {@link Line3D#getPrecision() precision} of the line.
+     * @param line line to determine intersection with
+     * @return {@code true} if the region represented by this instance 
intersects
+     *      the given line
+     */
+    public boolean intersects(final Line3D line) {
+        return intersects(line.span());
+    }
+
+    /** Return {@code true} if the region represented by this instance shares 
any points with
+     * the given line convex subset. Floating point comparisons are made using 
the
+     * {@link Line3D#getPrecision() precision} of the subset's line.
+     * @param subset line convex subset to determine intersection with
+     * @return {@code true} if the region represented by this instance 
intersects
+     *      the given line convex subset
+     */
+    public boolean intersects(final LineConvexSubset3D subset) {
+        return new BoundsLinecaster3D(subset).intersectsRegion();
+    }
+
+    /** Return a {@link Segment3D} representing the intersection of the region
+     * represented by this instance with the given line or {@code null} if no 
such
+     * intersection exists. Floating point comparisons are made using the
+     * {@link Line3D#getPrecision() precision} of the line.
+     * @param line line to intersect with
+     * @return {@link Segment3D} representing the intersection of the region
+     *      represented by this instance with the given line or {@code null}
+     *      if no such intersection exists
+     */
+    public Segment3D intersection(final Line3D line) {
+        return intersection(line.span());
+    }
+
+    /** Return a {@link Segment3D} representing the intersection of the region
+     * represented by this instance with the given line convex subset or 
{@code null}
+     * if no such intersection exists. Floating point comparisons are made 
using the
+     * {@link Line3D#getPrecision() precision} of the subset's line.
+     * @param subset line convex subset to intersect with
+     * @return {@link Segment3D} representing the intersection of the region
+     *      represented by this instance with the given line convex subset or 
{@code null}
+     *      if no such intersection exists
+     */
+    public Segment3D intersection(final LineConvexSubset3D subset) {
+        return new BoundsLinecaster3D(subset).getRegionIntersection();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public List<LinecastPoint3D> linecast(final LineConvexSubset3D subset) {
+        return new BoundsLinecaster3D(subset).getBoundaryIntersections();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public LinecastPoint3D linecastFirst(final LineConvexSubset3D subset) {
+        return new BoundsLinecaster3D(subset).getFirstBoundaryIntersection();
+    }
+
     /** {@inheritDoc}
      *
      * @throws IllegalArgumentException if any dimension of the bounding box 
is zero
@@ -287,4 +356,105 @@ public final class Bounds3D extends 
AbstractBounds<Vector3D, Bounds3D> {
             return new Bounds3D(min, max);
         }
     }
+
+    /** Subclass of {@link BoundsLinecaster} for 3D space.
+     */
+    private final class BoundsLinecaster3D extends BoundsLinecaster<Segment3D, 
LinecastPoint3D> {
+
+        /** Line convex subset to be tested against the bounds. */
+        private final LineConvexSubset3D subset;
+
+        /** Line instance for the subset being tested. */
+        private final Line3D line;
+
+         /** Construct a new instance for computing bounds intersection 
information with
+          * the given line convex subset.
+          * @param subset line convex subset to compute intersection 
information for
+          */
+        BoundsLinecaster3D(final LineConvexSubset3D subset) {
+            super(subset.getLine().getPrecision());
+
+            this.subset = subset;
+            this.line = subset.getLine();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected Segment3D createSegment(final double startAbscissa, final 
double endAbscissa) {
+            return line.segment(startAbscissa, endAbscissa);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected LinecastPoint3D createBoundaryIntersection(final Vector3D 
pt, final Vector3D normal) {
+            return new LinecastPoint3D(pt, normal, line);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected void addBoundaryIntersections(final double abscissa, final 
List<LinecastPoint3D> results) {
+            if (subset.classifyAbscissa(abscissa) != RegionLocation.OUTSIDE) {
+                final Vector3D pt = line.toSpace(abscissa);
+
+                addBoundaryIntersectionIfPresent(
+                        pt,
+                        Vector3D.Unit.MINUS_X,
+                        Vector3D.Unit.PLUS_X,
+                        Vector3D::getX,
+                        results);
+
+                addBoundaryIntersectionIfPresent(
+                        pt,
+                        Vector3D.Unit.MINUS_Y,
+                        Vector3D.Unit.PLUS_Y,
+                        Vector3D::getY,
+                        results);
+
+                addBoundaryIntersectionIfPresent(
+                        pt,
+                        Vector3D.Unit.MINUS_Z,
+                        Vector3D.Unit.PLUS_Z,
+                        Vector3D::getZ,
+                        results);
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected boolean computeNearFar() {
+            return updateNearFar(Vector3D::getX) &&
+                    updateNearFar(Vector3D::getY) &&
+                    updateNearFar(Vector3D::getZ);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected Comparator<LinecastPoint3D> 
getBoundaryIntersectionComparator() {
+            return LinecastPoint3D.ABSCISSA_ORDER;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected Vector3D getLineDir() {
+            return line.getDirection();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected Vector3D getLineOrigin() {
+            return line.getOrigin();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected double getSubspaceStart() {
+            return subset.getSubspaceStart();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected double getSubspaceEnd() {
+            return subset.getSubspaceEnd();
+        }
+    }
 }
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3D.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3D.java
index 72466d90..18e78af4 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3D.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3D.java
@@ -19,6 +19,7 @@ package org.apache.commons.geometry.euclidean.threed.line;
 import java.util.ArrayList;
 import java.util.List;
 
+import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.euclidean.oned.Interval;
 import org.apache.commons.geometry.euclidean.oned.RegionBSPTree1D;
@@ -104,6 +105,12 @@ public final class EmbeddedTreeLineSubset3D extends 
LineSubset3D {
         return null;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public RegionLocation classifyAbscissa(final double abscissa) {
+        return region.classify(abscissa);
+    }
+
     /** Transform this instance.
      * @param transform the transform to apply
      * @return a new, transformed instance
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineConvexSubset3D.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineConvexSubset3D.java
index d3d9d4bc..dee15b40 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineConvexSubset3D.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineConvexSubset3D.java
@@ -16,6 +16,7 @@
  */
 package org.apache.commons.geometry.euclidean.threed.line;
 
+import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.euclidean.oned.Interval;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
@@ -79,7 +80,7 @@ public abstract class LineConvexSubset3D extends LineSubset3D 
{
      */
     public boolean contains(final Vector3D pt) {
         final Line3D line = getLine();
-        return line.contains(pt) && containsAbscissa(line.abscissa(pt));
+        return line.contains(pt) && classifyAbscissa(line.abscissa(pt)) != 
RegionLocation.OUTSIDE;
     }
 
     /** Transform this instance.
@@ -87,11 +88,4 @@ public abstract class LineConvexSubset3D extends 
LineSubset3D {
      * @return a new, transformed instance
      */
     public abstract LineConvexSubset3D transform(Transform<Vector3D> 
transform);
-
-    /** Return true if the given abscissa value is contained in the line 
subset (ie, in the subspace region
-     * or one of its 1D boundaries).
-     * @param abscissa abscissa to check
-     * @return true if {@code abscissa} lies on the inside or boundary of the 
subspace region
-     */
-    abstract boolean containsAbscissa(double abscissa);
 }
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSpanningSubset3D.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSpanningSubset3D.java
index 48ddfeca..9b3404b9 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSpanningSubset3D.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSpanningSubset3D.java
@@ -18,6 +18,7 @@ package org.apache.commons.geometry.euclidean.threed.line;
 
 import java.text.MessageFormat;
 
+import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.euclidean.threed.Bounds3D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
@@ -117,6 +118,12 @@ final class LineSpanningSubset3D extends 
LineConvexSubset3D {
         return null; // infinite; no bounds
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public RegionLocation classifyAbscissa(final double abscissa) {
+        return RegionLocation.INSIDE;
+    }
+
     /** {@inheritDoc} */
     @Override
     public LineSpanningSubset3D transform(final Transform<Vector3D> transform) 
{
@@ -133,10 +140,4 @@ final class LineSpanningSubset3D extends 
LineConvexSubset3D {
                 line.getOrigin(),
                 line.getDirection());
     }
-
-    /** {@inheritDoc} */
-    @Override
-    boolean containsAbscissa(final double abscissa) {
-        return true;
-    }
 }
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSubset3D.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSubset3D.java
index 2bea44e4..77da45e9 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSubset3D.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSubset3D.java
@@ -17,6 +17,7 @@
 package org.apache.commons.geometry.euclidean.threed.line;
 
 import org.apache.commons.geometry.core.RegionEmbedding;
+import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Sized;
 import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
 import org.apache.commons.geometry.euclidean.oned.Vector1D;
@@ -75,4 +76,10 @@ public abstract class LineSubset3D implements 
RegionEmbedding<Vector3D, Vector1D
      */
     @Override
     public abstract HyperplaneBoundedRegion<Vector1D> getSubspaceRegion();
+
+    /** Classify the given line abscissa value with respect to the subspace 
region.
+     * @param abscissa the abscissa value to classify
+     * @return the region location of the line abscissa value
+     */
+    public abstract RegionLocation classifyAbscissa(double abscissa);
 }
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LinecastPoint3D.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LinecastPoint3D.java
index 33ddac76..cc519531 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LinecastPoint3D.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LinecastPoint3D.java
@@ -44,7 +44,8 @@ public class LinecastPoint3D extends 
AbstractLinecastPoint<Vector3D, Vector3D.Un
         return cmp;
     };
 
-    /** Construct a new instance from its components.
+    /** Construct a new instance from its components. Callers are responsible 
for ensuring that
+     * {@code point} lies on the given line.
      * @param point intersection point
      * @param normal normal of the target boundary at the intersection point
      * @param line intersecting line
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Ray3D.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Ray3D.java
index 42df5849..469b8f19 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Ray3D.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Ray3D.java
@@ -16,6 +16,7 @@
  */
 package org.apache.commons.geometry.euclidean.threed.line;
 
+import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.euclidean.threed.Bounds3D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
@@ -133,6 +134,18 @@ public final class Ray3D extends LineConvexSubset3D {
         return getLine().getDirection();
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public RegionLocation classifyAbscissa(final double abscissa) {
+        final int cmp = getLine().getPrecision().compare(abscissa, start);
+        if (cmp < 0) {
+            return RegionLocation.OUTSIDE;
+        } else if (cmp == 0) {
+            return RegionLocation.BOUNDARY;
+        }
+        return RegionLocation.INSIDE;
+    }
+
     /** {@inheritDoc} */
     @Override
     public Ray3D transform(final Transform<Vector3D> transform) {
@@ -155,10 +168,4 @@ public final class Ray3D extends LineConvexSubset3D {
 
         return sb.toString();
     }
-
-    /** {@inheritDoc} */
-    @Override
-    boolean containsAbscissa(final double abscissa) {
-        return getLine().getPrecision().gte(abscissa, start);
-    }
 }
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3D.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3D.java
index 6e1e0ef0..3257b863 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3D.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3D.java
@@ -16,6 +16,7 @@
  */
 package org.apache.commons.geometry.euclidean.threed.line;
 
+import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.euclidean.threed.Bounds3D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
@@ -127,6 +128,18 @@ public final class ReverseRay3D extends LineConvexSubset3D 
{
         return null; // infinite; no bounds
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public RegionLocation classifyAbscissa(final double abscissa) {
+        final int cmp = getLine().getPrecision().compare(abscissa, end);
+        if (cmp > 0) {
+            return RegionLocation.OUTSIDE;
+        } else if (cmp == 0) {
+            return RegionLocation.BOUNDARY;
+        }
+        return RegionLocation.INSIDE;
+    }
+
     /** {@inheritDoc} */
     @Override
     public ReverseRay3D transform(final Transform<Vector3D> transform) {
@@ -149,10 +162,4 @@ public final class ReverseRay3D extends LineConvexSubset3D 
{
 
         return sb.toString();
     }
-
-    /** {@inheritDoc} */
-    @Override
-    boolean containsAbscissa(final double abscissa) {
-        return getLine().getPrecision().lte(abscissa, end);
-    }
 }
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Segment3D.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Segment3D.java
index e4695cfe..0cda311a 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Segment3D.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Segment3D.java
@@ -16,6 +16,7 @@
  */
 package org.apache.commons.geometry.euclidean.threed.line;
 
+import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.euclidean.threed.Bounds3D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
@@ -122,6 +123,26 @@ public final class Segment3D extends LineConvexSubset3D {
                 .build();
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public RegionLocation classifyAbscissa(final double abscissa) {
+        final Precision.DoubleEquivalence precision = getLine().getPrecision();
+        final int startCmp = precision.compare(abscissa, start);
+        if (startCmp < 0) {
+            return RegionLocation.OUTSIDE;
+        } else if (startCmp == 0) {
+            return RegionLocation.BOUNDARY;
+        } else {
+            final int endCmp = precision.compare(abscissa, end);
+            if (endCmp > 0) {
+                return RegionLocation.OUTSIDE;
+            } else if (endCmp == 0) {
+                return RegionLocation.BOUNDARY;
+            }
+            return RegionLocation.INSIDE;
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     public Segment3D transform(final Transform<Vector3D> transform) {
@@ -146,12 +167,4 @@ public final class Segment3D extends LineConvexSubset3D {
 
         return sb.toString();
     }
-
-    /** {@inheritDoc} */
-    @Override
-    boolean containsAbscissa(final double abscissa) {
-        final Precision.DoubleEquivalence precision = getLine().getPrecision();
-        return precision.gte(abscissa, start) &&
-                precision.lte(abscissa, end);
-    }
 }
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Bounds2D.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Bounds2D.java
index 3f30ab13..4b6cec5f 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Bounds2D.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Bounds2D.java
@@ -17,8 +17,11 @@
 package org.apache.commons.geometry.euclidean.twod;
 
 import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
 import java.util.Objects;
 
+import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.euclidean.AbstractBounds;
 import org.apache.commons.geometry.euclidean.twod.shape.Parallelogram;
 import org.apache.commons.numbers.core.Precision;
@@ -29,7 +32,8 @@ import org.apache.commons.numbers.core.Precision;
  *
  * <p>Instances of this class are guaranteed to be immutable.</p>
  */
-public final class Bounds2D extends AbstractBounds<Vector2D, Bounds2D> {
+public final class Bounds2D extends AbstractBounds<Vector2D, Bounds2D>
+    implements Linecastable2D {
 
     /** Simple constructor. Callers are responsible for ensuring the min is 
not greater than max.
      * @param min minimum point
@@ -112,6 +116,66 @@ public final class Bounds2D extends 
AbstractBounds<Vector2D, Bounds2D> {
         return null; // no intersection
     }
 
+    /** Return {@code true} if the region represented by this instance shares 
any points with
+     * the given line. Floating point comparisons are made using the
+     * {@link Line#getPrecision() precision} of the line.
+     * @param line line to determine intersection with
+     * @return {@code true} if the region represented by this instance 
intersects
+     *      the given line
+     */
+    public boolean intersects(final Line line) {
+        return intersects(line.span());
+    }
+
+    /** Return {@code true} if the region represented by this instance shares 
any points with
+     * the given line convex subset. Floating point comparisons are made using 
the
+     * {@link Line#getPrecision() precision} of the subset's line.
+     * @param subset line convex subset to determine intersection with
+     * @return {@code true} if the region represented by this instance 
intersects
+     *      the given line convex subset
+     */
+    public boolean intersects(final LineConvexSubset subset) {
+        return new BoundsLinecaster2D(subset).intersectsRegion();
+    }
+
+    /** Return a {@link Segment} representing the intersection of the region
+     * represented by this instance with the given line or {@code null} if no 
such
+     * intersection exists. Floating point comparisons are made using the
+     * {@link Line#getPrecision() precision} of the line.
+     * @param line line to intersect with
+     * @return {@link Segment} representing the intersection of the region
+     *      represented by this instance with the given line or {@code null}
+     *      if no such intersection exists
+     */
+    public Segment intersection(final Line line) {
+        return intersection(line.span());
+    }
+
+    /** Return a {@link Segment} representing the intersection of the region
+     * represented by this instance with the given line convex subset or 
{@code null}
+     * if no such intersection exists. Floating point comparisons are made 
using the
+     * {@link Line#getPrecision() precision} of the subset's line.
+     * @param subset line convex subset to intersect with
+     * @return {@link Segment} representing the intersection of the region
+     *      represented by this instance with the given line convex subset or 
{@code null}
+     *      if no such intersection exists
+     */
+    public Segment intersection(final LineConvexSubset subset) {
+        return new BoundsLinecaster2D(subset).getRegionIntersection();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public List<LinecastPoint2D> linecast(final LineConvexSubset subset) {
+        return new BoundsLinecaster2D(subset).getBoundaryIntersections();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public LinecastPoint2D linecastFirst(final LineConvexSubset subset) {
+        return new BoundsLinecaster2D(subset).getFirstBoundaryIntersection();
+    }
+
     /** {@inheritDoc}
      *
      * @throws IllegalArgumentException if any dimension of the bounding box 
is zero
@@ -268,4 +332,97 @@ public final class Bounds2D extends 
AbstractBounds<Vector2D, Bounds2D> {
             return new Bounds2D(min, max);
         }
     }
+
+    /** Subclass of {@link BoundsLinecaster} for 2D space.
+     */
+    private final class BoundsLinecaster2D extends BoundsLinecaster<Segment, 
LinecastPoint2D> {
+
+        /** Line convex subset to be tested against the bounds. */
+        private final LineConvexSubset subset;
+
+        /** Line instance for the subset being tested. */
+        private final Line line;
+
+         /** Construct a new instance for computing bounds intersection 
information with
+          * the given line convex subset.
+          * @param subset line convex subset to compute intersection 
information for
+          */
+        BoundsLinecaster2D(final LineConvexSubset subset) {
+            super(subset.getLine().getPrecision());
+
+            this.subset = subset;
+            this.line = subset.getLine();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected Segment createSegment(final double startAbscissa, final 
double endAbscissa) {
+            return line.segment(startAbscissa, endAbscissa);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected LinecastPoint2D createBoundaryIntersection(final Vector2D 
pt, final Vector2D normal) {
+            return new LinecastPoint2D(pt, normal, line);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected void addBoundaryIntersections(final double abscissa, final 
List<LinecastPoint2D> results) {
+            if (subset.classifyAbscissa(abscissa) != RegionLocation.OUTSIDE) {
+                final Vector2D pt = line.toSpace(abscissa);
+
+                addBoundaryIntersectionIfPresent(
+                        pt,
+                        Vector2D.Unit.MINUS_X,
+                        Vector2D.Unit.PLUS_X,
+                        Vector2D::getX,
+                        results);
+
+                addBoundaryIntersectionIfPresent(
+                        pt,
+                        Vector2D.Unit.MINUS_Y,
+                        Vector2D.Unit.PLUS_Y,
+                        Vector2D::getY,
+                        results);
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected boolean computeNearFar() {
+            return updateNearFar(Vector2D::getX) &&
+                    updateNearFar(Vector2D::getY);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected Comparator<LinecastPoint2D> 
getBoundaryIntersectionComparator() {
+            return LinecastPoint2D.ABSCISSA_ORDER;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected Vector2D getLineDir() {
+            return line.getDirection();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected Vector2D getLineOrigin() {
+            return line.getOrigin();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected double getSubspaceStart() {
+            return subset.getSubspaceStart();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected double getSubspaceEnd() {
+            return subset.getSubspaceEnd();
+        }
+    }
 }
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/EmbeddedTreeLineSubset.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/EmbeddedTreeLineSubset.java
index 8e94faf5..2539f174 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/EmbeddedTreeLineSubset.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/EmbeddedTreeLineSubset.java
@@ -156,6 +156,12 @@ public final class EmbeddedTreeLineSubset extends 
LineSubset {
         return region;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public RegionLocation classifyAbscissa(final double abscissa) {
+        return region.classify(abscissa);
+    }
+
     /** {@inheritDoc}
      *
      * <p>In all cases, the current instance is not modified. However, In 
order to avoid
@@ -243,10 +249,4 @@ public final class EmbeddedTreeLineSubset extends 
LineSubset {
 
         return sb.toString();
     }
-
-    /** {@inheritDoc} */
-    @Override
-    RegionLocation classifyAbscissa(final double abscissa) {
-        return region.classify(abscissa);
-    }
 }
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineConvexSubset.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineConvexSubset.java
index 11a8db16..408fd8ef 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineConvexSubset.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineConvexSubset.java
@@ -19,6 +19,7 @@ package org.apache.commons.geometry.euclidean.twod;
 import java.util.Collections;
 import java.util.List;
 
+import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
 import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
@@ -116,6 +117,12 @@ public abstract class LineConvexSubset extends LineSubset 
implements HyperplaneC
         return line.toSpace(closestAbscissa(abscissa));
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public RegionLocation classifyAbscissa(final double abscissa) {
+        return RegionLocation.INSIDE;
+    }
+
     /** {@inheritDoc} */
     @Override
     public abstract LineConvexSubset transform(Transform<Vector2D> transform);
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSpanningSubset.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSpanningSubset.java
index c99ed3cb..c0251fd6 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSpanningSubset.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSpanningSubset.java
@@ -18,7 +18,6 @@ package org.apache.commons.geometry.euclidean.twod;
 
 import java.text.MessageFormat;
 
-import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partitioning.Split;
 
@@ -149,12 +148,6 @@ final class LineSpanningSubset extends LineConvexSubset {
                 line.getDirection());
     }
 
-    /** {@inheritDoc} */
-    @Override
-    RegionLocation classifyAbscissa(final double abscissa) {
-        return RegionLocation.INSIDE;
-    }
-
     /** {@inheritDoc} */
     @Override
     double closestAbscissa(final double abscissa) {
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSubset.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSubset.java
index ef1e60fd..a56a4c0f 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSubset.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSubset.java
@@ -131,7 +131,7 @@ public abstract class LineSubset implements 
HyperplaneSubset<Vector2D>, RegionEm
      * @param abscissa the abscissa value to classify
      * @return the region location of the line abscissa value
      */
-    abstract RegionLocation classifyAbscissa(double abscissa);
+    public abstract RegionLocation classifyAbscissa(double abscissa);
 
     /** Get a split result for cases where no intersection exists between the 
splitting line and the
      * line underlying the given line subset. This occurs when the two lines 
are parallel or coincident.
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Ray.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Ray.java
index 3c97e44c..5a58cf56 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Ray.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Ray.java
@@ -149,6 +149,19 @@ public final class Ray extends LineConvexSubset {
         return new ReverseRay(getLine().reverse(), startPoint);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public RegionLocation classifyAbscissa(final double abscissa) {
+        final int cmp = getPrecision().compare(abscissa, getSubspaceStart());
+        if (cmp > 0) {
+            return RegionLocation.INSIDE;
+        } else if (cmp == 0) {
+            return RegionLocation.BOUNDARY;
+        }
+
+        return RegionLocation.OUTSIDE;
+    }
+
     /** {@inheritDoc} */
     @Override
     public String toString() {
@@ -163,19 +176,6 @@ public final class Ray extends LineConvexSubset {
         return sb.toString();
     }
 
-    /** {@inheritDoc} */
-    @Override
-    RegionLocation classifyAbscissa(final double abscissa) {
-        final int cmp = getPrecision().compare(abscissa, getSubspaceStart());
-        if (cmp > 0) {
-            return RegionLocation.INSIDE;
-        } else if (cmp == 0) {
-            return RegionLocation.BOUNDARY;
-        }
-
-        return RegionLocation.OUTSIDE;
-    }
-
     /** {@inheritDoc} */
     @Override
     double closestAbscissa(final double abscissa) {
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ReverseRay.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ReverseRay.java
index d36756fc..86080124 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ReverseRay.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ReverseRay.java
@@ -144,6 +144,19 @@ public final class ReverseRay extends LineConvexSubset {
         return new Ray(getLine().reverse(), endPoint);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public RegionLocation classifyAbscissa(final double abscissa) {
+        final int cmp = getPrecision().compare(abscissa, getSubspaceEnd());
+        if (cmp < 0) {
+            return RegionLocation.INSIDE;
+        } else if (cmp == 0) {
+            return RegionLocation.BOUNDARY;
+        }
+
+        return RegionLocation.OUTSIDE;
+    }
+
     /** {@inheritDoc} */
     @Override
     public String toString() {
@@ -158,19 +171,6 @@ public final class ReverseRay extends LineConvexSubset {
         return sb.toString();
     }
 
-    /** {@inheritDoc} */
-    @Override
-    RegionLocation classifyAbscissa(final double abscissa) {
-        final int cmp = getPrecision().compare(abscissa, getSubspaceEnd());
-        if (cmp < 0) {
-            return RegionLocation.INSIDE;
-        } else if (cmp == 0) {
-            return RegionLocation.BOUNDARY;
-        }
-
-        return RegionLocation.OUTSIDE;
-    }
-
     /** {@inheritDoc} */
     @Override
     double closestAbscissa(final double abscissa) {
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java
index 0be6054a..a93ea809 100644
--- 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java
@@ -141,21 +141,7 @@ public final class Segment extends LineConvexSubset {
 
     /** {@inheritDoc} */
     @Override
-    public String toString() {
-        final StringBuilder sb = new StringBuilder();
-        sb.append(getClass().getSimpleName())
-            .append("[startPoint= ")
-            .append(getStartPoint())
-            .append(", endPoint= ")
-            .append(getEndPoint())
-            .append(']');
-
-        return sb.toString();
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    RegionLocation classifyAbscissa(final double abscissa) {
+    public RegionLocation classifyAbscissa(final double abscissa) {
         final Precision.DoubleEquivalence precision = getPrecision();
         final int startCmp = precision.compare(abscissa, getSubspaceStart());
         if (startCmp > 0) {
@@ -172,6 +158,20 @@ public final class Segment extends LineConvexSubset {
         return RegionLocation.OUTSIDE;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName())
+            .append("[startPoint= ")
+            .append(getStartPoint())
+            .append(", endPoint= ")
+            .append(getEndPoint())
+            .append(']');
+
+        return sb.toString();
+    }
+
     /** {@inheritDoc} */
     @Override
     double closestAbscissa(final double abscissa) {
diff --git 
a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Bounds3DTest.java
 
b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Bounds3DTest.java
index aada5a4e..d753dcec 100644
--- 
a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Bounds3DTest.java
+++ 
b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Bounds3DTest.java
@@ -18,14 +18,25 @@ package org.apache.commons.geometry.euclidean.threed;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
 import java.util.function.BiFunction;
 import java.util.function.ToDoubleFunction;
 import java.util.regex.Pattern;
 
 import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.threed.line.Line3D;
+import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
+import org.apache.commons.geometry.euclidean.threed.line.LinecastPoint3D;
+import org.apache.commons.geometry.euclidean.threed.line.Lines3D;
+import org.apache.commons.geometry.euclidean.threed.line.Segment3D;
+import 
org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
 import org.apache.commons.geometry.euclidean.threed.shape.Parallelepiped;
+import org.apache.commons.numbers.angle.Angle;
 import org.apache.commons.numbers.core.Precision;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -410,6 +421,498 @@ class Bounds3DTest {
         Assertions.assertFalse(b4.eq(b1, high));
     }
 
+    @Test
+    void testLinecast_intersectsFace() {
+        // -- arrange
+        // use unequal face sizes so that our test lines do not end up passing 
through
+        // a vertex on the opposite side
+        final Bounds3D bounds = Bounds3D.from(Vector3D.of(-0.9, -2, -3), 
Vector3D.of(0.9, 2, 3));
+
+        // -- act/assert
+        checkLinecastIntersectingFace(bounds, Vector3D.of(0.9, 0, 0), 
Vector3D.Unit.PLUS_X);
+        checkLinecastIntersectingFace(bounds, Vector3D.of(-0.9, 0, 0), 
Vector3D.Unit.MINUS_X);
+
+        checkLinecastIntersectingFace(bounds, Vector3D.of(0, 2, 0), 
Vector3D.Unit.PLUS_Y);
+        checkLinecastIntersectingFace(bounds, Vector3D.of(0, -2, 0), 
Vector3D.Unit.MINUS_Y);
+
+        checkLinecastIntersectingFace(bounds, Vector3D.of(0, 0, 3), 
Vector3D.Unit.PLUS_Z);
+        checkLinecastIntersectingFace(bounds, Vector3D.of(0, 0, -3), 
Vector3D.Unit.MINUS_Z);
+    }
+
+    private void checkLinecastIntersectingFace(
+            final Bounds3D bounds,
+            final Vector3D facePt,
+            final Vector3D normal) {
+
+        // -- arrange
+        final Vector3D offset = normal.multiply(1.2);
+        final Parallelepiped region = bounds.toRegion(TEST_PRECISION);
+
+        EuclideanTestUtils.permute(-1, 1, 0.5, (x, y, z) -> {
+            final Vector3D otherPt = facePt
+                    .add(Vector3D.of(x, y, z))
+                    .add(offset);
+            final Line3D line = Lines3D.fromPoints(otherPt, facePt, 
TEST_PRECISION);
+
+            final LinecastPoint3D reversePt = 
region.linecastFirst(line.reverse());
+
+            // -- act/assert
+            linecastChecker(bounds)
+                .expect(facePt, normal)
+                .and(reversePt.getPoint(), reversePt.getNormal())
+                .whenGiven(line);
+
+            linecastChecker(bounds)
+                .and(reversePt.getPoint(), reversePt.getNormal())
+                .expect(facePt, normal)
+                .whenGiven(line.reverse());
+        });
+    }
+
+    @Test
+    void testLinecast_intersectsSingleVertex() {
+        // -- arrange
+        final Bounds3D bounds = Bounds3D.from(Vector3D.ZERO, Vector3D.of(1, 1, 
1));
+
+        // -- act/assert
+        checkLinecastIntersectingSingleVertex(bounds, Vector3D.ZERO);
+        checkLinecastIntersectingSingleVertex(bounds, Vector3D.of(0, 0, 1));
+        checkLinecastIntersectingSingleVertex(bounds, Vector3D.of(0, 1, 0));
+        checkLinecastIntersectingSingleVertex(bounds, Vector3D.of(0, 1, 1));
+        checkLinecastIntersectingSingleVertex(bounds, Vector3D.of(1, 0, 0));
+        checkLinecastIntersectingSingleVertex(bounds, Vector3D.of(1, 0, 1));
+        checkLinecastIntersectingSingleVertex(bounds, Vector3D.of(1, 1, 0));
+        checkLinecastIntersectingSingleVertex(bounds, Vector3D.of(1, 1, 1));
+    }
+
+    private void checkLinecastIntersectingSingleVertex(
+            final Bounds3D bounds,
+            final Vector3D vertex) {
+
+        // -- arrange
+        final Vector3D centerToVertex = 
vertex.subtract(bounds.getCentroid()).normalize();
+        final Vector3D baseLineDir = centerToVertex.orthogonal();
+
+        final int runCnt = 10;
+        for (double a = 0; a < Angle.TWO_PI; a += Angle.TWO_PI / runCnt) {
+
+            // construct a line orthogonal to the vector from the bounds 
center to the vertex and passing
+            // through the vertex
+            final Vector3D lineDir = 
QuaternionRotation.fromAxisAngle(centerToVertex, a).apply(baseLineDir);
+            final Line3D line = Lines3D.fromPointAndDirection(vertex, lineDir, 
TEST_PRECISION);
+
+            // construct possible normals for this vertex
+            final List<Vector3D> normals = new ArrayList<>();
+            
normals.add(centerToVertex.project(Vector3D.Unit.PLUS_X).normalize());
+            
normals.add(centerToVertex.project(Vector3D.Unit.PLUS_Y).normalize());
+            
normals.add(centerToVertex.project(Vector3D.Unit.PLUS_Z).normalize());
+
+            normals.sort(Vector3D.COORDINATE_ASCENDING_ORDER);
+
+            // create the checker and populate it with the normals of faces 
that are not parallel to
+            // the line
+            final BoundsLinecastChecker3D checker = linecastChecker(bounds);
+            for (final Vector3D normal : normals) {
+                if (!TEST_PRECISION.eqZero(normal.dot(lineDir))) {
+                    checker.expect(vertex, normal);
+                }
+            }
+
+            // -- act/assert
+            checker.whenGiven(line)
+                .whenGiven(line.reverse());
+        }
+    }
+
+    @Test
+    void testLinecast_vertexToVertex() {
+        // -- arrange
+        final Vector3D min = Vector3D.ZERO;
+        final Vector3D max = Vector3D.of(1, 1, 1);
+
+        final Bounds3D bounds = Bounds3D.from(min, max);
+        final Line3D line = Lines3D.fromPoints(min, max, TEST_PRECISION);
+
+        // -- act/assert
+        linecastChecker(bounds)
+            .expect(min, Vector3D.Unit.MINUS_X)
+            .and(min, Vector3D.Unit.MINUS_Y)
+            .and(min, Vector3D.Unit.MINUS_Z)
+            .and(max, Vector3D.Unit.PLUS_Z)
+            .and(max, Vector3D.Unit.PLUS_Y)
+            .and(max, Vector3D.Unit.PLUS_X)
+            .whenGiven(line);
+    }
+
+    @Test
+    void testLinecast_edgeToEdge() {
+        // -- arrange
+        final Vector3D min = Vector3D.of(0, 0, 0);
+        final Vector3D max = Vector3D.of(1, 1, 1);
+
+        final Bounds3D bounds = Bounds3D.from(min, max);
+
+        final Vector3D start = Vector3D.of(0, 0, 0.5);
+        final Vector3D end = Vector3D.of(1, 1, 0.5);
+
+        final Line3D line = Lines3D.fromPoints(start, end, TEST_PRECISION);
+
+        // -- act/assert
+        linecastChecker(bounds)
+            .expect(start, Vector3D.Unit.MINUS_X)
+            .and(start, Vector3D.Unit.MINUS_Y)
+            .and(end, Vector3D.Unit.PLUS_Y)
+            .and(end, Vector3D.Unit.PLUS_X)
+            .whenGiven(line);
+    }
+
+    @Test
+    void testLinecast_alongFace() {
+        // -- arrange
+        final Vector3D min = Vector3D.ZERO;
+        final Vector3D max = Vector3D.of(1, 1, 1);
+
+        final Bounds3D bounds = Bounds3D.from(min, max);
+
+        final int cnt = 10;
+        for (double x = min.getX();
+                x <= max.getX();
+                x += bounds.getDiagonal().getX() / cnt) {
+
+            final Vector3D start = Vector3D.of(x, min.getY(), max.getZ());
+            final Vector3D end = Vector3D.of(x, max.getY(), max.getZ());
+
+            final Line3D line = Lines3D.fromPoints(start, end, TEST_PRECISION);
+
+            // -- act/assert
+            linecastChecker(bounds)
+                .expect(start, Vector3D.Unit.MINUS_Y)
+                .and(end, Vector3D.Unit.PLUS_Y)
+                .whenGiven(line);
+        }
+    }
+
+    @Test
+    void testLinecast_noIntersection() {
+        // -- arrange
+        final Bounds3D bounds = Bounds3D.from(Vector3D.ZERO, Vector3D.of(1, 1, 
1));
+
+        // -- act/assert
+        checkLinecastNoIntersection(bounds, Vector3D.ZERO);
+        checkLinecastNoIntersection(bounds, Vector3D.of(0, 0, 1));
+        checkLinecastNoIntersection(bounds, Vector3D.of(0, 1, 0));
+        checkLinecastNoIntersection(bounds, Vector3D.of(0, 1, 1));
+        checkLinecastNoIntersection(bounds, Vector3D.of(1, 0, 0));
+        checkLinecastNoIntersection(bounds, Vector3D.of(1, 0, 1));
+        checkLinecastNoIntersection(bounds, Vector3D.of(1, 1, 0));
+        checkLinecastNoIntersection(bounds, Vector3D.of(1, 1, 1));
+    }
+
+    private void checkLinecastNoIntersection(
+            final Bounds3D bounds,
+            final Vector3D vertex) {
+
+        // -- arrange
+        final Vector3D toVertex = bounds.getCentroid().directionTo(vertex);
+        final Vector3D baseLineDir = toVertex.orthogonal();
+
+        final Vector3D offsetVertex = vertex.add(toVertex);
+
+        final Line3D plusXLine = Lines3D.fromPointAndDirection(offsetVertex, 
Vector3D.Unit.PLUS_X, TEST_PRECISION);
+        final Line3D plusYLine = Lines3D.fromPointAndDirection(offsetVertex, 
Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+        final Line3D plusZLine = Lines3D.fromPointAndDirection(offsetVertex, 
Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+        final BoundsLinecastChecker3D emptyChecker = linecastChecker(bounds)
+                .expectNothing();
+
+        // -- act/assert
+        // check axis-aligned lines
+        emptyChecker
+            .whenGiven(plusXLine)
+            .whenGiven(plusYLine)
+            .whenGiven(plusZLine);
+
+        // check lines orthogonal to the axis
+        final int runCnt = 10;
+        for (double a = 0; a < Angle.TWO_PI; a += Angle.TWO_PI / runCnt) {
+            final Vector3D lineDir = 
QuaternionRotation.fromAxisAngle(toVertex, a).apply(baseLineDir);
+            final Line3D line = Lines3D.fromPointAndDirection(offsetVertex, 
lineDir, TEST_PRECISION);
+
+            emptyChecker.whenGiven(line);
+        }
+    }
+
+    @Test
+    void testLinecast_nonSpan() {
+        // -- arrange
+        final Vector3D min = Vector3D.ZERO;
+        final Vector3D max = Vector3D.of(1, 1, 1);
+
+        final Bounds3D bounds = Bounds3D.from(min, max);
+
+        final Vector3D centroid = bounds.getCentroid();
+
+        final Vector3D start = Vector3D.of(max.getX(), centroid.getY(), 
centroid.getZ());
+        final Vector3D end = Vector3D.of(min.getX(), centroid.getY(), 
centroid.getZ());
+
+        final Line3D line = Lines3D.fromPoints(start, end, TEST_PRECISION);
+
+        // -- act/assert
+        linecastChecker(bounds)
+            .expect(end, Vector3D.Unit.MINUS_X)
+            .whenGiven(line.rayFrom(-0.5));
+
+        linecastChecker(bounds)
+            .expect(start, Vector3D.Unit.PLUS_X)
+            .whenGiven(line.reverseRayTo(-0.5));
+
+        linecastChecker(bounds)
+            .expectNothing()
+            .whenGiven(line.segment(-0.9, -0.1));
+
+        linecastChecker(bounds)
+            .expect(end, Vector3D.Unit.MINUS_X)
+            .whenGiven(line.segment(-0.9, 0.1));
+
+        linecastChecker(bounds)
+            .expect(start, Vector3D.Unit.PLUS_X)
+            .whenGiven(line.segment(-1.1, -0.1));
+
+        linecastChecker(bounds)
+            .expect(start, Vector3D.Unit.PLUS_X)
+            .expect(end, Vector3D.Unit.MINUS_X)
+            .whenGiven(line.segment(-1.1, 0.1));
+    }
+
+    @Test
+    void testLinecast_subsetEndpointOnBounds() {
+        // -- arrange
+        final Vector3D min = Vector3D.ZERO;
+        final Vector3D max = Vector3D.of(1, 1, 1);
+
+        final Bounds3D bounds = Bounds3D.from(min, max);
+
+        final Vector3D centroid = bounds.getCentroid();
+
+        final Vector3D start = Vector3D.of(max.getX(), centroid.getY(), 
centroid.getZ());
+        final Vector3D end = Vector3D.of(min.getX(), centroid.getY(), 
centroid.getZ());
+
+        final Line3D line = Lines3D.fromPoints(start, end, TEST_PRECISION);
+
+        // -- act/assert
+        linecastChecker(bounds)
+            .expect(end, Vector3D.Unit.MINUS_X)
+            .whenGiven(line.rayFrom(0));
+
+        linecastChecker(bounds)
+            .expect(end, Vector3D.Unit.MINUS_X)
+            .whenGiven(line.segment(0, 1));
+
+        linecastChecker(bounds)
+            .expect(start, Vector3D.Unit.PLUS_X)
+            .whenGiven(line.reverseRayTo(-1));
+
+        linecastChecker(bounds)
+            .expect(start, Vector3D.Unit.PLUS_X)
+            .whenGiven(line.segment(-2, -1));
+    }
+
+    @Test
+    void testLinecast_usesLinePrecision() {
+        // -- arrange
+        final double withinEps = 0.9 * TEST_EPS;
+        final double outsideEps = 1.1 * TEST_EPS;
+
+        final Vector3D min = Vector3D.ZERO;
+        final Vector3D max = Vector3D.of(1, 1, 1);
+
+        final Bounds3D bounds = Bounds3D.from(min, max);
+
+        final Vector3D centroid = bounds.getCentroid();
+
+        final Vector3D centerStart = Vector3D.of(max.getX(), centroid.getY(), 
centroid.getZ());
+        final Vector3D centerEnd = Vector3D.of(min.getX(), centroid.getY(), 
centroid.getZ());
+
+        final Line3D centerLine = Lines3D.fromPoints(centerStart, centerEnd, 
TEST_PRECISION);
+
+        final Vector3D faceStart = Vector3D.of(max.getX() + withinEps, 
max.getY() + withinEps, min.getZ());
+        final Vector3D faceEnd = Vector3D.of(max.getX() + withinEps, 
max.getY() + withinEps, max.getZ());
+
+        final Line3D faceLine = Lines3D.fromPoints(faceStart, faceEnd, 
TEST_PRECISION);
+
+        // -- act/assert
+        linecastChecker(bounds)
+            .expect(centerEnd, Vector3D.Unit.MINUS_X)
+            .whenGiven(centerLine.rayFrom(withinEps));
+
+        linecastChecker(bounds)
+            .expectNothing()
+            .whenGiven(centerLine.rayFrom(outsideEps));
+
+        linecastChecker(bounds)
+            .expect(centerStart, Vector3D.Unit.PLUS_X)
+            .expect(centerEnd, Vector3D.Unit.MINUS_X)
+            .whenGiven(centerLine.segment(-1 + withinEps, -withinEps));
+
+        linecastChecker(bounds)
+            .expectNothing()
+            .whenGiven(centerLine.segment(-1 + outsideEps, -outsideEps));
+
+        linecastChecker(bounds)
+            .expect(faceStart, Vector3D.Unit.MINUS_Z)
+            .expect(faceEnd, Vector3D.Unit.PLUS_Z)
+            .whenGiven(faceLine.segment(withinEps, 1 - withinEps));
+
+        linecastChecker(bounds)
+            .expectNothing()
+            .whenGiven(faceLine.segment(outsideEps, 1 - outsideEps));
+    }
+
+    @Test
+    void testLinecast_boundsHasNoSize() {
+        // -- arrange
+        final Vector3D pt = Vector3D.of(1, 2, 3);
+
+        final Bounds3D bounds = Bounds3D.from(pt, pt);
+
+        final Line3D diagonalLine = Lines3D.fromPointAndDirection(pt, 
Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+        final Line3D plusXLine = Lines3D.fromPointAndDirection(pt, 
Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+        // -- act/assert
+        linecastChecker(bounds)
+            .expect(pt, Vector3D.Unit.MINUS_X)
+            .expect(pt, Vector3D.Unit.MINUS_Y)
+            .expect(pt, Vector3D.Unit.MINUS_Z)
+            .expect(pt, Vector3D.Unit.PLUS_Z)
+            .expect(pt, Vector3D.Unit.PLUS_Y)
+            .expect(pt, Vector3D.Unit.PLUS_X)
+            .whenGiven(diagonalLine);
+
+        linecastChecker(bounds)
+            .expect(pt, Vector3D.Unit.MINUS_X)
+            .expect(pt, Vector3D.Unit.PLUS_X)
+            .whenGiven(plusXLine);
+    }
+
+    @Test
+    void testLineIntersection() {
+        // -- arrange
+        final Vector3D min = Vector3D.ZERO;
+        final Vector3D max = Vector3D.of(1, 1, 1);
+
+        final Vector3D insideMin = Vector3D.of(0.1, 0.1, 0.1);
+        final Vector3D insideMax = Vector3D.of(0.9, 0.9, 0.9);
+
+        final Vector3D outsideMin = Vector3D.of(-0.1, -0.1, -0.1);
+        final Vector3D outsideMax = Vector3D.of(1.1, 1.1, 1.1);
+
+        final Bounds3D bounds = Bounds3D.from(min, max);
+
+        final Line3D diagonal = Lines3D.fromPoints(min, max, TEST_PRECISION);
+
+        // -- act/assert
+        assertLineIntersection(bounds, diagonal, min, max);
+
+        assertLineIntersection(bounds, diagonal.segment(outsideMin, 
outsideMax), min, max);
+        assertLineIntersection(bounds, diagonal.segment(outsideMin, 
insideMax), min, insideMax);
+        assertLineIntersection(bounds, diagonal.segment(insideMin, 
outsideMax), insideMin, max);
+        assertLineIntersection(bounds, diagonal.segment(insideMin, insideMax), 
insideMin, insideMax);
+
+        assertLineIntersection(bounds, diagonal.rayFrom(min), min, max);
+        assertLineIntersection(bounds, diagonal.reverseRayTo(min), min, min);
+
+        assertLineIntersection(bounds, diagonal.rayFrom(max), max, max);
+        assertLineIntersection(bounds, diagonal.reverseRayTo(max), min, max);
+
+        assertLineIntersection(bounds, diagonal.rayFrom(insideMax), insideMax, 
max);
+        assertLineIntersection(bounds, diagonal.reverseRayTo(insideMax), min, 
insideMax);
+    }
+
+    @Test
+    void testLineIntersection_noIntersection() {
+        // -- arrange
+        final Bounds3D bounds = Bounds3D.from(Vector3D.ZERO, Vector3D.of(1, 1, 
1));
+
+        final Line3D plusXLine =
+                Lines3D.fromPointAndDirection(bounds.getCentroid(), 
Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+        // -- act/assert
+        checkLineNoIntersection(bounds, Vector3D.ZERO);
+        checkLineNoIntersection(bounds, Vector3D.of(0, 0, 1));
+        checkLineNoIntersection(bounds, Vector3D.of(0, 1, 0));
+        checkLineNoIntersection(bounds, Vector3D.of(0, 1, 1));
+        checkLineNoIntersection(bounds, Vector3D.of(1, 0, 0));
+        checkLineNoIntersection(bounds, Vector3D.of(1, 0, 1));
+        checkLineNoIntersection(bounds, Vector3D.of(1, 1, 0));
+        checkLineNoIntersection(bounds, Vector3D.of(1, 1, 1));
+
+        assertNoLineIntersection(bounds, plusXLine.segment(-0.2, -0.1));
+        assertNoLineIntersection(bounds, plusXLine.reverseRayTo(-0.1));
+
+        assertNoLineIntersection(bounds, plusXLine.segment(1.1, 1.2));
+        assertNoLineIntersection(bounds, plusXLine.rayFrom(1.1));
+    }
+
+    private void checkLineNoIntersection(
+            final Bounds3D bounds,
+            final Vector3D vertex) {
+
+        // -- arrange
+        final Vector3D toVertex = bounds.getCentroid().directionTo(vertex);
+        final Vector3D baseLineDir = toVertex.orthogonal();
+
+        final Vector3D offsetVertex = vertex.add(toVertex);
+
+        final Line3D plusXLine = Lines3D.fromPointAndDirection(offsetVertex, 
Vector3D.Unit.PLUS_X, TEST_PRECISION);
+        final Line3D plusYLine = Lines3D.fromPointAndDirection(offsetVertex, 
Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+        final Line3D plusZLine = Lines3D.fromPointAndDirection(offsetVertex, 
Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+        // -- act/assert
+        // check axis-aligned lines
+        assertNoLineIntersection(bounds, plusXLine);
+        assertNoLineIntersection(bounds, plusYLine);
+        assertNoLineIntersection(bounds, plusZLine);
+
+        // check lines orthogonal to the axis
+        final int runCnt = 10;
+        for (double a = 0; a < Angle.TWO_PI; a += Angle.TWO_PI / runCnt) {
+            final Vector3D lineDir = 
QuaternionRotation.fromAxisAngle(toVertex, a).apply(baseLineDir);
+            final Line3D line = Lines3D.fromPointAndDirection(offsetVertex, 
lineDir, TEST_PRECISION);
+
+            assertNoLineIntersection(bounds, line);
+        }
+    }
+
+    @Test
+    void testLineIntersection_boundsHasNoSize() {
+        // -- arrange
+        final Vector3D pt = Vector3D.of(1, 2, 3);
+
+        final Bounds3D bounds = Bounds3D.from(pt, pt);
+
+        final Line3D plusXLine = Lines3D.fromPointAndDirection(pt, 
Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+        // -- act/assert
+        assertLineIntersection(bounds, plusXLine, pt, pt);
+        assertLineIntersection(bounds, plusXLine.rayFrom(pt), pt, pt);
+    }
+
+    @Test
+    void testLineIntersection_lineAlmostParallel() {
+        // -- arrange
+        final Vector3D min = Vector3D.of(1e150, -1, -1);
+        final Vector3D max = Vector3D.of(1.1e150, 1, 1);
+
+        final Bounds3D bounds = Bounds3D.from(min, max);
+
+        final Vector3D lineDir = Vector3D.of(1, -5e-11, 0);
+        final Line3D line = Lines3D.fromPointAndDirection(Vector3D.ZERO, 
lineDir, TEST_PRECISION);
+
+        // -- act
+        assertNoLineIntersection(bounds, line);
+    }
+
     @Test
     void testHashCode() {
         // arrange
@@ -519,15 +1022,204 @@ class Bounds3DTest {
         EuclideanTestUtils.assertCoordinatesEqual(max, b.getMax(), TEST_EPS);
     }
 
-    private static void assertContainsStrict(final Bounds3D bounds, final 
boolean contains, final Vector3D... pts) {
+    private static void assertContainsStrict(
+            final Bounds3D bounds,
+            final boolean contains,
+            final Vector3D... pts) {
         for (final Vector3D pt : pts) {
             Assertions.assertEquals(contains, bounds.contains(pt), "Unexpected 
location for point " + pt);
         }
     }
 
-    private static void assertContainsWithPrecision(final Bounds3D bounds, 
final boolean contains, final Vector3D... pts) {
+    private static void assertContainsWithPrecision(
+            final Bounds3D bounds,
+            final boolean contains,
+            final Vector3D... pts) {
         for (final Vector3D pt : pts) {
             Assertions.assertEquals(contains, bounds.contains(pt, 
TEST_PRECISION), "Unexpected location for point " + pt);
         }
     }
+
+    private static void assertLineIntersection(
+            final Bounds3D bounds,
+            final Line3D line,
+            final Vector3D start,
+            final Vector3D end) {
+        final Segment3D segment = bounds.intersection(line);
+
+        Assertions.assertSame(line, segment.getLine());
+        assertSegment(segment, start, end);
+
+        Assertions.assertTrue(bounds.intersects(line));
+    }
+
+    private static void assertLineIntersection(
+            final Bounds3D bounds,
+            final LineConvexSubset3D subset,
+            final Vector3D start,
+            final Vector3D end) {
+        final Segment3D segment = bounds.intersection(subset);
+
+        Assertions.assertSame(subset.getLine(), segment.getLine());
+        assertSegment(segment, start, end);
+
+        Assertions.assertTrue(bounds.intersects(subset));
+    }
+
+    private static void assertNoLineIntersection(
+            final Bounds3D bounds,
+            final Line3D line) {
+        Assertions.assertNull(bounds.intersection(line));
+        Assertions.assertFalse(bounds.intersects(line));
+    }
+
+    private static void assertNoLineIntersection(
+            final Bounds3D bounds,
+            final LineConvexSubset3D subset) {
+        Assertions.assertNull(bounds.intersection(subset));
+        Assertions.assertFalse(bounds.intersects(subset));
+    }
+
+    private static void assertSegment(final Segment3D segment, final Vector3D 
start, final Vector3D end) {
+        EuclideanTestUtils.assertCoordinatesEqual(start, 
segment.getStartPoint(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(end, segment.getEndPoint(), 
TEST_EPS);
+    }
+
+    private static BoundsLinecastChecker3D linecastChecker(final Bounds3D 
bounds) {
+        return new BoundsLinecastChecker3D(bounds);
+    }
+
+    /**
+     * Internal test class used to perform and verify linecast operations.
+     */
+    private static final class BoundsLinecastChecker3D {
+
+        private final Bounds3D bounds;
+
+        private final Parallelepiped region;
+
+        private final LinecastChecker3D checker;
+
+        BoundsLinecastChecker3D(final Bounds3D bounds) {
+            this.bounds = bounds;
+            this.region = bounds.hasSize(TEST_PRECISION) ?
+                    bounds.toRegion(TEST_PRECISION) :
+                    null;
+            this.checker = LinecastChecker3D.with(bounds);
+        }
+
+        public BoundsLinecastChecker3D expectNothing() {
+            checker.expectNothing();
+            return this;
+        }
+
+        public BoundsLinecastChecker3D expect(final Vector3D pt, final 
Vector3D normal) {
+            checker.expect(pt, normal);
+            return this;
+        }
+
+        public BoundsLinecastChecker3D and(final Vector3D pt, final Vector3D 
normal) {
+            return expect(pt, normal);
+        }
+
+        public BoundsLinecastChecker3D whenGiven(final Line3D line) {
+            // perform the standard checks
+            checker.whenGiven(line);
+
+            // check that the returned points are equivalent to those returned 
by linecasting against
+            // the region
+            final List<LinecastPoint3D> boundsResults = bounds.linecast(line);
+
+            if (region != null) {
+                assertLinecastElements(region.linecast(line), 
bounds.linecast(line));
+            }
+
+            // check consistency with the intersects method; having linecast 
results guarantees
+            // that we intersect the bounds but not vice versa
+            if (!boundsResults.isEmpty()) {
+                Assertions.assertTrue(bounds.intersects(line),
+                        () -> "Linecast result is inconsistent with intersects 
method: line= " + line);
+
+                assertLinecastResultsConsistentWithSegment(boundsResults, 
bounds.intersection(line));
+            }
+
+            return this;
+        }
+
+        public BoundsLinecastChecker3D whenGiven(final LineConvexSubset3D 
subset) {
+            // perform the standard checks
+            checker.whenGiven(subset);
+
+            // check that the returned points are equivalent to those returned 
by linecasting against
+            // the region
+            final List<LinecastPoint3D> boundsResults = 
bounds.linecast(subset);
+
+            if (region != null) {
+                assertLinecastElements(region.linecast(subset), boundsResults);
+            }
+
+            // check consistency with the intersects methods; having linecast 
results guarantees
+            // that we intersect the bounds but not vice versa
+            if (!boundsResults.isEmpty()) {
+                Assertions.assertTrue(bounds.intersects(subset),
+                        () -> "Linecast result is inconsistent with intersects 
method: line subset= " + subset);
+
+                assertLinecastResultsConsistentWithSegment(boundsResults, 
bounds.intersection(subset));
+            }
+
+            return this;
+        }
+
+        /** Assert that the two collections contain the same linecast points 
and that the elements
+         * of {@code actual} are arranged in ascending abscissa order. Note 
that this does <em>not</em>
+         * assert that {@code expected} and {@code actual} have the same exact 
ordering, since the
+         * specific ordering if sensitive to floating point errors.
+         * @param expected expected collection
+         * @param actual actual collection
+         */
+        private void assertLinecastElements(
+                final Collection<LinecastPoint3D> expected,
+                final Collection<LinecastPoint3D> actual) {
+            Assertions.assertEquals(expected.size(), actual.size(), 
"Unexpected list size");
+
+            // create a sorted copy
+            final List<LinecastPoint3D> sortedList = new ArrayList<>(actual);
+            sortedList.sort(LinecastPoint3D.ABSCISSA_ORDER);
+
+            // check element membership
+            for (final LinecastPoint3D expectedPt : expected) {
+                final Iterator<LinecastPoint3D> sortedIt = 
sortedList.iterator();
+
+                boolean found = false;
+                while (sortedIt.hasNext()) {
+                    if (expectedPt.eq(sortedIt.next(), TEST_PRECISION)) {
+                        found = true;
+                        break;
+                    }
+                }
+
+                if (!found) {
+                    Assertions.fail("Missing expected linecast point " + 
expectedPt);
+                }
+            }
+
+            // check the order
+            Assertions.assertEquals(sortedList, actual);
+        }
+
+        /** Assert that the linecast results are consistent with the given 
segment, which is taken
+         * to be the intersection of a line or line convex subset with the 
bounding box.
+         * @param linecastResults
+         * @param segment
+         */
+        private void assertLinecastResultsConsistentWithSegment(
+                final List<LinecastPoint3D> linecastResults,
+                final Segment3D segment) {
+
+            for (final LinecastPoint3D pt : linecastResults) {
+                Assertions.assertEquals(RegionLocation.BOUNDARY, 
segment.classifyAbscissa(pt.getAbscissa()),
+                        () -> "Expected linecast point to lie on segment 
boundary");
+            }
+        }
+    }
 }
diff --git 
a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3DTest.java
 
b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3DTest.java
index 4e99598b..d0ef401a 100644
--- 
a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3DTest.java
+++ 
b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3DTest.java
@@ -19,6 +19,7 @@ package org.apache.commons.geometry.euclidean.threed.line;
 import java.util.List;
 
 import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
 import org.apache.commons.geometry.euclidean.oned.Interval;
@@ -114,6 +115,8 @@ class EmbeddedTreeLineSubset3DTest {
         Assertions.assertEquals(0, empty.getSize(), TEST_EPS);
         Assertions.assertNull(empty.getCentroid());
         Assertions.assertNull(empty.getBounds());
+
+        Assertions.assertEquals(RegionLocation.OUTSIDE, 
empty.classifyAbscissa(0));
     }
 
     @Test
@@ -129,6 +132,10 @@ class EmbeddedTreeLineSubset3DTest {
         GeometryTestUtils.assertPositiveInfinity(half.getSize());
         Assertions.assertNull(half.getCentroid());
         Assertions.assertNull(half.getBounds());
+
+        Assertions.assertEquals(RegionLocation.OUTSIDE, 
half.classifyAbscissa(0));
+        Assertions.assertEquals(RegionLocation.BOUNDARY, 
half.classifyAbscissa(1));
+        Assertions.assertEquals(RegionLocation.INSIDE, 
half.classifyAbscissa(2));
     }
 
     @Test
@@ -151,6 +158,18 @@ class EmbeddedTreeLineSubset3DTest {
         final Bounds3D bounds = sub.getBounds();
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-2, -2, 1), 
bounds.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 1), 
bounds.getMax(), TEST_EPS);
+
+        Assertions.assertEquals(RegionLocation.OUTSIDE, 
sub.classifyAbscissa(-10));
+        Assertions.assertEquals(RegionLocation.BOUNDARY, 
sub.classifyAbscissa(-2 * sqrt2));
+        Assertions.assertEquals(RegionLocation.INSIDE, 
sub.classifyAbscissa(-1.5 * sqrt2));
+        Assertions.assertEquals(RegionLocation.BOUNDARY, 
sub.classifyAbscissa(-sqrt2));
+        Assertions.assertEquals(RegionLocation.OUTSIDE, 
sub.classifyAbscissa(-1.1));
+
+        Assertions.assertEquals(RegionLocation.OUTSIDE, 
sub.classifyAbscissa(-0.1));
+        Assertions.assertEquals(RegionLocation.BOUNDARY, 
sub.classifyAbscissa(0));
+        Assertions.assertEquals(RegionLocation.INSIDE, 
sub.classifyAbscissa(0.1));
+        Assertions.assertEquals(RegionLocation.BOUNDARY, 
sub.classifyAbscissa(sqrt2));
+        Assertions.assertEquals(RegionLocation.OUTSIDE, 
sub.classifyAbscissa(2));
     }
 
     @Test
diff --git 
a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Bounds2DTest.java
 
b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Bounds2DTest.java
index 17cf4979..7cac2b3d 100644
--- 
a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Bounds2DTest.java
+++ 
b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Bounds2DTest.java
@@ -18,13 +18,18 @@ package org.apache.commons.geometry.euclidean.twod;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
 import java.util.function.BiFunction;
 import java.util.function.ToDoubleFunction;
 import java.util.regex.Pattern;
 
 import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.twod.rotation.Rotation2D;
 import org.apache.commons.geometry.euclidean.twod.shape.Parallelogram;
 import org.apache.commons.numbers.core.Precision;
 import org.junit.jupiter.api.Assertions;
@@ -381,6 +386,429 @@ class Bounds2DTest {
         Assertions.assertFalse(b4.eq(b1, high));
     }
 
+    @Test
+    void testLinecast_intersectsSide() {
+        // -- arrange
+        // use unequal side sizes so that our test lines do not end up passing 
through
+        // a vertex on the opposite side
+        final Bounds2D bounds = Bounds2D.from(Vector2D.of(-0.9, -2), 
Vector2D.of(0.9, 2));
+
+        // -- act/assert
+        checkLinecastIntersectingSide(bounds, Vector2D.of(0.9, 0), 
Vector2D.Unit.PLUS_X);
+        checkLinecastIntersectingSide(bounds, Vector2D.of(-0.9, 0), 
Vector2D.Unit.MINUS_X);
+
+        checkLinecastIntersectingSide(bounds, Vector2D.of(0, 2), 
Vector2D.Unit.PLUS_Y);
+        checkLinecastIntersectingSide(bounds, Vector2D.of(0, -2), 
Vector2D.Unit.MINUS_Y);
+    }
+
+    private void checkLinecastIntersectingSide(
+            final Bounds2D bounds,
+            final Vector2D sidePt,
+            final Vector2D normal) {
+
+        // -- arrange
+        final Vector2D offset = normal.multiply(1.2);
+        final Parallelogram region = bounds.toRegion(TEST_PRECISION);
+
+        EuclideanTestUtils.permute(-1, 1, 0.5, (x, y) -> {
+            final Vector2D otherPt = sidePt
+                    .add(Vector2D.of(x, y))
+                    .add(offset);
+            final Line line = Lines.fromPoints(otherPt, sidePt, 
TEST_PRECISION);
+
+            final LinecastPoint2D reversePt = 
region.linecastFirst(line.reverse());
+
+            // -- act/assert
+            linecastChecker(bounds)
+                .expect(sidePt, normal)
+                .and(reversePt.getPoint(), reversePt.getNormal())
+                .whenGiven(line);
+
+            linecastChecker(bounds)
+                .and(reversePt.getPoint(), reversePt.getNormal())
+                .expect(sidePt, normal)
+                .whenGiven(line.reverse());
+        });
+    }
+
+    @Test
+    void testLinecast_intersectsSingleVertex() {
+        // -- arrange
+        final Bounds2D bounds = Bounds2D.from(Vector2D.ZERO, Vector2D.of(1, 
1));
+
+        // -- act/assert
+        checkLinecastIntersectingSingleVertex(bounds, Vector2D.ZERO);
+        checkLinecastIntersectingSingleVertex(bounds, Vector2D.of(0, 1));
+        checkLinecastIntersectingSingleVertex(bounds, Vector2D.of(1, 0));
+        checkLinecastIntersectingSingleVertex(bounds, Vector2D.of(1, 1));
+    }
+
+    private void checkLinecastIntersectingSingleVertex(
+            final Bounds2D bounds,
+            final Vector2D vertex) {
+
+        // -- arrange
+        final Vector2D centerToVertex = 
vertex.subtract(bounds.getCentroid()).normalize();
+
+        final Vector2D lineDir = centerToVertex.orthogonal();
+        final Line line = Lines.fromPointAndDirection(vertex, lineDir, 
TEST_PRECISION);
+
+        // construct possible normals for this vertex
+        final List<Vector2D> normals = new ArrayList<>();
+        normals.add(centerToVertex.project(Vector2D.Unit.PLUS_X).normalize());
+        normals.add(centerToVertex.project(Vector2D.Unit.PLUS_Y).normalize());
+
+        normals.sort(Vector2D.COORDINATE_ASCENDING_ORDER);
+
+        final BoundsLinecastChecker2D checker = linecastChecker(bounds);
+        for (final Vector2D normal : normals) {
+            checker.expect(vertex, normal);
+        }
+
+        // -- act/assert
+        checker
+            .whenGiven(line)
+            .whenGiven(line.reverse());
+    }
+
+    @Test
+    void testLinecast_vertexToVertex() {
+        // -- arrange
+        final Vector2D min = Vector2D.ZERO;
+        final Vector2D max = Vector2D.of(1, 1);
+
+        final Bounds2D bounds = Bounds2D.from(min, max);
+        final Line line = Lines.fromPoints(min, max, TEST_PRECISION);
+
+        // -- act/assert
+        linecastChecker(bounds)
+            .expect(min, Vector2D.Unit.MINUS_X)
+            .and(min, Vector2D.Unit.MINUS_Y)
+            .and(max, Vector2D.Unit.PLUS_Y)
+            .and(max, Vector2D.Unit.PLUS_X)
+            .whenGiven(line);
+    }
+
+    @Test
+    void testLinecast_alongSide() {
+        // -- arrange
+        final Vector2D min = Vector2D.ZERO;
+        final Vector2D max = Vector2D.of(1, 1);
+
+        final Bounds2D bounds = Bounds2D.from(min, max);
+
+        final int cnt = 10;
+        for (double x = min.getX();
+                x <= max.getX();
+                x += bounds.getDiagonal().getX() / cnt) {
+
+            final Vector2D start = Vector2D.of(x, min.getY());
+            final Vector2D end = Vector2D.of(x, max.getY());
+
+            final Line line = Lines.fromPoints(start, end, TEST_PRECISION);
+
+            // -- act/assert
+            linecastChecker(bounds)
+                .expect(start, Vector2D.Unit.MINUS_Y)
+                .and(end, Vector2D.Unit.PLUS_Y)
+                .whenGiven(line);
+        }
+    }
+
+    @Test
+    void testLinecast_noIntersection() {
+        // -- arrange
+        final Bounds2D bounds = Bounds2D.from(Vector2D.ZERO, Vector2D.of(1, 
1));
+
+        // -- act/assert
+        checkLinecastNoIntersection(bounds, Vector2D.ZERO);
+        checkLinecastNoIntersection(bounds, Vector2D.of(0, 1));
+        checkLinecastNoIntersection(bounds, Vector2D.of(1, 0));
+        checkLinecastNoIntersection(bounds, Vector2D.of(1, 1));
+    }
+
+    private void checkLinecastNoIntersection(
+            final Bounds2D bounds,
+            final Vector2D vertex) {
+
+        // -- arrange
+        final Vector2D toVertex = bounds.getCentroid().directionTo(vertex);
+
+        final Vector2D offsetVertex = vertex.add(toVertex);
+
+        final Line plusXLine = Lines.fromPointAndDirection(offsetVertex, 
Vector2D.Unit.PLUS_X, TEST_PRECISION);
+        final Line plusYLine = Lines.fromPointAndDirection(offsetVertex, 
Vector2D.Unit.PLUS_Y, TEST_PRECISION);
+
+        final BoundsLinecastChecker2D emptyChecker = linecastChecker(bounds)
+                .expectNothing();
+
+        // -- act/assert
+        // check axis-aligned lines
+        emptyChecker
+            .whenGiven(plusXLine)
+            .whenGiven(plusYLine);
+
+        // check slightly rotated lines
+        final Rotation2D rot = Rotation2D.of(0.1 * Math.PI);
+        emptyChecker
+            .whenGiven(plusXLine.transform(rot))
+            .whenGiven(plusYLine.transform(rot));
+    }
+
+    @Test
+    void testLinecast_nonSpan() {
+        // -- arrange
+        final Vector2D min = Vector2D.ZERO;
+        final Vector2D max = Vector2D.of(1, 1);
+
+        final Bounds2D bounds = Bounds2D.from(min, max);
+
+        final Vector2D centroid = bounds.getCentroid();
+
+        final Vector2D start = Vector2D.of(max.getX(), centroid.getY());
+        final Vector2D end = Vector2D.of(min.getX(), centroid.getY());
+
+        final Line line = Lines.fromPoints(start, end, TEST_PRECISION);
+
+        // -- act/assert
+        linecastChecker(bounds)
+            .expect(end, Vector2D.Unit.MINUS_X)
+            .whenGiven(line.rayFrom(-0.5));
+
+        linecastChecker(bounds)
+            .expect(start, Vector2D.Unit.PLUS_X)
+            .whenGiven(line.reverseRayTo(-0.5));
+
+        linecastChecker(bounds)
+            .expectNothing()
+            .whenGiven(line.segment(-0.9, -0.1));
+
+        linecastChecker(bounds)
+            .expect(end, Vector2D.Unit.MINUS_X)
+            .whenGiven(line.segment(-0.9, 0.1));
+
+        linecastChecker(bounds)
+            .expect(start, Vector2D.Unit.PLUS_X)
+            .whenGiven(line.segment(-1.1, -0.1));
+
+        linecastChecker(bounds)
+            .expect(start, Vector2D.Unit.PLUS_X)
+            .expect(end, Vector2D.Unit.MINUS_X)
+            .whenGiven(line.segment(-1.1, 0.1));
+    }
+
+    @Test
+    void testLinecast_subsetEndpointOnBounds() {
+        // -- arrange
+        final Vector2D min = Vector2D.ZERO;
+        final Vector2D max = Vector2D.of(1, 1);
+
+        final Bounds2D bounds = Bounds2D.from(min, max);
+
+        final Vector2D centroid = bounds.getCentroid();
+
+        final Vector2D start = Vector2D.of(max.getX(), centroid.getY());
+        final Vector2D end = Vector2D.of(min.getX(), centroid.getY());
+
+        final Line line = Lines.fromPoints(start, end, TEST_PRECISION);
+
+        // -- act/assert
+        linecastChecker(bounds)
+            .expect(end, Vector2D.Unit.MINUS_X)
+            .whenGiven(line.rayFrom(0));
+
+        linecastChecker(bounds)
+            .expect(end, Vector2D.Unit.MINUS_X)
+            .whenGiven(line.segment(0, 1));
+
+        linecastChecker(bounds)
+            .expect(start, Vector2D.Unit.PLUS_X)
+            .whenGiven(line.reverseRayTo(-1));
+
+        linecastChecker(bounds)
+            .expect(start, Vector2D.Unit.PLUS_X)
+            .whenGiven(line.segment(-2, -1));
+    }
+
+    @Test
+    void testLinecast_usesLinePrecision() {
+        // -- arrange
+        final double withinEps = 0.9 * TEST_EPS;
+        final double outsideEps = 1.1 * TEST_EPS;
+
+        final Vector2D min = Vector2D.ZERO;
+        final Vector2D max = Vector2D.of(1, 1);
+
+        final Bounds2D bounds = Bounds2D.from(min, max);
+
+        final Vector2D centroid = bounds.getCentroid();
+
+        final Vector2D centerStart = Vector2D.of(max.getX(), centroid.getY());
+        final Vector2D centerEnd = Vector2D.of(min.getX(), centroid.getY());
+
+        final Line centerLine = Lines.fromPoints(centerStart, centerEnd, 
TEST_PRECISION);
+
+        final Vector2D sideStart = Vector2D.of(max.getX() + withinEps, 
min.getY() + withinEps);
+        final Vector2D sideEnd = Vector2D.of(max.getX() + withinEps, 
max.getY() + withinEps);
+
+        final Line sideLine = Lines.fromPoints(sideStart, sideEnd, 
TEST_PRECISION);
+
+        // -- act/assert
+        linecastChecker(bounds)
+            .expect(centerEnd, Vector2D.Unit.MINUS_X)
+            .whenGiven(centerLine.rayFrom(withinEps));
+
+        linecastChecker(bounds)
+            .expectNothing()
+            .whenGiven(centerLine.rayFrom(outsideEps));
+
+        linecastChecker(bounds)
+            .expect(centerStart, Vector2D.Unit.PLUS_X)
+            .expect(centerEnd, Vector2D.Unit.MINUS_X)
+            .whenGiven(centerLine.segment(-1 + withinEps, -withinEps));
+
+        linecastChecker(bounds)
+            .expectNothing()
+            .whenGiven(centerLine.segment(-1 + outsideEps, -outsideEps));
+
+        linecastChecker(bounds)
+            .expectNothing()
+            .whenGiven(sideLine.segment(outsideEps, 1 - outsideEps));
+    }
+
+    @Test
+    void testLinecast_boundsHasNoSize() {
+        // -- arrange
+        final Vector2D pt = Vector2D.of(1, 2);
+
+        final Bounds2D bounds = Bounds2D.from(pt, pt);
+
+        final Line diagonalLine = Lines.fromPointAndDirection(pt, 
Vector2D.of(1, 1), TEST_PRECISION);
+
+        final Line plusXLine = Lines.fromPointAndDirection(pt, 
Vector2D.Unit.PLUS_X, TEST_PRECISION);
+
+        // -- act/assert
+        linecastChecker(bounds)
+            .expect(pt, Vector2D.Unit.MINUS_X)
+            .expect(pt, Vector2D.Unit.MINUS_Y)
+            .expect(pt, Vector2D.Unit.PLUS_Y)
+            .expect(pt, Vector2D.Unit.PLUS_X)
+            .whenGiven(diagonalLine);
+
+        linecastChecker(bounds)
+            .expect(pt, Vector2D.Unit.MINUS_X)
+            .expect(pt, Vector2D.Unit.PLUS_X)
+            .whenGiven(plusXLine);
+    }
+
+    @Test
+    void testLineIntersection() {
+        // -- arrange
+        final Vector2D min = Vector2D.ZERO;
+        final Vector2D max = Vector2D.of(1, 1);
+
+        final Vector2D insideMin = Vector2D.of(0.1, 0.1);
+        final Vector2D insideMax = Vector2D.of(0.9, 0.9);
+
+        final Vector2D outsideMin = Vector2D.of(-0.1, -0.1);
+        final Vector2D outsideMax = Vector2D.of(1.1, 1.1);
+
+        final Bounds2D bounds = Bounds2D.from(min, max);
+
+        final Line diagonal = Lines.fromPoints(min, max, TEST_PRECISION);
+
+        // -- act/assert
+        assertLineIntersection(bounds, diagonal, min, max);
+
+        assertLineIntersection(bounds, diagonal.segment(outsideMin, 
outsideMax), min, max);
+        assertLineIntersection(bounds, diagonal.segment(outsideMin, 
insideMax), min, insideMax);
+        assertLineIntersection(bounds, diagonal.segment(insideMin, 
outsideMax), insideMin, max);
+        assertLineIntersection(bounds, diagonal.segment(insideMin, insideMax), 
insideMin, insideMax);
+
+        assertLineIntersection(bounds, diagonal.rayFrom(min), min, max);
+        assertLineIntersection(bounds, diagonal.reverseRayTo(min), min, min);
+
+        assertLineIntersection(bounds, diagonal.rayFrom(max), max, max);
+        assertLineIntersection(bounds, diagonal.reverseRayTo(max), min, max);
+
+        assertLineIntersection(bounds, diagonal.rayFrom(insideMax), insideMax, 
max);
+        assertLineIntersection(bounds, diagonal.reverseRayTo(insideMax), min, 
insideMax);
+    }
+
+    @Test
+    void testLineIntersection_noIntersection() {
+        // -- arrange
+        final Bounds2D bounds = Bounds2D.from(Vector2D.ZERO, Vector2D.of(1, 
1));
+
+        final Line plusXLine =
+                Lines.fromPointAndDirection(bounds.getCentroid(), 
Vector2D.Unit.PLUS_X, TEST_PRECISION);
+
+        // -- act/assert
+        checkLineNoIntersection(bounds, Vector2D.ZERO);
+        checkLineNoIntersection(bounds, Vector2D.of(0, 0));
+        checkLineNoIntersection(bounds, Vector2D.of(0, 1));
+        checkLineNoIntersection(bounds, Vector2D.of(1, 0));
+        checkLineNoIntersection(bounds, Vector2D.of(1, 1));
+
+        assertNoLineIntersection(bounds, plusXLine.segment(-0.2, -0.1));
+        assertNoLineIntersection(bounds, plusXLine.reverseRayTo(-0.1));
+
+        assertNoLineIntersection(bounds, plusXLine.segment(1.1, 1.2));
+        assertNoLineIntersection(bounds, plusXLine.rayFrom(1.1));
+    }
+
+    private void checkLineNoIntersection(
+            final Bounds2D bounds,
+            final Vector2D vertex) {
+
+        // -- arrange
+        final Vector2D toVertex = bounds.getCentroid().directionTo(vertex);
+
+        final Vector2D offsetVertex = vertex.add(toVertex);
+
+        final Line plusXLine = Lines.fromPointAndDirection(offsetVertex, 
Vector2D.Unit.PLUS_X, TEST_PRECISION);
+        final Line plusYLine = Lines.fromPointAndDirection(offsetVertex, 
Vector2D.Unit.PLUS_Y, TEST_PRECISION);
+
+        // -- act/assert
+        // check axis-aligned lines
+        assertNoLineIntersection(bounds, plusXLine);
+        assertNoLineIntersection(bounds, plusYLine);
+
+        // check slightly rotated lines
+        final Rotation2D rot = Rotation2D.of(0.1 * Math.PI);
+        assertNoLineIntersection(bounds, plusXLine.transform(rot));
+        assertNoLineIntersection(bounds, plusYLine.transform(rot));
+    }
+
+    @Test
+    void testLineIntersection_boundsHasNoSize() {
+        // -- arrange
+        final Vector2D pt = Vector2D.of(1, 2);
+
+        final Bounds2D bounds = Bounds2D.from(pt, pt);
+
+        final Line plusXLine = Lines.fromPointAndDirection(pt, 
Vector2D.Unit.PLUS_X, TEST_PRECISION);
+
+        // -- act/assert
+        assertLineIntersection(bounds, plusXLine, pt, pt);
+        assertLineIntersection(bounds, plusXLine.rayFrom(pt), pt, pt);
+    }
+
+    @Test
+    void testLineIntersection_lineAlmostParallel() {
+        // -- arrange
+        final Vector2D min = Vector2D.of(1e150, -1);
+        final Vector2D max = Vector2D.of(1.1e150, 1);
+
+        final Bounds2D bounds = Bounds2D.from(min, max);
+
+        final Vector2D lineDir = Vector2D.of(1, -5e-11);
+        final Line line = Lines.fromPointAndDirection(Vector2D.ZERO, lineDir, 
TEST_PRECISION);
+
+        // -- act
+        assertNoLineIntersection(bounds, line);
+    }
+
     @Test
     void testHashCode() {
         // arrange
@@ -498,4 +926,187 @@ class Bounds2DTest {
             Assertions.assertEquals(contains, bounds.contains(pt, 
TEST_PRECISION), "Unexpected location for point " + pt);
         }
     }
+
+    private static void assertLineIntersection(
+            final Bounds2D bounds,
+            final Line line,
+            final Vector2D start,
+            final Vector2D end) {
+        final Segment segment = bounds.intersection(line);
+
+        Assertions.assertSame(line, segment.getLine());
+        assertSegment(segment, start, end);
+
+        Assertions.assertTrue(bounds.intersects(line));
+    }
+
+    private static void assertLineIntersection(
+            final Bounds2D bounds,
+            final LineConvexSubset subset,
+            final Vector2D start,
+            final Vector2D end) {
+        final Segment segment = bounds.intersection(subset);
+
+        Assertions.assertSame(subset.getLine(), segment.getLine());
+        assertSegment(segment, start, end);
+
+        Assertions.assertTrue(bounds.intersects(subset));
+    }
+
+    private static void assertNoLineIntersection(
+            final Bounds2D bounds,
+            final Line line) {
+        Assertions.assertNull(bounds.intersection(line));
+        Assertions.assertFalse(bounds.intersects(line));
+    }
+
+    private static void assertNoLineIntersection(
+            final Bounds2D bounds,
+            final LineConvexSubset subset) {
+        Assertions.assertNull(bounds.intersection(subset));
+        Assertions.assertFalse(bounds.intersects(subset));
+    }
+
+    private static void assertSegment(final Segment segment, final Vector2D 
start, final Vector2D end) {
+        EuclideanTestUtils.assertCoordinatesEqual(start, 
segment.getStartPoint(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(end, segment.getEndPoint(), 
TEST_EPS);
+    }
+
+    private static BoundsLinecastChecker2D linecastChecker(final Bounds2D 
bounds) {
+        return new BoundsLinecastChecker2D(bounds);
+    }
+
+    /**
+     * Internal test class used to perform and verify linecast operations.
+     */
+    private static final class BoundsLinecastChecker2D {
+
+        private final Bounds2D bounds;
+
+        private final Parallelogram region;
+
+        private final LinecastChecker2D checker;
+
+        BoundsLinecastChecker2D(final Bounds2D bounds) {
+            this.bounds = bounds;
+            this.region = bounds.hasSize(TEST_PRECISION) ?
+                    bounds.toRegion(TEST_PRECISION) :
+                    null;
+            this.checker = LinecastChecker2D.with(bounds);
+        }
+
+        public BoundsLinecastChecker2D expectNothing() {
+            checker.expectNothing();
+            return this;
+        }
+
+        public BoundsLinecastChecker2D expect(final Vector2D pt, final 
Vector2D normal) {
+            checker.expect(pt, normal);
+            return this;
+        }
+
+        public BoundsLinecastChecker2D and(final Vector2D pt, final Vector2D 
normal) {
+            return expect(pt, normal);
+        }
+
+        public BoundsLinecastChecker2D whenGiven(final Line line) {
+            // perform the standard checks
+            checker.whenGiven(line);
+
+            // check that the returned points are equivalent to those returned 
by linecasting against
+            // the region
+            final List<LinecastPoint2D> boundsResults = bounds.linecast(line);
+
+            if (region != null) {
+                assertLinecastElements(region.linecast(line), 
bounds.linecast(line));
+            }
+
+            // check consistency with the intersects method; having linecast 
results guarantees
+            // that we intersect the bounds but not vice versa
+            if (!boundsResults.isEmpty()) {
+                Assertions.assertTrue(bounds.intersects(line),
+                        () -> "Linecast result is inconsistent with intersects 
method: line= " + line);
+
+                assertLinecastResultsConsistentWithSegment(boundsResults, 
bounds.intersection(line));
+            }
+
+            return this;
+        }
+
+        public BoundsLinecastChecker2D whenGiven(final LineConvexSubset 
subset) {
+            // perform the standard checks
+            checker.whenGiven(subset);
+
+            // check that the returned points are equivalent to those returned 
by linecasting against
+            // the region
+            final List<LinecastPoint2D> boundsResults = 
bounds.linecast(subset);
+
+            if (region != null) {
+                assertLinecastElements(region.linecast(subset), boundsResults);
+            }
+
+            // check consistency with the intersects methods; having linecast 
results guarantees
+            // that we intersect the bounds but not vice versa
+            if (!boundsResults.isEmpty()) {
+                Assertions.assertTrue(bounds.intersects(subset),
+                        () -> "Linecast result is inconsistent with intersects 
method: line subset= " + subset);
+
+                assertLinecastResultsConsistentWithSegment(boundsResults, 
bounds.intersection(subset));
+            }
+
+            return this;
+        }
+
+        /** Assert that the two collections contain the same linecast points 
and that the elements
+         * of {@code actual} are arranged in ascending abscissa order. Note 
that this does <em>not</em>
+         * assert that {@code expected} and {@code actual} have the same exact 
ordering, since the
+         * specific ordering if sensitive to floating point errors.
+         * @param expected expected collection
+         * @param actual actual collection
+         */
+        private void assertLinecastElements(
+                final Collection<LinecastPoint2D> expected,
+                final Collection<LinecastPoint2D> actual) {
+            Assertions.assertEquals(expected.size(), actual.size(), 
"Unexpected list size");
+
+            // create a sorted copy
+            final List<LinecastPoint2D> sortedList = new ArrayList<>(actual);
+            sortedList.sort(LinecastPoint2D.ABSCISSA_ORDER);
+
+            // check element membership
+            for (final LinecastPoint2D expectedPt : expected) {
+                final Iterator<LinecastPoint2D> sortedIt = 
sortedList.iterator();
+
+                boolean found = false;
+                while (sortedIt.hasNext()) {
+                    if (expectedPt.eq(sortedIt.next(), TEST_PRECISION)) {
+                        found = true;
+                        break;
+                    }
+                }
+
+                if (!found) {
+                    Assertions.fail("Missing expected linecast point " + 
expectedPt);
+                }
+            }
+
+            // check the order
+            Assertions.assertEquals(sortedList, actual);
+        }
+
+        /** Assert that the linecast results are consistent with the given 
segment, which is taken
+         * to be the intersection of a line or line convex subset with the 
bounding box.
+         * @param linecastResults
+         * @param segment
+         */
+        private void assertLinecastResultsConsistentWithSegment(
+                final List<LinecastPoint2D> linecastResults,
+                final Segment segment) {
+
+            for (final LinecastPoint2D pt : linecastResults) {
+                Assertions.assertEquals(RegionLocation.BOUNDARY, 
segment.classifyAbscissa(pt.getAbscissa()),
+                        () -> "Expected linecast point to lie on segment 
boundary");
+            }
+        }
+    }
 }
diff --git 
a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/Bounds3DPerformance.java
 
b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/Bounds3DPerformance.java
new file mode 100644
index 00000000..1db0e8ec
--- /dev/null
+++ 
b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/Bounds3DPerformance.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.examples.jmh.euclidean;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.geometry.euclidean.threed.Bounds3D;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.euclidean.threed.line.Line3D;
+import org.apache.commons.geometry.euclidean.threed.line.Lines3D;
+import org.apache.commons.geometry.euclidean.threed.line.Segment3D;
+import org.apache.commons.numbers.core.Precision;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.sampling.shape.UnitBallSampler;
+import org.apache.commons.rng.simple.RandomSource;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+/** Benchmarks for the {@link Bounds3D} class.
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Fork(value = 1, jvmArgs = {"-server", "-Xms512M", "-Xmx512M"})
+public class Bounds3DPerformance {
+
+    /** Precision context. */
+    private static final Precision.DoubleEquivalence PRECISION =
+            Precision.doubleEquivalenceOfEpsilon(1e-6);
+
+    /** Benchmark input class providing random line segments. */
+    @State(Scope.Thread)
+    public static class SegmentInput {
+
+        /** Minimum value used to construct random point coordinates. */
+        private static final double COORDINATE_MIN = -10;
+
+        /** Maximum value used to construct random point coordinates. */
+        private static final double COORDINATE_MAX = +10;
+
+        /** Number of line segments to generate. */
+        @Param({"1000"})
+        private int count;
+
+        /** Seed value for randomization. */
+        @Param({"1"})
+        private int randomSeed;
+
+        /** List of segments for the run. */
+        private List<Segment3D> segments;
+
+        /** Random instance. */
+        private UniformRandomProvider random;
+
+        /** Get the segments for the run.
+         * @return segments for the run
+         */
+        public List<Segment3D> getSegments() {
+            return segments;
+        }
+
+        /** Set up the instance for the benchmark. */
+        @Setup(Level.Iteration)
+        public void setup() {
+            random = RandomSource.MT.create(randomSeed);
+
+            final UnitBallSampler ballSampler = UnitBallSampler.of(random, 3);
+
+            segments = new ArrayList<>(count);
+            for (int i = 0; i < count; ++i) {
+                final Vector3D pt = randomPoint();
+                final Vector3D dir = Vector3D.of(ballSampler.sample());
+
+                final Line3D line = Lines3D.fromPointAndDirection(pt, dir, 
PRECISION);
+
+                segments.add(line.segment(randomCoordinate(), 
randomCoordinate()));
+            }
+        }
+
+        /** Return a random point with coordinates within the configured min 
and max range.
+         * @return a random point with coordinates within the configured min 
and max range
+         */
+        private Vector3D randomPoint() {
+            return Vector3D.of(
+                    randomCoordinate(),
+                    randomCoordinate(),
+                    randomCoordinate());
+        }
+
+        /** Return a random double coordinate value within the configured min 
and max range.
+         * @return a random double coordinate value within the configured min 
and max range
+         */
+        private double randomCoordinate() {
+            return ((COORDINATE_MAX - COORDINATE_MIN) * random.nextDouble()) + 
COORDINATE_MIN;
+
+        }
+    }
+
+    /** Construct a default {@link Bounds3D} instance for performance testing.
+     * @return a default {@link Bounds3D} instance
+     */
+    private static Bounds3D createDefaultTestBounds() {
+        return Bounds3D.from(Vector3D.of(-1, -1, -1), Vector3D.of(1, 1, 1));
+    }
+
+    /** Baseline benchmark that construct a bounds instance and iterates 
through all
+     * input values.
+     * @param input input for the run
+     * @param bh blackhole instance
+     * @return bounds instance
+     */
+    @Benchmark
+    public Bounds3D segmentBaseline(final SegmentInput input, final Blackhole 
bh) {
+        final Bounds3D bounds = createDefaultTestBounds();
+        for (final Segment3D segment : input.getSegments()) {
+            bh.consume(segment);
+        }
+        return bounds;
+    }
+
+    /** Benchmark that tests the performance of the
+     * {@link 
Bounds3D#intersects(org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D)}
 method.
+     * @param input input for the run
+     * @param bh blackhole instance
+     * @return bounds instance
+     */
+    @Benchmark
+    public Bounds3D segmentIntersects(final SegmentInput input, final 
Blackhole bh) {
+        final Bounds3D bounds = createDefaultTestBounds();
+        for (final Segment3D segment : input.getSegments()) {
+            bh.consume(bounds.intersects(segment));
+        }
+        return bounds;
+    }
+
+    /** Benchmark that tests the performance of the
+     * {@link 
Bounds3D#intersection(org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D)}
 method.
+     * @param input input for the run
+     * @param bh blackhole instance
+     * @return bounds instance
+     */
+    @Benchmark
+    public Bounds3D segmentIntersection(final SegmentInput input, final 
Blackhole bh) {
+        final Bounds3D bounds = createDefaultTestBounds();
+        for (final Segment3D segment : input.getSegments()) {
+            bh.consume(bounds.intersection(segment));
+        }
+        return bounds;
+    }
+
+    /** Benchmark that tests the performance of the
+     * {@link 
Bounds3D#linecast(org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D)}
 method.
+     * @param input input for the run
+     * @param bh blackhole instance
+     * @return bounds instance
+     */
+    @Benchmark
+    public Bounds3D segmentLinecast(final SegmentInput input, final Blackhole 
bh) {
+        final Bounds3D bounds = createDefaultTestBounds();
+        for (final Segment3D segment : input.getSegments()) {
+            bh.consume(bounds.linecast(segment));
+        }
+        return bounds;
+    }
+
+    /** Benchmark that tests the performance of the
+     * {@link 
Bounds3D#linecastFirst(org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D)}
 method.
+     * @param input input for the run
+     * @param bh blackhole instance
+     * @return bounds instance
+     */
+    @Benchmark
+    public Bounds3D segmentLinecastFirst(final SegmentInput input, final 
Blackhole bh) {
+        final Bounds3D bounds = createDefaultTestBounds();
+        for (final Segment3D segment : input.getSegments()) {
+            bh.consume(bounds.linecastFirst(segment));
+        }
+        return bounds;
+    }
+}

Reply via email to