You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by ma...@apache.org on 2020/05/27 11:29:25 UTC
[commons-geometry] 01/03: GEOMETRY-94: adding PlaneConvexSubset
additional implementations;
GEOMETRY-77: adding Bounds2D and Bounds3D classes
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
commit ed41804dc3f8273e483ddfabd331732e1ae2c550
Author: Matt Juntunen <ma...@apache.org>
AuthorDate: Sun May 24 12:17:37 2020 -0400
GEOMETRY-94: adding PlaneConvexSubset additional implementations; GEOMETRY-77: adding Bounds2D and Bounds3D classes
---
.../org/apache/commons/geometry/core/Region.java | 2 +-
.../commons/geometry/core/RegionEmbedding.java | 11 +-
.../geometry/core/internal/HyperplaneSubsets.java | 89 +++
.../AbstractRegionEmbeddingHyperplaneSubset.java | 99 ---
.../core/partitioning/HyperplaneSubset.java | 27 +-
.../commons/geometry/core/EmbeddingTest.java | 8 +-
.../commons/geometry/core/RegionEmbeddingTest.java | 116 ----
.../core/internal/HyperplaneSubsetsTest.java | 143 ++++
.../AbstractConvexHyperplaneBoundedRegionTest.java | 10 +-
.../core/partitioning/AbstractHyperplaneTest.java | 4 +-
...bstractRegionEmbeddingHyperplaneSubsetTest.java | 219 -------
.../bsp/AbstractBSPTreeMergeOperatorTest.java | 10 +-
.../core/partitioning/bsp/AbstractBSPTreeTest.java | 16 +-
.../bsp/AbstractRegionBSPTreeBooleanTest.java | 10 +-
.../bsp/AbstractRegionBSPTreeTest.java | 16 +-
.../core/partitioning/bsp/BSPTreeVisitorTest.java | 8 +-
.../core/partitioning/bsp/MergeChecker.java | 6 +-
.../partitioning/bsp/RegionCutBoundaryTest.java | 6 +-
.../test/AttributeBSPTree.java | 2 +-
.../test/PartitionTestUtils.java | 2 +-
.../test/TestBSPTree.java | 2 +-
.../{partition => partitioning}/test/TestLine.java | 2 +-
.../test/TestLineSegment.java | 8 +-
.../test/TestLineSegmentCollection.java | 12 +-
.../test/TestLineSegmentCollectionBuilder.java | 2 +-
.../test/TestPoint1D.java | 2 +-
.../test/TestPoint2D.java | 2 +-
.../test/TestRegionBSPTree.java | 2 +-
.../test/TestTransform2D.java | 2 +-
.../euclidean/threed/SphereGenerator.java | 4 +-
.../commons/geometry/euclidean/AbstractBounds.java | 159 +++++
.../geometry/euclidean/oned/OrientedPoint.java | 21 +-
.../euclidean/threed/AbstractConvexPolygon3D.java | 451 +++++++++++++
.../threed/AbstractEmbeddedRegionPlaneSubset.java | 137 ++++
.../euclidean/threed/AbstractPlaneSubset.java | 99 ++-
.../euclidean/threed/BoundarySource3D.java | 21 +
.../threed/BoundarySourceBoundsBuilder3D.java | 57 ++
.../threed/BoundarySourceLinecaster3D.java | 16 +-
.../geometry/euclidean/threed/Bounds3D.java | 290 +++++++++
.../geometry/euclidean/threed/ConvexPolygon3D.java | 29 +-
.../geometry/euclidean/threed/ConvexVolume.java | 28 +-
.../threed/EmbeddedAreaPlaneConvexSubset.java | 117 ++++
.../euclidean/threed/EmbeddedTreePlaneSubset.java | 122 +++-
.../geometry/euclidean/threed/EmbeddingPlane.java | 333 ++++++++++
.../commons/geometry/euclidean/threed/Plane.java | 647 +++++++-----------
.../euclidean/threed/PlaneConvexSubset.java | 114 +---
.../geometry/euclidean/threed/PlaneSubset.java | 259 ++------
.../commons/geometry/euclidean/threed/Planes.java | 721 ++++++++++++++++----
.../geometry/euclidean/threed/RegionBSPTree3D.java | 27 +-
.../euclidean/threed/SimpleTriangle3D.java | 110 ++++
.../geometry/euclidean/threed/Triangle3D.java | 59 ++
.../geometry/euclidean/threed/Vector3D.java | 181 +++++-
.../threed/VertexListConvexPolygon3D.java | 84 +++
.../threed/line/EmbeddedTreeLineSubset3D.java | 47 +-
.../threed/line/LineSpanningSubset3D.java | 19 +
.../euclidean/threed/line/LineSubset3D.java | 18 +-
.../geometry/euclidean/threed/line/Ray3D.java | 19 +
.../euclidean/threed/line/ReverseRay3D.java | 19 +
.../geometry/euclidean/threed/line/Segment3D.java | 16 +
.../euclidean/threed/shape/Parallelepiped.java | 2 +-
.../geometry/euclidean/twod/BoundarySource2D.java | 9 +
.../twod/BoundarySourceBoundsBuilder2D.java | 58 ++
.../euclidean/twod/BoundarySourceLinecaster2D.java | 16 +-
.../commons/geometry/euclidean/twod/Bounds2D.java | 271 ++++++++
.../geometry/euclidean/twod/ConvexArea.java | 138 ++--
.../euclidean/twod/EmbeddedTreeLineSubset.java | 54 ++
.../commons/geometry/euclidean/twod/Line.java | 6 +-
.../euclidean/twod/LineSpanningSubset.java | 18 +
.../geometry/euclidean/twod/LineSubset.java | 28 +-
.../commons/geometry/euclidean/twod/Lines.java | 25 +-
.../commons/geometry/euclidean/twod/Ray.java | 81 ++-
.../geometry/euclidean/twod/ReverseRay.java | 99 +--
.../commons/geometry/euclidean/twod/Segment.java | 109 ++--
.../commons/geometry/euclidean/twod/Vector2D.java | 145 +++++
.../geometry/euclidean/twod/path/LinePath.java | 10 +-
.../euclidean/DocumentationExamplesTest.java | 37 +-
.../geometry/euclidean/EuclideanTestUtils.java | 43 ++
.../geometry/euclidean/oned/OrientedPointTest.java | 3 +-
...ubsetTest.java => AbstractPlaneSubsetTest.java} | 106 ++-
.../euclidean/threed/BoundarySource3DTest.java | 12 +-
.../threed/BoundarySourceBoundsBuilder3DTest.java | 155 +++++
.../threed/BoundarySourceLinecaster3DTest.java | 8 +-
.../geometry/euclidean/threed/Bounds3DTest.java | 538 +++++++++++++++
.../euclidean/threed/ConvexVolumeTest.java | 105 ++-
.../threed/EmbeddedAreaPlaneConvexSubsetTest.java | 468 +++++++++++++
.../threed/EmbeddedTreePlaneSubsetTest.java | 415 +++++++++---
.../euclidean/threed/EmbeddingPlaneTest.java | 509 +++++++++++++++
.../euclidean/threed/PlaneConvexSubsetTest.java | 362 ++---------
.../geometry/euclidean/threed/PlaneTest.java | 442 ++++++-------
.../geometry/euclidean/threed/PlanesTest.java | 723 +++++++++++++++++++++
.../euclidean/threed/RegionBSPTree3DTest.java | 94 ++-
.../euclidean/threed/SimpleTriangle3DTest.java | 336 ++++++++++
.../geometry/euclidean/threed/Vector3DTest.java | 79 +++
.../threed/VertexListConvexPolygon3DTest.java | 393 +++++++++++
.../threed/line/EmbeddedTreeLineSubset3DTest.java | 107 ++-
.../geometry/euclidean/threed/line/Line3DTest.java | 3 +
.../geometry/euclidean/threed/line/Ray3DTest.java | 9 +
.../euclidean/threed/line/ReverseRay3DTest.java | 9 +
.../euclidean/threed/line/Segment3DTest.java | 31 +
.../twod/BoundarySourceBoundsBuilder2DTest.java | 127 ++++
.../geometry/euclidean/twod/Bounds2DTest.java | 507 +++++++++++++++
.../geometry/euclidean/twod/ConvexAreaTest.java | 334 ++++------
.../euclidean/twod/EmbeddedTreeLineSubsetTest.java | 53 +-
.../euclidean/twod/LineSpanningSubsetTest.java | 2 +
.../geometry/euclidean/twod/LineSubsetTest.java | 21 +
.../commons/geometry/euclidean/twod/RayTest.java | 32 +-
.../euclidean/twod/RegionBSPTree2DTest.java | 29 +-
.../geometry/euclidean/twod/ReverseRayTest.java | 33 +-
.../geometry/euclidean/twod/SegmentTest.java | 43 ++
.../geometry/euclidean/twod/Vector2DTest.java | 83 ++-
.../geometry/euclidean/twod/path/LinePathTest.java | 8 +-
.../commons/geometry/examples/io/Format3D.java | 41 +-
.../geometry/hull/euclidean/twod/ConvexHull2D.java | 2 +-
.../commons/geometry/spherical/oned/CutAngle.java | 19 +-
.../geometry/spherical/twod/GreatCircleSubset.java | 64 +-
.../geometry/spherical/oned/CutAngleTest.java | 4 +-
.../twod/EmbeddedTreeSubGreatCircleTest.java | 5 +
.../geometry/spherical/twod/GreatArcTest.java | 2 +
.../spherical/twod/GreatCircleSubsetTest.java | 5 +
.../checkstyle/checkstyle-suppressions.xml | 1 +
.../resources/spotbugs/spotbugs-exclude-filter.xml | 26 +-
src/site/xdoc/userguide/index.xml | 48 +-
122 files changed, 9891 insertions(+), 2716 deletions(-)
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Region.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Region.java
index f22e00c..308f83f 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Region.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Region.java
@@ -45,7 +45,7 @@ public interface Region<P extends Point<P>> extends Sized {
/** Get the barycenter of the region or null if no barycenter exists or
* one exists but is not unique. A barycenter will not exist for empty or
* infinite regions.
- * @return the barycenter of the region or null if none exists
+ * @return the barycenter of the region or null if no unique barycenter exists
*/
P getBarycenter();
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionEmbedding.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionEmbedding.java
index b82b75a..feedb41 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionEmbedding.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionEmbedding.java
@@ -23,16 +23,7 @@ package org.apache.commons.geometry.core;
* @see Region
*/
public interface RegionEmbedding<P extends Point<P>, S extends Point<S>>
- extends Embedding<P, S>, Sized {
-
- /** Get the size of the instance, which by default is the size of the embedded
- * subspace region.
- * @return the size of instance
- */
- @Override
- default double getSize() {
- return getSubspaceRegion().getSize();
- }
+ extends Embedding<P, S> {
/** Get the embedded subspace region.
* @return the embedded subspace region
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/HyperplaneSubsets.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/HyperplaneSubsets.java
new file mode 100644
index 0000000..1f453a6
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/HyperplaneSubsets.java
@@ -0,0 +1,89 @@
+/*
+ * 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.core.internal;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Region;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
+
+/** Utility methods for {@link org.apache.commons.geometry.core.partitioning.HyperplaneSubset}
+ * implementations.
+ */
+public final class HyperplaneSubsets {
+
+ /** Utility class; no instantiation. */
+ private HyperplaneSubsets() {
+ }
+
+ /** Classify a point against a region embedded in a hyperplane.
+ * @param <P> Point implementation class
+ * @param <S> Subspace point implementation class
+ * @param <H> Hyperplane implementation class
+ * @param <R> Region implementation class
+ * @param pt the point to classify
+ * @param hyperplane hyperplane containing the embedded region
+ * @param embeddedRegion embedded region to classify against
+ * @return the region location of the given point
+ */
+ public static <
+ P extends Point<P>,
+ S extends Point<S>,
+ H extends EmbeddingHyperplane<P, S>,
+ R extends Region<S>> RegionLocation classifyAgainstEmbeddedRegion(final P pt,
+ final H hyperplane, final R embeddedRegion) {
+
+ if (hyperplane.contains(pt)) {
+ final S subPoint = hyperplane.toSubspace(pt);
+
+ return embeddedRegion.classify(subPoint);
+ }
+
+ return RegionLocation.OUTSIDE;
+ }
+
+ /** Return the closest point to a given point in a region embedded in a hyperplane.
+ * @param <P> Point implementation class
+ * @param <S> Subspace point implementation class
+ * @param <H> Hyperplane implementation class
+ * @param <R> Region implementation class
+ * @param pt point to find the closest point to
+ * @param hyperplane hyperplane containing the embedded region
+ * @param embeddedRegion embedded region to find the closest point in
+ * @return the closest point to {@code pt} in the embedded region
+ */
+ public static <
+ P extends Point<P>,
+ S extends Point<S>,
+ H extends EmbeddingHyperplane<P, S>,
+ R extends Region<S>> P closestToEmbeddedRegion(final P pt,
+ final H hyperplane, final R embeddedRegion) {
+
+ final S subPt = hyperplane.toSubspace(pt);
+
+ if (embeddedRegion.contains(subPt)) {
+ return hyperplane.toSpace(subPt);
+ }
+
+ final S subProjected = embeddedRegion.project(subPt);
+ if (subProjected != null) {
+ return hyperplane.toSpace(subProjected);
+ }
+
+ return null;
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegionEmbeddingHyperplaneSubset.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegionEmbeddingHyperplaneSubset.java
deleted file mode 100644
index e72071a..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegionEmbeddingHyperplaneSubset.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.Region;
-import org.apache.commons.geometry.core.RegionEmbedding;
-import org.apache.commons.geometry.core.RegionLocation;
-
-/** Abstract base class for hyperplane subset implementations that embed a lower-dimension region through
- * an embedding hyperplane.
- * @param <P> Point implementation type
- * @param <S> Subspace point implementation type
- * @param <H> Hyperplane containing the embedded subspace
- */
-public abstract class AbstractRegionEmbeddingHyperplaneSubset<
- P extends Point<P>,
- S extends Point<S>,
- H extends EmbeddingHyperplane<P, S>> implements HyperplaneSubset<P>, RegionEmbedding<P, S> {
-
- /** {@inheritDoc} */
- @Override
- public boolean isFull() {
- return getSubspaceRegion().isFull();
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean isEmpty() {
- return getSubspaceRegion().isEmpty();
- }
-
- /** {@inheritDoc} */
- @Override
- public S toSubspace(final P pt) {
- return getHyperplane().toSubspace(pt);
- }
-
- /** {@inheritDoc} */
- @Override
- public P toSpace(final S pt) {
- return getHyperplane().toSpace(pt);
- }
-
- /** {@inheritDoc} */
- @Override
- public RegionLocation classify(P point) {
- final H hyperplane = getHyperplane();
-
- if (hyperplane.contains(point)) {
- final S subPoint = hyperplane.toSubspace(point);
-
- return getSubspaceRegion().classify(subPoint);
- }
- return RegionLocation.OUTSIDE;
- }
-
- /** {@inheritDoc} */
- @Override
- public P closest(P point) {
- final H hyperplane = getHyperplane();
-
- final P projected = hyperplane.project(point);
- final S subProjected = hyperplane.toSubspace(projected);
-
- final Region<S> region = getSubspaceRegion();
- if (region.contains(subProjected)) {
- return projected;
- }
-
- final S subRegionBoundary = region.project(subProjected);
- if (subRegionBoundary != null) {
- return hyperplane.toSpace(subRegionBoundary);
- }
- return null;
- }
-
- /** {@inheritDoc} */
- @Override
- public abstract H getHyperplane();
-
- /** {@inheritDoc} */
- @Override
- public abstract HyperplaneBoundedRegion<S> getSubspaceRegion();
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneSubset.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneSubset.java
index 8d3f73b..580350c 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneSubset.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneSubset.java
@@ -23,12 +23,26 @@ import org.apache.commons.geometry.core.RegionLocation;
import org.apache.commons.geometry.core.Sized;
import org.apache.commons.geometry.core.Transform;
-/** Interface representing a subset of the points lying on a hyperplane. Examples include
+/** Interface representing a subset of the points lying in a hyperplane. Examples include
* rays and line segments in Euclidean 2D space and triangular facets in Euclidean 3D space.
* Hyperplane subsets can have finite or infinite size and can represent contiguous regions
* of the hyperplane (as in the examples aboves); multiple, disjoint regions; or the
* {@link Hyperplane#span() entire hyperplane}.
*
+ * <p>This interface is very similar to the {@link org.apache.commons.geometry.core.Region Region}
+ * interface but has slightly different semantics. Whereas {@code Region} instances represent sets
+ * of points that can expand through all of the dimensions of a space, {@code HyperplaneSubset} instances
+ * are constrained to their containing hyperplane and are more accurately defined as {@code Region}s
+ * of the {@code n-1} dimension subspace defined by the hyperplane. This makes the methods of this interface
+ * have slightly different meanings as compared to their {@code Region} counterparts. For example, consider
+ * a triangular facet in Euclidean 3D space. The {@link #getSize()} method of this hyperplane subset does
+ * not return the <em>volume</em> of the instance (which would be {@code 0}) as a regular 3D region would, but
+ * rather returns the <em>area</em> of the 2D polygon defined by the facet. Similarly, the {@link #classify(Point)}
+ * method returns {@link RegionLocation#INSIDE} for points that lie inside of the 2D polygon defined by the
+ * facet, instead of the {@link RegionLocation#BOUNDARY} value that would be expected if the facet was considered
+ * as a true 3D region with zero thickness.
+ * </p>
+ *
* @param <P> Point implementation type
* @see Hyperplane
*/
@@ -51,6 +65,13 @@ public interface HyperplaneSubset<P extends Point<P>> extends Splittable<P, Hype
*/
boolean isEmpty();
+ /** Get the barycenter of the hyperplane subset or null if no barycenter exists or
+ * one exists but is not unique. A barycenter will not exist for empty or
+ * infinite hyperplane subsets.
+ * @return the barycenter of the hyperplane subset or null if no unique barycenter exists
+ */
+ P getBarycenter();
+
/** Classify a point with respect to the subset region. The point is classified as follows:
* <ul>
* <li>{@link RegionLocation#INSIDE INSIDE} - The point lies on the hyperplane
@@ -99,7 +120,9 @@ public interface HyperplaneSubset<P extends Point<P>> extends Splittable<P, Hype
*/
HyperplaneSubset<P> transform(Transform<P> transform);
- /** Convert this instance into a list of convex child subsets.
+ /** Convert this instance into a list of convex child subsets representing the same region.
+ * Implementations are not required to return an optimal convex subdivision of the current
+ * instance. They are free to return whatever subdivision is readily available.
* @return a list of hyperplane convex subsets representing the same subspace
* region as this instance
*/
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/EmbeddingTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/EmbeddingTest.java
index 0274232..ec9a1d2 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/EmbeddingTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/EmbeddingTest.java
@@ -20,10 +20,10 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestPoint1D;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint1D;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
import org.junit.Assert;
import org.junit.Test;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/RegionEmbeddingTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/RegionEmbeddingTest.java
deleted file mode 100644
index 667bea3..0000000
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/RegionEmbeddingTest.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * 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.core;
-
-import org.apache.commons.geometry.core.partition.test.TestPoint1D;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class RegionEmbeddingTest {
-
- private static final double TEST_EPS = 1e-10;
-
- @Test
- public void testGetSize() {
- // arrange
- StubRegionEmbedding finite = new StubRegionEmbedding(2.0);
- StubRegionEmbedding infinite = new StubRegionEmbedding(Double.POSITIVE_INFINITY);
- StubRegionEmbedding nan = new StubRegionEmbedding(Double.NaN);
-
- // act/assert
- Assert.assertEquals(2.0, finite.getSize(), TEST_EPS);
- Assert.assertTrue(finite.isFinite());
- Assert.assertFalse(finite.isInfinite());
-
- GeometryTestUtils.assertPositiveInfinity(infinite.getSize());
- Assert.assertFalse(infinite.isFinite());
- Assert.assertTrue(infinite.isInfinite());
-
- Assert.assertTrue(Double.isNaN(nan.getSize()));
- Assert.assertFalse(nan.isFinite());
- Assert.assertFalse(nan.isInfinite());
- }
-
- private static class StubRegionEmbedding implements RegionEmbedding<TestPoint2D, TestPoint1D> {
-
- private final StubRegion1D subspaceRegion;
-
- StubRegionEmbedding(final double size) {
- subspaceRegion = new StubRegion1D(size);
- }
-
- @Override
- public TestPoint1D toSubspace(TestPoint2D pt) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public TestPoint2D toSpace(TestPoint1D pt) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public Region<TestPoint1D> getSubspaceRegion() {
- return subspaceRegion;
- }
- }
-
- private static class StubRegion1D implements Region<TestPoint1D> {
-
- private final double size;
-
- StubRegion1D(final double size) {
- this.size = size;
- }
-
- @Override
- public double getSize() {
- return size;
- }
-
- @Override
- public boolean isFull() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public boolean isEmpty() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public double getBoundarySize() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public TestPoint1D getBarycenter() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public RegionLocation classify(TestPoint1D pt) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public TestPoint1D project(TestPoint1D pt) {
- throw new UnsupportedOperationException();
- }
- }
-}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/HyperplaneSubsetsTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/HyperplaneSubsetsTest.java
new file mode 100644
index 0000000..5eeeca5
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/HyperplaneSubsetsTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.core.internal;
+
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint1D;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class HyperplaneSubsetsTest {
+
+ @Test
+ public void testClassify() {
+ // arrange
+ TestLine line = TestLine.X_AXIS;
+ StubRegion1D region = new StubRegion1D();
+
+ // act/assert
+ Assert.assertEquals(RegionLocation.INSIDE,
+ HyperplaneSubsets.classifyAgainstEmbeddedRegion(new TestPoint2D(-1, 0), line, region));
+ Assert.assertEquals(RegionLocation.BOUNDARY,
+ HyperplaneSubsets.classifyAgainstEmbeddedRegion(new TestPoint2D(0, 0), line, region));
+
+ Assert.assertEquals(RegionLocation.OUTSIDE,
+ HyperplaneSubsets.classifyAgainstEmbeddedRegion(new TestPoint2D(0, 1), line, region));
+ Assert.assertEquals(RegionLocation.OUTSIDE,
+ HyperplaneSubsets.classifyAgainstEmbeddedRegion(new TestPoint2D(-1, 1), line, region));
+ Assert.assertEquals(RegionLocation.OUTSIDE,
+ HyperplaneSubsets.classifyAgainstEmbeddedRegion(new TestPoint2D(-1, -1), line, region));
+ }
+
+ @Test
+ public void testClosest() {
+ // arrange
+ TestLine line = TestLine.X_AXIS;
+ StubRegion1D region = new StubRegion1D();
+ StubRegion1D emptyRegion = new StubRegion1D(true);
+
+ // act/assert
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0),
+ HyperplaneSubsets.closestToEmbeddedRegion(new TestPoint2D(-1, 0), line, region));
+
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0),
+ HyperplaneSubsets.closestToEmbeddedRegion(new TestPoint2D(0, 0), line, region));
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0),
+ HyperplaneSubsets.closestToEmbeddedRegion(new TestPoint2D(1, 0), line, region));
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0),
+ HyperplaneSubsets.closestToEmbeddedRegion(new TestPoint2D(1, 1), line, region));
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0),
+ HyperplaneSubsets.closestToEmbeddedRegion(new TestPoint2D(1, -1), line, region));
+
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0),
+ HyperplaneSubsets.closestToEmbeddedRegion(new TestPoint2D(-1, 1), line, region));
+ PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0),
+ HyperplaneSubsets.closestToEmbeddedRegion(new TestPoint2D(-1, -1), line, region));
+
+ Assert.assertNull(HyperplaneSubsets.closestToEmbeddedRegion(TestPoint2D.ZERO, line, emptyRegion));
+ }
+
+ /** Stub region implementation. Negative numbers are on the inside of the region.
+ */
+ private static class StubRegion1D implements HyperplaneBoundedRegion<TestPoint1D> {
+
+ private final boolean empty;
+
+ StubRegion1D() {
+ this(false);
+ }
+
+ StubRegion1D(boolean empty) {
+ this.empty = empty;
+ }
+
+ @Override
+ public boolean isFull() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public double getSize() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public double getBoundarySize() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public TestPoint1D getBarycenter() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public RegionLocation classify(TestPoint1D pt) {
+ if (!empty) {
+ int sign = PartitionTestUtils.PRECISION.sign(pt.getX());
+
+ if (sign < 0) {
+ return RegionLocation.INSIDE;
+ } else if (sign == 0) {
+ return RegionLocation.BOUNDARY;
+ }
+ }
+ return RegionLocation.OUTSIDE;
+ }
+
+ @Override
+ public TestPoint1D project(TestPoint1D pt) {
+ return empty ? null : new TestPoint1D(0);
+ }
+
+ @Override
+ public Split<? extends HyperplaneBoundedRegion<TestPoint1D>> split(Hyperplane<TestPoint1D> splitter) {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegionTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegionTest.java
index ccebc49..8c5a6bb 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegionTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegionTest.java
@@ -25,11 +25,11 @@ import org.apache.commons.geometry.core.GeometryTestUtils;
import org.apache.commons.geometry.core.Region;
import org.apache.commons.geometry.core.RegionLocation;
import org.apache.commons.geometry.core.Transform;
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestLineSegment;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
-import org.apache.commons.geometry.core.partition.test.TestTransform2D;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestLineSegment;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.TestTransform2D;
import org.junit.Assert;
import org.junit.Test;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplaneTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplaneTest.java
index 9b41cf0..54c9a7e 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplaneTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplaneTest.java
@@ -17,8 +17,8 @@
package org.apache.commons.geometry.core.partitioning;
import org.apache.commons.geometry.core.Transform;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
import org.junit.Assert;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractRegionEmbeddingHyperplaneSubsetTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractRegionEmbeddingHyperplaneSubsetTest.java
deleted file mode 100644
index 4dc0863..0000000
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractRegionEmbeddingHyperplaneSubsetTest.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * 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.core.partitioning;
-
-import java.util.List;
-
-import org.apache.commons.geometry.core.RegionLocation;
-import org.apache.commons.geometry.core.Transform;
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestPoint1D;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class AbstractRegionEmbeddingHyperplaneSubsetTest {
-
- @Test
- public void testSimpleProperties() {
- // arrange
- StubHyperplaneSubset sub = new StubHyperplaneSubset(1.0);
-
- // act/assert
- Assert.assertTrue(sub.isFull());
- Assert.assertTrue(sub.isEmpty());
- Assert.assertEquals(1.0, sub.getSize(), PartitionTestUtils.EPS);
-
- Assert.assertTrue(sub.isFinite());
- Assert.assertFalse(sub.isInfinite());
- }
-
- @Test
- public void testFiniteAndInfinite() {
- // arrange
- StubHyperplaneSubset finite = new StubHyperplaneSubset(1.0);
- StubHyperplaneSubset inf = new StubHyperplaneSubset(Double.POSITIVE_INFINITY);
- StubHyperplaneSubset nan = new StubHyperplaneSubset(Double.NaN);
-
- // act/assert
- Assert.assertTrue(finite.isFinite());
- Assert.assertFalse(finite.isInfinite());
-
- Assert.assertFalse(inf.isFinite());
- Assert.assertTrue(inf.isInfinite());
-
- Assert.assertFalse(nan.isFinite());
- Assert.assertFalse(nan.isInfinite());
- }
-
- @Test
- public void testSpaceConversions() {
- // arrange
- StubHyperplaneSubset sub = new StubHyperplaneSubset();
-
- // act/assert
- Assert.assertEquals(2.0, sub.toSubspace(new TestPoint2D(2.0, 3.0)).getX(), PartitionTestUtils.EPS);
- PartitionTestUtils.assertPointsEqual(new TestPoint2D(2.0, 0.0), sub.toSpace(new TestPoint1D(2.0)));
- }
-
- @Test
- public void testClassify() {
- // arrange
- StubHyperplaneSubset sub = new StubHyperplaneSubset();
-
- // act/assert
- Assert.assertEquals(RegionLocation.INSIDE, sub.classify(new TestPoint2D(-1, 0)));
- Assert.assertEquals(RegionLocation.BOUNDARY, sub.classify(new TestPoint2D(0, 0)));
-
- Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(new TestPoint2D(0, 1)));
- Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(new TestPoint2D(-1, 1)));
- Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(new TestPoint2D(-1, -1)));
- }
-
- @Test
- public void testClosest() {
- // arrange
- StubHyperplaneSubset sub = new StubHyperplaneSubset();
-
- // act/assert
- PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0), sub.closest(new TestPoint2D(-1, 0)));
-
- PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0), sub.closest(new TestPoint2D(0, 0)));
- PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0), sub.closest(new TestPoint2D(1, 0)));
- PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0), sub.closest(new TestPoint2D(1, 1)));
- PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0), sub.closest(new TestPoint2D(1, -1)));
-
- PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0), sub.closest(new TestPoint2D(-1, 1)));
- PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0), sub.closest(new TestPoint2D(-1, -1)));
- }
-
- @Test
- public void testClosest_nullSubspaceRegionProjection() {
- // arrange
- StubHyperplaneSubset sub = new StubHyperplaneSubset();
- sub.region.projected = null;
-
- // act/assert
- Assert.assertNull(sub.closest(new TestPoint2D(1, 1)));
- }
-
- private static class StubHyperplaneSubset
- extends AbstractRegionEmbeddingHyperplaneSubset<TestPoint2D, TestPoint1D, TestLine> {
-
- private final StubRegion1D region;
-
- StubHyperplaneSubset() {
- this(0);
- }
-
- StubHyperplaneSubset(final double size) {
- this.region = new StubRegion1D(size);
- }
-
- @Override
- public Builder<TestPoint2D> builder() {
- return null;
- }
-
- @Override
- public List<? extends HyperplaneConvexSubset<TestPoint2D>> toConvex() {
- return null;
- }
-
- @Override
- public TestLine getHyperplane() {
- return TestLine.X_AXIS;
- }
-
- @Override
- public HyperplaneBoundedRegion<TestPoint1D> getSubspaceRegion() {
- return region;
- }
-
- @Override
- public Split<StubHyperplaneSubset> split(Hyperplane<TestPoint2D> splitter) {
- return null;
- }
-
- @Override
- public HyperplaneSubset<TestPoint2D> transform(Transform<TestPoint2D> transform) {
- return null;
- }
- }
-
- /** Stub region implementation with some hard-coded values. Negative numbers are
- * on the inside of the region.
- */
- private static class StubRegion1D implements HyperplaneBoundedRegion<TestPoint1D> {
-
- private TestPoint1D projected = new TestPoint1D(0);
-
- private final double size;
-
- StubRegion1D(final double size) {
- this.size = size;
- }
-
- @Override
- public boolean isFull() {
- return true;
- }
-
- @Override
- public boolean isEmpty() {
- return true;
- }
-
- @Override
- public double getSize() {
- return size;
- }
-
- @Override
- public double getBoundarySize() {
- return 0;
- }
-
- @Override
- public TestPoint1D getBarycenter() {
- return null;
- }
-
- @Override
- public RegionLocation classify(TestPoint1D pt) {
- int sign = PartitionTestUtils.PRECISION.sign(pt.getX());
-
- if (sign < 0) {
- return RegionLocation.INSIDE;
- } else if (sign == 0) {
- return RegionLocation.BOUNDARY;
- }
- return RegionLocation.OUTSIDE;
- }
-
- @Override
- public TestPoint1D project(TestPoint1D pt) {
- return projected;
- }
-
- @Override
- public Split<? extends HyperplaneBoundedRegion<TestPoint1D>> split(Hyperplane<TestPoint1D> splitter) {
- throw new UnsupportedOperationException();
- }
- }
-}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperatorTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperatorTest.java
index 8d59095..01f9900 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperatorTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperatorTest.java
@@ -18,11 +18,11 @@ package org.apache.commons.geometry.core.partitioning.bsp;
import java.util.stream.StreamSupport;
-import org.apache.commons.geometry.core.partition.test.AttributeBSPTree;
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
-import org.apache.commons.geometry.core.partition.test.AttributeBSPTree.AttributeNode;
+import org.apache.commons.geometry.core.partitioning.test.AttributeBSPTree;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.AttributeBSPTree.AttributeNode;
import org.junit.Assert;
import org.junit.Test;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeTest.java
index a79cab1..d70a1e9 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeTest.java
@@ -27,16 +27,16 @@ import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.apache.commons.geometry.core.Transform;
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestBSPTree;
-import org.apache.commons.geometry.core.partition.test.TestBSPTree.TestNode;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestLineSegment;
-import org.apache.commons.geometry.core.partition.test.TestLineSegmentCollection;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
-import org.apache.commons.geometry.core.partition.test.TestTransform2D;
import org.apache.commons.geometry.core.partitioning.BoundarySource;
import org.apache.commons.geometry.core.partitioning.bsp.BSPTree.FindNodeCutRule;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestBSPTree;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestLineSegment;
+import org.apache.commons.geometry.core.partitioning.test.TestLineSegmentCollection;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.TestTransform2D;
+import org.apache.commons.geometry.core.partitioning.test.TestBSPTree.TestNode;
import org.junit.Assert;
import org.junit.Test;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeBooleanTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeBooleanTest.java
index 999c79d..dd58d62 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeBooleanTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeBooleanTest.java
@@ -19,11 +19,11 @@ package org.apache.commons.geometry.core.partitioning.bsp;
import java.util.Arrays;
import java.util.function.Supplier;
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestLineSegment;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
-import org.apache.commons.geometry.core.partition.test.TestRegionBSPTree;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestLineSegment;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.TestRegionBSPTree;
import org.junit.Test;
public class AbstractRegionBSPTreeBooleanTest {
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java
index 3bbf64f..59dd3b0 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java
@@ -25,20 +25,20 @@ import java.util.function.Function;
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.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestLineSegment;
-import org.apache.commons.geometry.core.partition.test.TestLineSegmentCollection;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
-import org.apache.commons.geometry.core.partition.test.TestRegionBSPTree;
-import org.apache.commons.geometry.core.partition.test.TestRegionBSPTree.TestRegionNode;
-import org.apache.commons.geometry.core.partition.test.TestTransform2D;
import org.apache.commons.geometry.core.partitioning.BoundarySource;
import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
import org.apache.commons.geometry.core.partitioning.Split;
import org.apache.commons.geometry.core.partitioning.SplitLocation;
import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree.RegionSizeProperties;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestLineSegment;
+import org.apache.commons.geometry.core.partitioning.test.TestLineSegmentCollection;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.TestRegionBSPTree;
+import org.apache.commons.geometry.core.partitioning.test.TestTransform2D;
+import org.apache.commons.geometry.core.partitioning.test.TestRegionBSPTree.TestRegionNode;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitorTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitorTest.java
index 4beadd8..73ae682 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitorTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitorTest.java
@@ -16,12 +16,12 @@
*/
package org.apache.commons.geometry.core.partitioning.bsp;
-import org.apache.commons.geometry.core.partition.test.TestBSPTree;
-import org.apache.commons.geometry.core.partition.test.TestBSPTree.TestNode;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.ClosestFirstVisitor;
import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.FarthestFirstVisitor;
+import org.apache.commons.geometry.core.partitioning.test.TestBSPTree;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.TestBSPTree.TestNode;
import org.junit.Assert;
import org.junit.Test;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/MergeChecker.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/MergeChecker.java
index 55c3611..feb91cd 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/MergeChecker.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/MergeChecker.java
@@ -23,9 +23,9 @@ import java.util.function.Consumer;
import java.util.function.Supplier;
import org.apache.commons.geometry.core.RegionLocation;
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
-import org.apache.commons.geometry.core.partition.test.TestRegionBSPTree;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.TestRegionBSPTree;
import org.junit.Assert;
/** Helper class with a fluent API used to construct assert conditions on tree merge operations.
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundaryTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundaryTest.java
index 5b305ce..9e2bb90 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundaryTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundaryTest.java
@@ -16,9 +16,9 @@
*/
package org.apache.commons.geometry.core.partitioning.bsp;
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestLineSegment;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestLineSegment;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
import org.junit.Assert;
import org.junit.Test;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/AttributeBSPTree.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/AttributeBSPTree.java
similarity index 98%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/AttributeBSPTree.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/AttributeBSPTree.java
index 2b83824..c3b43fa 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/AttributeBSPTree.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/AttributeBSPTree.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
import org.apache.commons.geometry.core.Point;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/PartitionTestUtils.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/PartitionTestUtils.java
similarity index 99%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/PartitionTestUtils.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/PartitionTestUtils.java
index 2172224..8c4512b 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/PartitionTestUtils.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/PartitionTestUtils.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
import java.util.Arrays;
import java.util.List;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestBSPTree.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestBSPTree.java
similarity index 98%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestBSPTree.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestBSPTree.java
index f976fd1..973fa09 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestBSPTree.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestBSPTree.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
import org.apache.commons.geometry.core.partitioning.BoundarySource;
import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLine.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLine.java
similarity index 99%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLine.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLine.java
index 0f4ba88..f5feb5f 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLine.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLine.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegment.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegment.java
similarity index 98%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegment.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegment.java
index 896b286..ab923b5 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegment.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegment.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
import java.util.Arrays;
import java.util.List;
@@ -141,6 +141,12 @@ public class TestLineSegment implements HyperplaneConvexSubset<TestPoint2D> {
/** {@inheritDoc} */
@Override
+ public TestPoint2D getBarycenter() {
+ return line.toSpace(0.5 * (end - start));
+ }
+
+ /** {@inheritDoc} */
+ @Override
public RegionLocation classify(TestPoint2D point) {
if (line.contains(point)) {
final double value = line.toSubspaceValue(point);
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollection.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegmentCollection.java
similarity index 96%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollection.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegmentCollection.java
index a036d9d..4918fc3 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollection.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegmentCollection.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
import java.util.ArrayList;
import java.util.Collections;
@@ -22,10 +22,10 @@ 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.HyperplaneConvexSubset;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
+import org.apache.commons.geometry.core.partitioning.Split;
/** Class containing a collection line segments. This class should only be used for
* testing purposes.
@@ -110,6 +110,12 @@ public class TestLineSegmentCollection implements HyperplaneSubset<TestPoint2D>
/** {@inheritDoc} */
@Override
+ public TestPoint2D getBarycenter() {
+ throw new UnsupportedOperationException();
+ }
+
+ /** {@inheritDoc} */
+ @Override
public Split<TestLineSegmentCollection> split(Hyperplane<TestPoint2D> splitter) {
final List<TestLineSegment> minusList = new ArrayList<>();
final List<TestLineSegment> plusList = new ArrayList<>();
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollectionBuilder.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegmentCollectionBuilder.java
similarity index 98%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollectionBuilder.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegmentCollectionBuilder.java
index 7589574..0e7adf8 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollectionBuilder.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegmentCollectionBuilder.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
import java.util.ArrayList;
import java.util.LinkedList;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint1D.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestPoint1D.java
similarity index 97%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint1D.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestPoint1D.java
index 59ce9a8..400b149 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint1D.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestPoint1D.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
import org.apache.commons.geometry.core.Point;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint2D.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestPoint2D.java
similarity index 97%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint2D.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestPoint2D.java
index f3aebab..190e235 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint2D.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestPoint2D.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
import org.apache.commons.geometry.core.Point;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestRegionBSPTree.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestRegionBSPTree.java
similarity index 98%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestRegionBSPTree.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestRegionBSPTree.java
index 5b46c41..651c117 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestRegionBSPTree.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestRegionBSPTree.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
import org.apache.commons.geometry.core.RegionLocation;
import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestTransform2D.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestTransform2D.java
similarity index 97%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestTransform2D.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestTransform2D.java
index 738e90b..e12e125 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestTransform2D.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestTransform2D.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
import java.util.function.UnaryOperator;
diff --git a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/euclidean/threed/SphereGenerator.java b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/euclidean/threed/SphereGenerator.java
index 246a579..2b60cd4 100644
--- a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/euclidean/threed/SphereGenerator.java
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/euclidean/threed/SphereGenerator.java
@@ -24,7 +24,7 @@ import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.enclosing.EnclosingBall;
import org.apache.commons.geometry.enclosing.SupportBallGenerator;
import org.apache.commons.geometry.enclosing.euclidean.twod.DiskGenerator;
-import org.apache.commons.geometry.euclidean.threed.Plane;
+import org.apache.commons.geometry.euclidean.threed.EmbeddingPlane;
import org.apache.commons.geometry.euclidean.threed.Planes;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
@@ -62,7 +62,7 @@ public class SphereGenerator implements SupportBallGenerator<Vector3D> {
}
final Vector3D vC = support.get(2);
if (support.size() < 4) {
- final Plane p = Planes.fromPoints(vA, vB, vC, precision);
+ final EmbeddingPlane p = Planes.fromPoints(vA, vB, vC, precision).getEmbedding();
final EnclosingBall<Vector2D> disk =
new DiskGenerator().ballOnSupport(Arrays.asList(p.toSubspace(vA),
p.toSubspace(vB),
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
new file mode 100644
index 0000000..307d98b
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractBounds.java
@@ -0,0 +1,159 @@
+/*
+ * 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.euclidean;
+
+import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+
+/** Base class representing an axis-aligned bounding box with minimum and maximum bounding points.
+ * @param <P> Point implementation type
+ * @param <B> Bounds implementation type
+ */
+public abstract class AbstractBounds<
+ P extends EuclideanVector<P>,
+ B extends AbstractBounds<P, B>> {
+
+ /** Minimum point. */
+ private final P min;
+
+ /** Maximum point. */
+ private final P max;
+
+ /** Simple constructor. Callers are responsible for ensuring that all coordinate values are finite and
+ * that all values in {@code min} are less than or equal to their corresponding values in {@code max}.
+ * No validation is performed.
+ * @param min minimum point
+ * @param max maximum point
+ */
+ protected AbstractBounds(final P min, final P max) {
+ this.min = min;
+ this.max = max;
+ }
+
+ /** Get the minimum point.
+ * @return the minimum point
+ */
+ public P getMin() {
+ return min;
+ }
+
+ /** Get the maximum point.
+ * @return the maximum point
+ */
+ public P getMax() {
+ return max;
+ }
+
+ /** Get the diagonal of the bounding box. The return value is a vector pointing from
+ * {@code min} to {@code max} and contains the size of the box along each coordinate axis.
+ * @return the diagonal vector of the bounding box
+ */
+ public P getDiagonal() {
+ return min.vectorTo(max);
+ }
+
+ /** Return the center of the bounding box.
+ * @return the center of the bounding box
+ */
+ public P getBarycenter() {
+ return min.lerp(max, 0.5);
+ }
+
+ /** Return true if the bounding box has non-zero size along each coordinate axis, as
+ * evaluated by the given precision context.
+ * @param precision precision context used for floating point comparisons
+ * @return true if the bounding box has non-zero size along each coordinate axis
+ */
+ public abstract boolean hasSize(DoublePrecisionContext precision);
+
+ /** Return true if the given point is strictly within or on the boundary of the bounding box.
+ * In other words, true if returned if <code>p<sub>t</sub> >= min<sub>t</sub></code> and
+ * <code>p<sub>t</sub> <= max<sub>t</sub></code> for each coordinate value <code>t</code>.
+ * Floating point comparisons are strict; values are considered equal only if they match exactly.
+ * @param pt the point to check
+ * @return true if the given point is strictly within or on the boundary of the instance
+ * @see #contains(EuclideanVector, DoublePrecisionContext)
+ */
+ public abstract boolean contains(P pt);
+
+ /** Return true if the given point is within or on the boundary of the bounding box, using the given
+ * precision context for floating point comparisons. This is similar to {@link #contains(EuclideanVector)}
+ * but allows points that may be strictly outside of the box due to floating point errors to be considered
+ * inside.
+ * @param pt the point to check
+ * @param precision precision context used to compare floating point values
+ * @return if the given point is within or on the boundary of the bounds, as determined
+ * by the given precision context
+ * @see #contains(EuclideanVector, DoublePrecisionContext)
+ */
+ public abstract boolean contains(P pt, DoublePrecisionContext precision);
+
+ /** Return true if any point on the interior or boundary of this instance is also considered to be
+ * on the interior or boundary of the argument. Specifically, true is returned if
+ * <code>aMin<sub>t</sub> <= bMax<sub>t</sub></code> and <code>aMax<sub>t</sub> >= bMin<sub>t</sub></code>
+ * for all coordinate values {@code t}, where {@code a} is the current instance and {@code b} is the argument.
+ * Floating point comparisons are strict; values are considered equal only if they match exactly.
+ * @param other bounding box to intersect with
+ * @return true if the bounds intersect
+ */
+ public abstract boolean intersects(B other);
+
+ /** Return the intersection of this bounding box and the argument, or null if no intersection exists.
+ * Floating point comparisons are strict; values are considered equal only if they match exactly. Note
+ * this this method may return bounding boxes with zero size in one or more coordinate axes.
+ * @param other bounding box to intersect with
+ * @return the intersection of this instance and the argument, or null if no such intersection
+ * exists
+ * @see #intersects(AbstractBounds)
+ */
+ public abstract B intersection(B other);
+
+ /** Return a hyperplane-bounded region containing the same points as this instance.
+ * @param precision precision context used for floating point comparisons in the returned
+ * region instance
+ * @return a hyperplane-bounded region containing the same points as this instance
+ */
+ public abstract HyperplaneBoundedRegion<P> toRegion(DoublePrecisionContext precision);
+
+ /** Return true if the current instance and argument are considered equal as evaluated by the
+ * given precision context. Bounds are considered equal if they contain equivalent min and max
+ * points.
+ * @param other bounds to compare with
+ * @param precision precision context to compare floating point numbers
+ * @return true if this instance is equivalent to the argument, as evaluated by the given
+ * precision context
+ * @see EuclideanVector#eq(EuclideanVector, DoublePrecisionContext)
+ */
+ public boolean eq(final B other, final DoublePrecisionContext precision) {
+ return min.eq(other.getMin(), precision) &&
+ max.eq(other.getMax(), precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(getClass().getSimpleName())
+ .append("[min= ")
+ .append(min)
+ .append(", max= ")
+ .append(max)
+ .append(']');
+
+ return sb.toString();
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java
index 0463139..1e641b8 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java
@@ -80,7 +80,7 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
/** Get the direction of the hyperplane's plus side.
* @return the hyperplane direction
*/
- public Vector1D getDirection() {
+ public Vector1D.Unit getDirection() {
return positiveFacing ? Vector1D.Unit.PLUS : Vector1D.Unit.MINUS;
}
@@ -272,7 +272,7 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
/** {@inheritDoc}
*
- * <p>This method simply returns false.</p>
+ * <p>This method always returns {@code false}.</p>
*/
@Override
public boolean isFull() {
@@ -281,7 +281,7 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
/** {@inheritDoc}
*
- * <p>This method simply returns false.</p>
+ * <p>This method always returns {@code false}.</p>
*/
@Override
public boolean isEmpty() {
@@ -290,7 +290,7 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
/** {@inheritDoc}
*
- * <p>This method simply returns false.</p>
+ * <p>This method always returns {@code false}.</p>
*/
@Override
public boolean isInfinite() {
@@ -299,7 +299,7 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
/** {@inheritDoc}
*
- * <p>This method simply returns true.</p>
+ * <p>This method always returns {@code true}.</p>
*/
@Override
public boolean isFinite() {
@@ -308,7 +308,7 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
/** {@inheritDoc}
*
- * <p>This method simply returns {@code 0}.</p>
+ * <p>This method always returns {@code 0}.</p>
*/
@Override
public double getSize() {
@@ -316,6 +316,15 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
}
/** {@inheritDoc}
+ *
+ * <p>This method returns the point for the defining hyperplane.</p>
+ */
+ @Override
+ public Vector1D getBarycenter() {
+ return hyperplane.getPoint();
+ }
+
+ /** {@inheritDoc}
*
* <p>This method returns {@link RegionLocation#BOUNDARY} if the
* point is on the hyperplane and {@link RegionLocation#OUTSIDE}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractConvexPolygon3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractConvexPolygon3D.java
new file mode 100644
index 0000000..8d69812
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractConvexPolygon3D.java
@@ -0,0 +1,451 @@
+/*
+ * 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.euclidean.threed;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.internal.Vectors;
+import org.apache.commons.geometry.euclidean.threed.line.Lines3D;
+import org.apache.commons.geometry.euclidean.twod.ConvexArea;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+
+/** Abstract base class for {@link ConvexPolygon3D} implementations.
+ */
+abstract class AbstractConvexPolygon3D extends AbstractPlaneSubset implements ConvexPolygon3D {
+
+ /** Plane containing the convex polygon. */
+ private final Plane plane;
+
+ /** Simple constructor.
+ * @param plane the plane containing the convex polygon
+ */
+ AbstractConvexPolygon3D(final Plane plane) {
+ this.plane = plane;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Plane getPlane() {
+ return plane;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method always returns {@code false}.</p>
+ */
+ @Override
+ public boolean isFull() {
+ return false;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method always returns {@code false}.</p>
+ */
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getSize() {
+ // see http://geomalgorithms.com/a01-_area.html#3D-Planar-Polygons
+ final List<Vector3D> vertices = getVertices();
+
+ double crossSumX = 0.0;
+ double crossSumY = 0.0;
+ double crossSumZ = 0.0;
+
+ Vector3D prevPt = vertices.get(vertices.size() - 1);
+ Vector3D cross;
+ for (final Vector3D curPt : vertices) {
+ cross = prevPt.cross(curPt);
+
+ crossSumX += cross.getX();
+ crossSumY += cross.getY();
+ crossSumZ += cross.getZ();
+
+ prevPt = curPt;
+ }
+
+ return 0.5 * plane.getNormal().dot(Vector3D.of(crossSumX, crossSumY, crossSumZ));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D getBarycenter() {
+ final List<Vector3D> vertices = getVertices();
+
+ double areaSum = 0.0;
+ double scaledBarycenterSumX = 0.0;
+ double scaledBarycenterSumY = 0.0;
+ double scaledBarycenterSumZ = 0.0;
+
+ Iterator<Vector3D> it = vertices.iterator();
+
+ Vector3D startPt = it.next();
+
+ Vector3D prevPt = it.next();
+ Vector3D curPt;
+
+ Vector3D prevVec = startPt.vectorTo(prevPt);
+ Vector3D curVec = null;
+
+ double triArea;
+ Vector3D triBarycenter;
+ while (it.hasNext()) {
+ curPt = it.next();
+ curVec = startPt.vectorTo(curPt);
+
+ triArea = 0.5 * prevVec.cross(curVec).norm();
+ triBarycenter = Vector3D.centroid(startPt, prevPt, curPt);
+
+ areaSum += triArea;
+
+ scaledBarycenterSumX += triArea * triBarycenter.getX();
+ scaledBarycenterSumY += triArea * triBarycenter.getY();
+ scaledBarycenterSumZ += triArea * triBarycenter.getZ();
+
+ prevPt = curPt;
+ prevVec = curVec;
+ }
+
+ if (areaSum > 0.0) {
+ final double scale = 1 / areaSum;
+ return Vector3D.of(
+ scale * scaledBarycenterSumX,
+ scale * scaledBarycenterSumY,
+ scale * scaledBarycenterSumZ
+ );
+ }
+
+ // zero area, which means that the points are all linear; return the point midway between the
+ // min and max points
+ final Vector3D min = Vector3D.min(vertices);
+ final Vector3D max = Vector3D.max(vertices);
+
+ return min.lerp(max, 0.5);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Bounds3D getBounds() {
+ return Bounds3D.from(getVertices());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public RegionLocation classify(final Vector3D pt) {
+ if (plane.contains(pt)) {
+ final List<Vector3D> vertices = getVertices();
+ final DoublePrecisionContext precision = plane.getPrecision();
+
+ final Vector3D normal = plane.getNormal();
+ Vector3D edgeVec;
+ Vector3D edgePlusVec;
+ Vector3D testVec;
+
+ Vector3D offsetVec;
+ double offsetSign;
+ double offset;
+ int cmp;
+
+ boolean onBoundary = false;
+
+ Vector3D startVertex = vertices.get(vertices.size() - 1);
+ for (final Vector3D nextVertex : vertices) {
+
+ edgeVec = startVertex.vectorTo(nextVertex);
+ edgePlusVec = edgeVec.cross(normal);
+
+ testVec = startVertex.vectorTo(pt);
+
+ offsetVec = testVec.reject(edgeVec);
+ offsetSign = Math.signum(offsetVec.dot(edgePlusVec));
+ offset = offsetSign * offsetVec.norm();
+
+ cmp = precision.compare(offset, 0.0);
+ if (cmp > 0) {
+ // the point is on the plus side (outside) of a boundary
+ return RegionLocation.OUTSIDE;
+ } else if (cmp == 0) {
+ onBoundary = true;
+ }
+
+ startVertex = nextVertex;
+ }
+
+ if (onBoundary) {
+ // the point is not on the outside of any boundaries and is directly on at least one
+ return RegionLocation.BOUNDARY;
+ }
+
+ // the point is on the inside of all boundaries
+ return RegionLocation.INSIDE;
+ }
+
+ // the point is not on the plane
+ return RegionLocation.OUTSIDE;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D closest(final Vector3D pt) {
+ final Vector3D normal = plane.getNormal();
+ final DoublePrecisionContext precision = plane.getPrecision();
+
+ final List<Vector3D> vertices = getVertices();
+
+ final Vector3D projPt = plane.project(pt);
+
+ Vector3D edgeVec;
+ Vector3D edgePlusVec;
+ Vector3D testVec;
+
+ Vector3D offsetVec;
+ double offsetSign;
+ double offset;
+ int cmp;
+
+ Vector3D boundaryVec;
+ double boundaryPointT;
+ Vector3D boundaryPoint = null;
+ double boundaryPointDistSq;
+
+ double closestBoundaryPointDistSq = Double.POSITIVE_INFINITY;
+ Vector3D closestBoundaryPoint = null;
+
+ Vector3D startVertex = vertices.get(vertices.size() - 1);
+ for (final Vector3D nextVertex : vertices) {
+
+ edgeVec = startVertex.vectorTo(nextVertex);
+ edgePlusVec = edgeVec.cross(normal);
+
+ testVec = startVertex.vectorTo(projPt);
+
+ offsetVec = testVec.reject(edgeVec);
+ offsetSign = Math.signum(offsetVec.dot(edgePlusVec));
+ offset = offsetSign * offsetVec.norm();
+
+ cmp = precision.compare(offset, 0.0);
+ if (cmp >= 0) {
+ // the point is on directly on the boundary or on its plus side; project the point onto the
+ // boundary, taking care to restrict the point to the actual extent of the boundary,
+ // and select the point with the shortest distance
+ boundaryVec = testVec.subtract(offsetVec);
+ boundaryPointT =
+ Math.signum(boundaryVec.dot(edgeVec)) * (boundaryVec.norm() / Vectors.checkedNorm(edgeVec));
+ boundaryPointT = Math.max(0, Math.min(1, boundaryPointT));
+
+ boundaryPoint = startVertex.lerp(nextVertex, boundaryPointT);
+
+ boundaryPointDistSq = boundaryPoint.distanceSq(projPt);
+ if (boundaryPointDistSq < closestBoundaryPointDistSq) {
+ closestBoundaryPointDistSq = boundaryPointDistSq;
+ closestBoundaryPoint = boundaryPoint;
+ }
+ }
+
+ startVertex = nextVertex;
+ }
+
+ if (closestBoundaryPoint != null) {
+ // the point is on the outside of the polygon; return the closest point on the boundary
+ return closestBoundaryPoint;
+ }
+
+ // the projected point is on the inside of all boundaries and therefore on the inside of the subset
+ return projPt;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public PlaneConvexSubset.Embedded getEmbedded() {
+ final EmbeddingPlane embeddingPlane = plane.getEmbedding();
+ final List<Vector2D> subspaceVertices = embeddingPlane.toSubspace(getVertices());
+ final ConvexArea area = ConvexArea.convexPolygonFromVertices(subspaceVertices,
+ embeddingPlane.getPrecision());
+
+ return new EmbeddedAreaPlaneConvexSubset(embeddingPlane, area);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Split<PlaneConvexSubset> split(final Hyperplane<Vector3D> splitter) {
+ final Plane splitterPlane = (Plane) splitter;
+ final List<Vector3D> vertices = getVertices();
+
+ final int size = vertices.size();
+
+ int minusPlusTransitionIdx = -1;
+ Vector3D minusPlusInsertVertex = null;
+
+ int plusMinusTransitionIdx = -1;
+ Vector3D plusMinusInsertVertex = null;
+
+ int transitionCount = 0;
+
+ Vector3D curVertex;
+ HyperplaneLocation curLoc;
+
+ int lastSideIdx = -1;
+ Vector3D lastSideVertex = null;
+ HyperplaneLocation lastSideLoc = null;
+
+ int lastBoundaryIdx = -1;
+
+ for (int i = 0; i <= size || transitionCount == 1; ++i) {
+
+ curVertex = vertices.get(i % size);
+ curLoc = splitter.classify(curVertex);
+
+ if (lastSideLoc == HyperplaneLocation.MINUS && curLoc == HyperplaneLocation.PLUS) {
+ // transitioned from minus side to plus side
+ minusPlusTransitionIdx = Math.max(lastSideIdx, lastBoundaryIdx);
+ ++transitionCount;
+
+ if (lastBoundaryIdx < 0) {
+ // no shared boundary point; compute a new vertex
+ minusPlusInsertVertex = splitterPlane.intersection(
+ Lines3D.fromPoints(lastSideVertex, curVertex, splitterPlane.getPrecision()));
+ }
+ } else if (lastSideLoc == HyperplaneLocation.PLUS && curLoc == HyperplaneLocation.MINUS) {
+ // transitioned from plus side to minus side
+ plusMinusTransitionIdx = Math.max(lastSideIdx, lastBoundaryIdx);
+ ++transitionCount;
+
+ if (lastBoundaryIdx < 0) {
+ // no shared boundary point; compute a new vertex
+ plusMinusInsertVertex = splitterPlane.intersection(
+ Lines3D.fromPoints(lastSideVertex, curVertex, splitterPlane.getPrecision()));
+ }
+ }
+
+ if (curLoc == HyperplaneLocation.ON) {
+ lastBoundaryIdx = i;
+ } else {
+ lastBoundaryIdx = -1;
+
+ lastSideIdx = i;
+ lastSideVertex = curVertex;
+ lastSideLoc = curLoc;
+ }
+ }
+
+ if (minusPlusTransitionIdx > -1 && plusMinusTransitionIdx > -1) {
+ // we've split; compute the vertex list for each side
+ final List<Vector3D> minusVertices = buildPolygonSplitVertexList(
+ plusMinusTransitionIdx, plusMinusInsertVertex,
+ minusPlusTransitionIdx, minusPlusInsertVertex, vertices);
+ final List<Vector3D> plusVertices = buildPolygonSplitVertexList(
+ minusPlusTransitionIdx, minusPlusInsertVertex,
+ plusMinusTransitionIdx, plusMinusInsertVertex, vertices);
+
+ // delegate back to the Planes factory methods to determine the concrete types
+ // for each side of the split
+ return new Split<>(
+ Planes.fromConvexPlanarVertices(plane, minusVertices),
+ Planes.fromConvexPlanarVertices(plane, plusVertices));
+
+ } else if (lastSideLoc == HyperplaneLocation.PLUS) {
+ // we lie entirely on the plus side of the splitter
+ return new Split<>(null, this);
+ } else if (lastSideLoc == HyperplaneLocation.MINUS) {
+ // we lie entirely on the minus side of the splitter
+ return new Split<>(this, null);
+ }
+
+ // we lie entirely on the splitter
+ return new Split<>(null, null);
+ }
+
+ /** Internal method for building a vertex list for one side of a split result. The method is
+ * designed to make the fewest allocations possible.
+ * @param enterIdx the index of the vertex from {@code vertices} immediately before the polygon transitioned
+ * to being fully entered into this side of the split result. If no point from {@code vertices} lay
+ * directly on the splitting plane while entering this side and a new vertex had to be computed for the
+ * split result, then this index will be the last vertex on the opposite side of the split. If a vertex
+ * did lie directly on the splitting plane, then this index will point to that vertex.
+ * @param newEnterPt the newly-computed point to be added as the first vertex in the split result; may
+ * be null if no such point exists
+ * @param exitIdx the index of the vertex from {@code vertices} immediately before the polygon transitioned
+ * to being fully exited from this side of the split result. If no point from {@code vertices} lay
+ * directly on the splitting plane while exiting this side and a new vertex had to be computed for the
+ * split result, then this index will be the last vertex on the this side of the split. If a vertex did
+ * lie directly on the splitting plane, then this index will point to that vertex.
+ * @param newExitPt the newly-computed point to be added as the last vertex in the split result; may
+ * be null if no such point exists
+ * @param vertices the original list of vertices that this split result originated from; this list is
+ * not modified by this operation
+ * @return the list of vertices for the split result
+ */
+ private List<Vector3D> buildPolygonSplitVertexList(final int enterIdx, final Vector3D newEnterPt,
+ final int exitIdx, final Vector3D newExitPt, final List<Vector3D> vertices) {
+
+ final int size = vertices.size();
+
+ final boolean hasNewEnterPt = newEnterPt != null;
+ final boolean hasNewExitPt = newExitPt != null;
+
+ final int startIdx = (hasNewEnterPt ? enterIdx + 1 : enterIdx) % size;
+ final int endIdx = exitIdx % size;
+
+ final boolean hasWrappedIndices = endIdx < startIdx;
+
+ final int resultSize = (hasWrappedIndices ? endIdx + size : endIdx) - startIdx + 1;
+ final List<Vector3D> result = new ArrayList<>(resultSize);
+
+ if (hasNewEnterPt) {
+ result.add(newEnterPt);
+ }
+
+ if (hasWrappedIndices) {
+ result.addAll(vertices.subList(startIdx, size));
+ result.addAll(vertices.subList(0, endIdx + 1));
+ } else {
+ result.addAll(vertices.subList(startIdx, endIdx + 1));
+ }
+
+ if (hasNewExitPt) {
+ result.add(newExitPt);
+ }
+
+ return result;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(getClass().getSimpleName())
+ .append("[normal= ")
+ .append(getPlane().getNormal())
+ .append(", vertices= ")
+ .append(getVertices())
+ .append(']');
+
+ return sb.toString();
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractEmbeddedRegionPlaneSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractEmbeddedRegionPlaneSubset.java
new file mode 100644
index 0000000..b19f3c4
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractEmbeddedRegionPlaneSubset.java
@@ -0,0 +1,137 @@
+/*
+ * 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.euclidean.threed;
+
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.internal.HyperplaneSubsets;
+import org.apache.commons.geometry.euclidean.twod.BoundarySource2D;
+import org.apache.commons.geometry.euclidean.twod.Bounds2D;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+
+/** Base class for {@link PlaneSubset} implementations that use an embedded subspace region
+ * to define their plane subsets.
+ */
+abstract class AbstractEmbeddedRegionPlaneSubset extends AbstractPlaneSubset implements PlaneSubset.Embedded {
+
+ /** The plane containing the embedded region. */
+ private final EmbeddingPlane plane;
+
+ /** Construct a new instance in the given plane.
+ * @param plane plane containing the subset
+ */
+ AbstractEmbeddedRegionPlaneSubset(final EmbeddingPlane plane) {
+ this.plane = plane;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public EmbeddingPlane getPlane() {
+ return plane;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public EmbeddingPlane getHyperplane() {
+ return plane;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isFull() {
+ return getSubspaceRegion().isFull();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isEmpty() {
+ return getSubspaceRegion().isEmpty();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getSize() {
+ return getSubspaceRegion().getSize();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D getBarycenter() {
+ final Vector2D subspaceBarycenter = getSubspaceRegion().getBarycenter();
+ if (subspaceBarycenter != null) {
+ return getPlane().toSpace(subspaceBarycenter);
+ }
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D toSpace(final Vector2D pt) {
+ return plane.toSpace(pt);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector2D toSubspace(final Vector3D pt) {
+ return plane.toSubspace(pt);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public RegionLocation classify(final Vector3D pt) {
+ return HyperplaneSubsets.classifyAgainstEmbeddedRegion(pt, plane, getSubspaceRegion());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D closest(final Vector3D pt) {
+ return HyperplaneSubsets.closestToEmbeddedRegion(pt, plane, getSubspaceRegion());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(getClass().getSimpleName())
+ .append("[plane= ")
+ .append(getPlane())
+ .append(", subspaceRegion= ")
+ .append(getSubspaceRegion())
+ .append(']');
+
+ return sb.toString();
+ }
+
+ /** Compute 3D bounds from a subspace boundary source.
+ * @param src subspace boundary source
+ * @return 3D bounds from the given embedded subspace boundary source or null
+ * if no valid bounds could be determined
+ */
+ protected Bounds3D getBoundsFromSubspace(final BoundarySource2D src) {
+ final Bounds2D subspaceBounds = src.getBounds();
+ if (subspaceBounds != null) {
+ final Vector3D min = plane.toSpace(subspaceBounds.getMin());
+ final Vector3D max = plane.toSpace(subspaceBounds.getMax());
+
+ return Bounds3D.builder()
+ .add(min)
+ .add(max)
+ .build();
+ }
+
+ return null;
+ }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircleSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubset.java
similarity index 54%
copy from commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircleSubset.java
copy to commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubset.java
index 5fbf0f5..6e9d017 100644
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircleSubset.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubset.java
@@ -14,102 +14,81 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.spherical.twod;
+package org.apache.commons.geometry.euclidean.threed;
-import java.util.List;
import java.util.Objects;
-import org.apache.commons.geometry.core.partitioning.AbstractRegionEmbeddingHyperplaneSubset;
import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.spherical.oned.Point1S;
+import org.apache.commons.geometry.euclidean.threed.line.Line3D;
+import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
-/** Class representing a subset of the points in a great circle.
- * @see GreatCircles
+/** Abstract base class for {@link PlaneSubset} implementations.
*/
-public abstract class GreatCircleSubset
- extends AbstractRegionEmbeddingHyperplaneSubset<Point2S, Point1S, GreatCircle> {
- /** The great circle defining this instance. */
- private final GreatCircle circle;
-
- /** Simple constructor.
- * @param circle great circle defining this instance
- */
- GreatCircleSubset(final GreatCircle circle) {
- this.circle = circle;
- }
-
- /** Get the great circle defining this instance.
- * @return the great circle defining this instance
- * @see #getHyperplane()
- */
- public GreatCircle getCircle() {
- return circle;
- }
+abstract class AbstractPlaneSubset implements PlaneSubset {
/** {@inheritDoc} */
@Override
- public GreatCircle getHyperplane() {
- return getCircle();
+ public Plane getHyperplane() {
+ return getPlane();
}
/** {@inheritDoc} */
@Override
- public abstract List<GreatArc> toConvex();
+ public HyperplaneSubset.Builder<Vector3D> builder() {
+ return new Builder(getPlane());
+ }
/** {@inheritDoc} */
@Override
- public HyperplaneSubset.Builder<Point2S> builder() {
- return new Builder(circle);
+ public Vector3D intersection(final Line3D line) {
+ return Planes.intersection(this, line);
}
- /** Return the object used to perform floating point comparisons, which is the
- * same object used by the underlying {@link GreatCircle}.
- * @return precision object used to perform floating point comparisons.
- */
- public DoublePrecisionContext getPrecision() {
- return circle.getPrecision();
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D intersection(LineConvexSubset3D lineSubset) {
+ return Planes.intersection(this, lineSubset);
}
/** Internal implementation of the {@link HyperplaneSubset.Builder} interface. In cases where only a single
* convex subset is given to the builder, this class returns the convex subset instance directly. In all other
- * cases, an {@link EmbeddedTreeGreatCircleSubset} is used to construct the final subset.
+ * cases, an {@link EmbeddedTreePlaneSubset} is used to construct the final subset.
*/
- private static final class Builder implements HyperplaneSubset.Builder<Point2S> {
- /** Great circle that a subset is being constructed for. */
- private final GreatCircle circle;
+ private static final class Builder implements HyperplaneSubset.Builder<Vector3D> {
+ /** Plane that a subset is being constructed for. */
+ private final Plane plane;
/** Embedded tree subset. */
- private EmbeddedTreeGreatCircleSubset treeSubset;
+ private EmbeddedTreePlaneSubset treeSubset;
/** Convex subset added as the first subset to the builder. This is returned directly if
* no other subsets are added.
*/
- private GreatArc convexSubset;
+ private PlaneConvexSubset convexSubset;
- /** Create a new subset builder for the given great circle.
- * @param circle great circle to build a subset for
+ /** Create a new subset builder for the given plane.
+ * @param plane plane to build a subset for
*/
- Builder(final GreatCircle circle) {
- this.circle = circle;
+ Builder(final Plane plane) {
+ this.plane = plane;
}
/** {@inheritDoc} */
@Override
- public void add(final HyperplaneSubset<Point2S> sub) {
+ public void add(final HyperplaneSubset<Vector3D> sub) {
addInternal(sub);
}
/** {@inheritDoc} */
@Override
- public void add(final HyperplaneConvexSubset<Point2S> sub) {
+ public void add(final HyperplaneConvexSubset<Vector3D> sub) {
addInternal(sub);
}
/** {@inheritDoc} */
@Override
- public GreatCircleSubset build() {
+ public PlaneSubset build() {
// return the convex subset directly if that was all we were given
if (convexSubset != null) {
return convexSubset;
@@ -120,13 +99,13 @@ public abstract class GreatCircleSubset
/** Internal method for adding hyperplane subsets to this builder.
* @param sub the hyperplane subset to add; may be either convex or non-convex
*/
- private void addInternal(final HyperplaneSubset<Point2S> sub) {
+ private void addInternal(final HyperplaneSubset<Vector3D> sub) {
Objects.requireNonNull(sub, "Hyperplane subset must not be null");
- if (sub instanceof GreatArc) {
- addConvexSubset((GreatArc) sub);
- } else if (sub instanceof EmbeddedTreeGreatCircleSubset) {
- addTreeSubset((EmbeddedTreeGreatCircleSubset) sub);
+ if (sub instanceof PlaneConvexSubset) {
+ addConvexSubset((PlaneConvexSubset) sub);
+ } else if (sub instanceof EmbeddedTreePlaneSubset) {
+ addTreeSubset((EmbeddedTreePlaneSubset) sub);
} else {
throw new IllegalArgumentException("Unsupported hyperplane subset type: " + sub.getClass().getName());
}
@@ -135,8 +114,8 @@ public abstract class GreatCircleSubset
/** Add a convex subset to the builder.
* @param convex convex subset to add
*/
- private void addConvexSubset(final GreatArc convex) {
- GreatCircles.validateGreatCirclesEquivalent(circle, convex.getCircle());
+ private void addConvexSubset(final PlaneConvexSubset convex) {
+ Planes.validatePlanesEquivalent(plane, convex.getPlane());
if (treeSubset == null && convexSubset == null) {
convexSubset = convex;
@@ -148,7 +127,7 @@ public abstract class GreatCircleSubset
/** Add an embedded tree subset to the builder.
* @param tree embedded tree subset to add
*/
- private void addTreeSubset(final EmbeddedTreeGreatCircleSubset tree) {
+ private void addTreeSubset(final EmbeddedTreePlaneSubset tree) {
// no need to validate the line here since the add() method does that for us
getTreeSubset().add(tree);
}
@@ -156,9 +135,9 @@ public abstract class GreatCircleSubset
/** Get the tree subset for the builder, creating it if needed.
* @return the tree subset for the builder
*/
- private EmbeddedTreeGreatCircleSubset getTreeSubset() {
+ private EmbeddedTreePlaneSubset getTreeSubset() {
if (treeSubset == null) {
- treeSubset = new EmbeddedTreeGreatCircleSubset(circle);
+ treeSubset = new EmbeddedTreePlaneSubset(plane.getEmbedding());
if (convexSubset != null) {
treeSubset.add(convexSubset);
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3D.java
index 791d7f4..7a4eb19 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3D.java
@@ -19,6 +19,7 @@ package org.apache.commons.geometry.euclidean.threed;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
+import java.util.stream.Stream;
import org.apache.commons.geometry.core.partitioning.BoundarySource;
import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
@@ -41,6 +42,17 @@ public interface BoundarySource3D extends BoundarySource<PlaneConvexSubset>, Lin
return tree;
}
+ /** Return the boundaries of this instance as a stream of {@link Triangle3D}
+ * instances. An {@link IllegalStateException} exception is thrown while reading
+ * from the stream if any boundary cannot be converted to a triangle (i.e. if it
+ * has infinite size).
+ * @return a stream of triangles representing the instance boundaries
+ * @see org.apache.commons.geometry.euclidean.threed.PlaneSubset#toTriangles()
+ */
+ default Stream<Triangle3D> triangleStream() {
+ return boundaryStream().flatMap(b -> b.toTriangles().stream());
+ }
+
/** {@inheritDoc} */
@Override
default List<LinecastPoint3D> linecast(final LineConvexSubset3D subset) {
@@ -53,6 +65,15 @@ public interface BoundarySource3D extends BoundarySource<PlaneConvexSubset>, Lin
return new BoundarySourceLinecaster3D(this).linecastFirst(subset);
}
+ /** Get a {@link Bounds3D} object defining the axis-aligned box containing all vertices
+ * in the boundaries for this instance. Null is returned if any boundaries are infinite
+ * or no vertices were found.
+ * @return the bounding box for this instance or null if no valid bounds could be determined
+ */
+ default Bounds3D getBounds() {
+ return new BoundarySourceBoundsBuilder3D().getBounds(this);
+ }
+
/** Return a {@link BoundarySource3D} instance containing the given boundaries.
* @param boundaries boundaries to include in the boundary source
* @return a boundary source containing the given boundaries
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceBoundsBuilder3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceBoundsBuilder3D.java
new file mode 100644
index 0000000..b0164dd
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceBoundsBuilder3D.java
@@ -0,0 +1,57 @@
+/*
+ * 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.euclidean.threed;
+
+import java.util.Iterator;
+import java.util.stream.Stream;
+
+
+/** Class used to construct {@link Bounds3D} instances representing the min and
+ * max points present in a {@link BoundarySource3D}. The implementation examines
+ * the vertices of each boundary in turn. Null is returned if any boundaries are
+ * infinite or no vertices are present.
+ */
+class BoundarySourceBoundsBuilder3D {
+
+ /** Get a {@link Bounds3D} instance containing all vertices in the given boundary source.
+ * Null is returned if any encountered boundaries were not finite or no vertices were found.
+ * @param src the boundary source to compute the bounds of
+ * @return the bounds of the argument or null if no valid bounds could be determined
+ */
+ public Bounds3D getBounds(final BoundarySource3D src) {
+ final Bounds3D.Builder builder = Bounds3D.builder();
+
+ try (Stream<PlaneConvexSubset> stream = src.boundaryStream()) {
+ final Iterator<PlaneConvexSubset> it = stream.iterator();
+
+ PlaneConvexSubset boundary;
+ while (it.hasNext()) {
+ boundary = it.next();
+
+ if (!boundary.isFinite()) {
+ return null;
+ }
+
+ builder.addAll(boundary.getVertices());
+ }
+ }
+
+ return builder.containsBounds() ?
+ builder.build() :
+ null;
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecaster3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecaster3D.java
index 56caf89..3129756 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecaster3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecaster3D.java
@@ -46,20 +46,22 @@ final class BoundarySourceLinecaster3D implements Linecastable3D {
/** {@inheritDoc} */
@Override
public List<LinecastPoint3D> linecast(final LineConvexSubset3D subset) {
- final List<LinecastPoint3D> results = getIntersectionStream(subset)
- .collect(Collectors.toCollection(ArrayList::new));
+ try (Stream<LinecastPoint3D> stream = getIntersectionStream(subset)) {
- LinecastPoint3D.sortAndFilter(results);
+ final List<LinecastPoint3D> results = stream.collect(Collectors.toCollection(ArrayList::new));
+ LinecastPoint3D.sortAndFilter(results);
- return results;
+ return results;
+ }
}
/** {@inheritDoc} */
@Override
public LinecastPoint3D linecastFirst(final LineConvexSubset3D subset) {
- return getIntersectionStream(subset)
- .min(LinecastPoint3D.ABSCISSA_ORDER)
- .orElse(null);
+ try (Stream<LinecastPoint3D> stream = getIntersectionStream(subset)) {
+ return stream.min(LinecastPoint3D.ABSCISSA_ORDER)
+ .orElse(null);
+ }
}
/** Return a stream containing intersections between the boundary source and the
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
new file mode 100644
index 0000000..d6cae6e
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Bounds3D.java
@@ -0,0 +1,290 @@
+/*
+ * 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.euclidean.threed;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.AbstractBounds;
+import org.apache.commons.geometry.euclidean.threed.shape.Parallelepiped;
+
+/** Class containing minimum and maximum points defining a 3D axis-aligned bounding box. Unless otherwise
+ * noted, floating point comparisons used in this class are strict, meaning that values are considered equal
+ * if and only if they match exactly.
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public final class Bounds3D extends AbstractBounds<Vector3D, Bounds3D> {
+
+ /** Simple constructor. Callers are responsible for ensuring the min is not greater than max.
+ * @param min minimum point
+ * @param max maximum point
+ */
+ private Bounds3D(final Vector3D min, final Vector3D max) {
+ super(min, max);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasSize(final DoublePrecisionContext precision) {
+ final Vector3D diag = getDiagonal();
+
+ return !precision.eqZero(diag.getX()) &&
+ !precision.eqZero(diag.getY()) &&
+ !precision.eqZero(diag.getZ());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean contains(final Vector3D pt) {
+ final double x = pt.getX();
+ final double y = pt.getY();
+ final double z = pt.getZ();
+
+ final Vector3D min = getMin();
+ final Vector3D max = getMax();
+
+ return x >= min.getX() && x <= max.getX() &&
+ y >= min.getY() && y <= max.getY() &&
+ z >= min.getZ() && z <= max.getZ();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean contains(final Vector3D pt, final DoublePrecisionContext precision) {
+ final double x = pt.getX();
+ final double y = pt.getY();
+ final double z = pt.getZ();
+
+ final Vector3D min = getMin();
+ final Vector3D max = getMax();
+
+ return precision.gte(x, min.getX()) && precision.lte(x, max.getX()) &&
+ precision.gte(y, min.getY()) && precision.lte(y, max.getY()) &&
+ precision.gte(z, min.getZ()) && precision.lte(z, max.getZ());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean intersects(final Bounds3D other) {
+ final Vector3D aMin = getMin();
+ final Vector3D aMax = getMax();
+
+ final Vector3D bMin = other.getMin();
+ final Vector3D bMax = other.getMax();
+
+ return aMin.getX() <= bMax.getX() && aMax.getX() >= bMin.getX() &&
+ aMin.getY() <= bMax.getY() && aMax.getY() >= bMin.getY() &&
+ aMin.getZ() <= bMax.getZ() && aMax.getZ() >= bMin.getZ();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Bounds3D intersection(final Bounds3D other) {
+ if (intersects(other)) {
+ final Vector3D aMin = getMin();
+ final Vector3D aMax = getMax();
+
+ final Vector3D bMin = other.getMin();
+ final Vector3D bMax = other.getMax();
+
+ // get the max of the mins and the mins of the maxes
+ final double minX = Math.max(aMin.getX(), bMin.getX());
+ final double minY = Math.max(aMin.getY(), bMin.getY());
+ final double minZ = Math.max(aMin.getZ(), bMin.getZ());
+
+ final double maxX = Math.min(aMax.getX(), bMax.getX());
+ final double maxY = Math.min(aMax.getY(), bMax.getY());
+ final double maxZ = Math.min(aMax.getZ(), bMax.getZ());
+
+ return new Bounds3D(
+ Vector3D.of(minX, minY, minZ),
+ Vector3D.of(maxX, maxY, maxZ));
+ }
+
+ return null; // no intersection
+ }
+
+ /** {@inheritDoc}
+ *
+ * @throws IllegalArgumentException if any dimension of the bounding box is zero
+ * as evaluated by the given precision context
+ */
+ @Override
+ public Parallelepiped toRegion(final DoublePrecisionContext precision) {
+ return Parallelepiped.axisAligned(getMin(), getMax(), precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(getMin(), getMax());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == this) {
+ return true;
+ } else if (!(obj instanceof Bounds3D)) {
+ return false;
+ }
+
+ final Bounds3D other = (Bounds3D) obj;
+
+ return getMin().equals(other.getMin()) &&
+ getMax().equals(other.getMax());
+ }
+
+ /** Construct a new instance from the given points.
+ * @param first first point
+ * @param more additional points
+ * @return a new instance containing the min and max coordinates values from the input points
+ */
+ public static Bounds3D from(final Vector3D first, final Vector3D... more) {
+ final Builder builder = builder();
+
+ builder.add(first);
+ builder.addAll(Arrays.asList(more));
+
+ return builder.build();
+ }
+
+ /** Construct a new instance from the given points.
+ * @param points input points
+ * @return a new instance containing the min and max coordinates values from the input points
+ */
+ public static Bounds3D from(final Iterable<Vector3D> points) {
+ final Builder builder = builder();
+
+ builder.addAll(points);
+
+ return builder.build();
+ }
+
+ /** Construct a new {@link Builder} instance for creating bounds.
+ * @return a new builder instance for creating bounds
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Class used to construct {@link Bounds3D} instances.
+ */
+ public static final class Builder {
+
+ /** Minimum x coordinate. */
+ private double minX = Double.POSITIVE_INFINITY;
+
+ /** Minimum y coordinate. */
+ private double minY = Double.POSITIVE_INFINITY;
+
+ /** Minimum z coordinate. */
+ private double minZ = Double.POSITIVE_INFINITY;
+
+ /** Maximum x coordinate. */
+ private double maxX = Double.NEGATIVE_INFINITY;
+
+ /** Maximum y coordinate. */
+ private double maxY = Double.NEGATIVE_INFINITY;
+
+ /** Maximum z coordinate. */
+ private double maxZ = Double.NEGATIVE_INFINITY;
+
+ /** Private constructor; instantiate through factory method. */
+ private Builder() { }
+
+ /** Add a point to this instance.
+ * @param pt point to add
+ * @return this instance
+ */
+ public Builder add(final Vector3D pt) {
+ final double x = pt.getX();
+ final double y = pt.getY();
+ final double z = pt.getZ();
+
+ minX = Math.min(x, minX);
+ minY = Math.min(y, minY);
+ minZ = Math.min(z, minZ);
+
+ maxX = Math.max(x, maxX);
+ maxY = Math.max(y, maxY);
+ maxZ = Math.max(z, maxZ);
+
+ return this;
+ }
+
+ /** Add a collection of points to this instance.
+ * @param pts points to add
+ * @return this instance
+ */
+ public Builder addAll(final Iterable<Vector3D> pts) {
+ for (final Vector3D pt : pts) {
+ add(pt);
+ }
+
+ return this;
+ }
+
+ /** Add the min and max points from the given bounds to this instance.
+ * @param bounds bounds containing the min and max points to add
+ * @return this instance
+ */
+ public Builder add(final Bounds3D bounds) {
+ add(bounds.getMin());
+ add(bounds.getMax());
+
+ return this;
+ }
+
+ /** Return true if this builder contains valid min and max coordinate values.
+ * @return true if this builder contains valid min and max coordinate values
+ */
+ public boolean containsBounds() {
+ return Double.isFinite(minX) &&
+ Double.isFinite(minY) &&
+ Double.isFinite(minZ) &&
+ Double.isFinite(maxX) &&
+ Double.isFinite(maxY) &&
+ Double.isFinite(maxZ);
+ }
+
+ /** Create a new {@link Bounds3D} instance from the values in this builder.
+ * The builder can continue to be used to create other instances.
+ * @return a new bounds instance
+ * @throws IllegalStateException if no points were given to the builder or any of the computed
+ * min and max coordinate values are NaN or infinite
+ * @see #containsBounds()
+ */
+ public Bounds3D build() {
+ final Vector3D min = Vector3D.of(minX, minY, minZ);
+ final Vector3D max = Vector3D.of(maxX, maxY, maxZ);
+
+ if (!containsBounds()) {
+ if (Double.isInfinite(minX) && minX > 0 &&
+ Double.isInfinite(maxX) && maxX < 0) {
+ throw new IllegalStateException("Cannot construct bounds: no points given");
+ }
+
+ throw new IllegalStateException("Invalid bounds: min= " + min + ", max= " + max);
+ }
+
+ return new Bounds3D(min, max);
+ }
+ }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionEmbedding.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexPolygon3D.java
similarity index 53%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionEmbedding.java
copy to commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexPolygon3D.java
index b82b75a..3c7b365 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionEmbedding.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexPolygon3D.java
@@ -14,28 +14,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.commons.geometry.core;
+package org.apache.commons.geometry.euclidean.threed;
-/** Interface representing a geometric element that embeds a region in a subspace.
- * @param <P> Point type defining the embedding space.
- * @param <S> Point type defining the embedded subspace.
- * @see Embedding
- * @see Region
+import org.apache.commons.geometry.core.Transform;
+
+/** Interface representing a closed, finite convex polygon in Euclidean 3D space.
*/
-public interface RegionEmbedding<P extends Point<P>, S extends Point<S>>
- extends Embedding<P, S>, Sized {
+public interface ConvexPolygon3D extends PlaneConvexSubset {
- /** Get the size of the instance, which by default is the size of the embedded
- * subspace region.
- * @return the size of instance
- */
+ /** {@inheritDoc} */
@Override
- default double getSize() {
- return getSubspaceRegion().getSize();
- }
+ ConvexPolygon3D reverse();
- /** Get the embedded subspace region.
- * @return the embedded subspace region
- */
- Region<S> getSubspaceRegion();
+ /** {@inheritDoc} */
+ @Override
+ ConvexPolygon3D transform(Transform<Vector3D> transform);
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexVolume.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexVolume.java
index 57949c3..fb9e940 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexVolume.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexVolume.java
@@ -26,7 +26,6 @@ import org.apache.commons.geometry.core.partitioning.AbstractConvexHyperplaneBou
import org.apache.commons.geometry.core.partitioning.Hyperplane;
import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
import org.apache.commons.geometry.core.partitioning.Split;
-import org.apache.commons.geometry.euclidean.twod.ConvexArea;
/** Class representing a finite or infinite convex volume in Euclidean 3D space.
* The boundaries of this area, if any, are composed of plane convex subsets.
@@ -65,14 +64,11 @@ public class ConvexVolume extends AbstractConvexHyperplaneBoundedRegion<Vector3D
return Double.POSITIVE_INFINITY;
}
- final Plane plane = boundary.getPlane();
- final ConvexArea subarea = boundary.getSubspaceRegion();
+ final Plane boundaryPlane = boundary.getPlane();
+ final double boundaryArea = boundary.getSize();
+ final Vector3D boundaryBarycenter = boundary.getBarycenter();
- final Vector3D facetBarycenter = boundary.getHyperplane().toSpace(
- subarea.getBarycenter());
-
-
- volumeSum += subarea.getSize() * facetBarycenter.dot(plane.getNormal());
+ volumeSum += boundaryArea * boundaryBarycenter.dot(boundaryPlane.getNormal());
}
return volumeSum / 3.0;
@@ -92,19 +88,17 @@ public class ConvexVolume extends AbstractConvexHyperplaneBoundedRegion<Vector3D
return null;
}
- final Plane plane = boundary.getPlane();
- final ConvexArea subarea = boundary.getSubspaceRegion();
-
- final Vector3D facetBarycenter = boundary.getHyperplane().toSpace(
- subarea.getBarycenter());
+ final Plane boundaryPlane = boundary.getPlane();
+ final double boundaryArea = boundary.getSize();
+ final Vector3D boundaryBarycenter = boundary.getBarycenter();
- final double scaledVolume = subarea.getSize() * facetBarycenter.dot(plane.getNormal());
+ final double scaledVolume = boundaryArea * boundaryBarycenter.dot(boundaryPlane.getNormal());
volumeSum += scaledVolume;
- sumX += scaledVolume * facetBarycenter.getX();
- sumY += scaledVolume * facetBarycenter.getY();
- sumZ += scaledVolume * facetBarycenter.getZ();
+ sumX += scaledVolume * boundaryBarycenter.getX();
+ sumY += scaledVolume * boundaryBarycenter.getY();
+ sumZ += scaledVolume * boundaryBarycenter.getZ();
}
if (volumeSum > 0) {
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddedAreaPlaneConvexSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddedAreaPlaneConvexSubset.java
new file mode 100644
index 0000000..2a11ce2
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddedAreaPlaneConvexSubset.java
@@ -0,0 +1,117 @@
+/*
+ * 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.euclidean.threed;
+
+import java.util.List;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
+import org.apache.commons.geometry.euclidean.twod.ConvexArea;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+
+/** Internal implementation of {@link PlaneConvexSubset} that uses an embedded
+ * {@link ConvexArea} to represent the subspace region. This class is capable of
+ * representing regions of infinite size.
+ */
+final class EmbeddedAreaPlaneConvexSubset extends AbstractEmbeddedRegionPlaneSubset
+ implements PlaneConvexSubset, PlaneConvexSubset.Embedded {
+
+ /** The embedded 2D area. */
+ private final ConvexArea area;
+
+ /** Create a new instance from its component parts.
+ * @param plane plane the the convex area is embedded in
+ * @param area the embedded convex area
+ */
+ EmbeddedAreaPlaneConvexSubset(final EmbeddingPlane plane, final ConvexArea area) {
+ super(plane);
+
+ this.area = area;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public PlaneConvexSubset.Embedded getEmbedded() {
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ConvexArea getSubspaceRegion() {
+ return area;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<Vector3D> getVertices() {
+ return getPlane().toSpace(area.getVertices());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Bounds3D getBounds() {
+ return getBoundsFromSubspace(area);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<Triangle3D> toTriangles() {
+ if (isInfinite()) {
+ throw new IllegalStateException("Cannot convert infinite plane subset to triangles: " + this);
+ }
+
+ final EmbeddingPlane plane = getPlane();
+ final List<Vector3D> vertices = plane.toSpace(area.getVertices());
+
+ return Planes.convexPolygonToTriangleFan(plane, vertices);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public EmbeddedAreaPlaneConvexSubset transform(final Transform<Vector3D> transform) {
+ final EmbeddingPlane.SubspaceTransform st = getPlane().subspaceTransform(transform);
+ final ConvexArea tArea = area.transform(st.getTransform());
+
+ return new EmbeddedAreaPlaneConvexSubset(st.getPlane().getEmbedding(), tArea);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public EmbeddedAreaPlaneConvexSubset reverse() {
+ final EmbeddingPlane plane = getPlane();
+ final EmbeddingPlane rPlane = plane.reverse();
+
+ final Vector2D rU = rPlane.toSubspace(plane.toSpace(Vector2D.Unit.PLUS_X));
+ final Vector2D rV = rPlane.toSubspace(plane.toSpace(Vector2D.Unit.PLUS_Y));
+
+ final AffineTransformMatrix2D transform =
+ AffineTransformMatrix2D.fromColumnVectors(rU, rV);
+
+ return new EmbeddedAreaPlaneConvexSubset(rPlane, area.transform(transform));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Split<PlaneConvexSubset> split(final Hyperplane<Vector3D> splitter) {
+ // delegate back to the Planes factory method so that it has a chance to decide
+ // on the best possible implementation for the given area
+ return Planes.subspaceSplit((Plane) splitter, this,
+ (p, r) -> Planes.subsetFromConvexArea(p, (ConvexArea) r));
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubset.java
index 7b15ef6..f1d27e1 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubset.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubset.java
@@ -22,41 +22,45 @@ import java.util.List;
import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.euclidean.twod.ConvexArea;
import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+import org.apache.commons.geometry.euclidean.twod.rotation.Rotation2D;
/** Class representing an arbitrary subset of a plane using a {@link RegionBSPTree2D}.
* This class can represent convex, non-convex, finite, infinite, and empty regions.
*
* <p>This class is mutable and <em>not</em> thread safe.</p>
*/
-public final class EmbeddedTreePlaneSubset extends PlaneSubset {
+public final class EmbeddedTreePlaneSubset extends AbstractEmbeddedRegionPlaneSubset {
+
/** The 2D region representing the area on the plane. */
private final RegionBSPTree2D region;
/** Construct a new, empty plane subset for the given plane.
- * @param plane plane defining the subset
+ * @param plane plane containing the subset
*/
- public EmbeddedTreePlaneSubset(final Plane plane) {
+ public EmbeddedTreePlaneSubset(final EmbeddingPlane plane) {
this(plane, false);
}
/** Construct a new subset for the given plane. If {@code full}
* is true, then the subset will cover the entire plane; otherwise,
* it will be empty.
- * @param plane plane defining the subset
+ * @param plane plane containing the subset
* @param full if true, the subset will cover the entire space;
* otherwise it will be empty
*/
- public EmbeddedTreePlaneSubset(final Plane plane, boolean full) {
+ public EmbeddedTreePlaneSubset(final EmbeddingPlane plane, boolean full) {
this(plane, new RegionBSPTree2D(full));
}
/** Construct a new instance from its defining plane and subspace region.
- * @param plane plane defining the subset
+ * @param plane plane containing the subset
* @param region subspace region for the plane subset
*/
- public EmbeddedTreePlaneSubset(final Plane plane, final RegionBSPTree2D region) {
+ public EmbeddedTreePlaneSubset(final EmbeddingPlane plane, final RegionBSPTree2D region) {
super(plane);
this.region = region;
@@ -64,19 +68,56 @@ public final class EmbeddedTreePlaneSubset extends PlaneSubset {
/** {@inheritDoc} */
@Override
+ public PlaneSubset.Embedded getEmbedded() {
+ return this;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public RegionBSPTree2D getSubspaceRegion() {
+ return region;
+ }
+
+ /** {@inheritDoc} */
+ @Override
public List<PlaneConvexSubset> toConvex() {
final List<ConvexArea> areas = region.toConvex();
- final Plane plane = getPlane();
final List<PlaneConvexSubset> facets = new ArrayList<>(areas.size());
for (final ConvexArea area : areas) {
- facets.add(Planes.subsetFromConvexArea(plane, area));
+ facets.add(Planes.subsetFromConvexArea(getPlane(), area));
}
return facets;
}
+ /** {@inheritDoc} */
+ @Override
+ public List<Triangle3D> toTriangles() {
+ final EmbeddingPlane plane = getPlane();
+ final List<Triangle3D> triangles = new ArrayList<>();
+
+ List<Vector3D> vertices;
+ for (final ConvexArea area : region.toConvex()) {
+ if (area.isInfinite()) {
+ throw new IllegalStateException("Cannot convert infinite plane subset to triangles: " + this);
+ }
+
+ vertices = plane.toSpace(area.getVertices());
+
+ triangles.addAll(Planes.convexPolygonToTriangleFan(plane, vertices));
+ }
+
+ return triangles;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Bounds3D getBounds() {
+ return getBoundsFromSubspace(region);
+ }
+
/** {@inheritDoc}
*
* <p>In all cases, the current instance is not modified. However, In order to avoid
@@ -89,19 +130,15 @@ public final class EmbeddedTreePlaneSubset extends PlaneSubset {
*/
@Override
public Split<EmbeddedTreePlaneSubset> split(final Hyperplane<Vector3D> splitter) {
- return splitInternal(splitter, this, (p, r) -> new EmbeddedTreePlaneSubset(p, (RegionBSPTree2D) r));
- }
-
- /** {@inheritDoc} */
- @Override
- public RegionBSPTree2D getSubspaceRegion() {
- return region;
+ return Planes.subspaceSplit((Plane) splitter, this,
+ (p, r) -> new EmbeddedTreePlaneSubset(p, (RegionBSPTree2D) r));
}
/** {@inheritDoc} */
@Override
public EmbeddedTreePlaneSubset transform(final Transform<Vector3D> transform) {
- final Plane.SubspaceTransform subTransform = getPlane().subspaceTransform(transform);
+ final EmbeddingPlane.SubspaceTransform subTransform =
+ getPlane().getEmbedding().subspaceTransform(transform);
final RegionBSPTree2D tRegion = RegionBSPTree2D.empty();
tRegion.copy(region);
@@ -118,7 +155,16 @@ public final class EmbeddedTreePlaneSubset extends PlaneSubset {
public void add(final PlaneConvexSubset subset) {
Planes.validatePlanesEquivalent(getPlane(), subset.getPlane());
- region.add(subset.getSubspaceRegion());
+ final PlaneConvexSubset.Embedded embedded = subset.getEmbedded();
+ final Rotation2D rot = getEmbeddedRegionRotation(embedded);
+
+ final ConvexArea subspaceArea = embedded.getSubspaceRegion();
+
+ final ConvexArea toAdd = rot != null ?
+ subspaceArea.transform(rot) :
+ subspaceArea;
+
+ region.add(toAdd);
}
/** Add a plane subset to this instance.
@@ -129,6 +175,44 @@ public final class EmbeddedTreePlaneSubset extends PlaneSubset {
public void add(final EmbeddedTreePlaneSubset subset) {
Planes.validatePlanesEquivalent(getPlane(), subset.getPlane());
- region.union(subset.getSubspaceRegion());
+ final RegionBSPTree2D otherTree = subset.getSubspaceRegion();
+ final Rotation2D rot = getEmbeddedRegionRotation(subset);
+
+ RegionBSPTree2D regionToAdd;
+ if (rot != null) {
+ // we need to transform the subspace region before adding
+ regionToAdd = otherTree.copy();
+ regionToAdd.transform(rot);
+ } else {
+ regionToAdd = otherTree;
+ }
+
+ region.union(regionToAdd);
+ }
+
+ /** Construct a rotation transform used to transform the subspace of the given embedded region plane
+ * subset into the subspace of this instance. Returns null if no transform is needed. This method must only
+ * be called with embedded regions that share an equivalent plane with this instance, meaning that the
+ * planes have the same origin point and normal
+ * @param embedded the embedded region plane subset to compare with the current instance
+ * @return a rotation transform to convert from the subspace of the argument into the current subspace; returns
+ * null if no such transform is needed
+ */
+ private Rotation2D getEmbeddedRegionRotation(final PlaneSubset.Embedded embedded) {
+ // check if we need to apply a rotation to the given embedded subspace
+ final EmbeddingPlane thisPlane = getPlane();
+ final EmbeddingPlane otherPlane = embedded.getPlane();
+
+ final DoublePrecisionContext precision = thisPlane.getPrecision();
+
+ final double uDot = thisPlane.getU().dot(otherPlane.getU());
+ if (!precision.eq(uDot, 1.0)) {
+ final Vector2D otherPlaneU = thisPlane.toSubspace(otherPlane.getOrigin().add(otherPlane.getU()));
+ final double angle = Math.atan2(otherPlaneU.getY(), otherPlaneU.getX());
+
+ return Rotation2D.of(angle);
+ }
+
+ return null;
}
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddingPlane.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddingPlane.java
new file mode 100644
index 0000000..055c050
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddingPlane.java
@@ -0,0 +1,333 @@
+/*
+ * 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.euclidean.threed;
+
+import java.util.Objects;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
+import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+
+/** Extension of the {@link Plane} class that supports embedding of 2D subspaces in the plane.
+ * This is accomplished by defining two additional vectors, {@link #getU() u} and {@link #getV() v},
+ * that define the {@code x} and {@code y} axes respectively of the embedded subspace. For completeness,
+ * an additional vector {@link #getW()} is defined, which is simply an alias for the plane normal.
+ * Together, the vectors {@code u}, {@code v}, and {@code w} form a right-handed orthonormal basis.
+ *
+ * <p>The additional {@code u} and {@code v} vectors are not required to fulfill the contract of
+ * {@link org.apache.commons.geometry.core.partitioning.Hyperplane Hyperplane}. Therefore, they
+ * are not considered when using instances of this type purely as a hyperplane. For example, the
+ * {@link Plane#eq(Plane, DoublePrecisionContext) eq} and
+ * {@link Plane#similarOrientation(org.apache.commons.geometry.core.partitioning.Hyperplane) similiarOrientation}
+ * methods do not consider them.</p>
+ */
+public final class EmbeddingPlane extends Plane implements EmbeddingHyperplane<Vector3D, Vector2D> {
+ /** First normalized vector of the plane frame (in plane). */
+ private final Vector3D.Unit u;
+
+ /** Second normalized vector of the plane frame (in plane). */
+ private final Vector3D.Unit v;
+
+ /** Construct a new instance from an orthonormal set of basis vectors and an origin offset.
+ * @param u first vector of the basis (in plane)
+ * @param v second vector of the basis (in plane)
+ * @param w third vector of the basis (plane normal)
+ * @param originOffset offset of the origin with respect to the plane.
+ * @param precision precision context used for floating point comparisons
+ */
+ EmbeddingPlane(final Vector3D.Unit u, final Vector3D.Unit v, final Vector3D.Unit w, double originOffset,
+ final DoublePrecisionContext precision) {
+ super(w, originOffset, precision);
+
+ this.u = u;
+ this.v = v;
+ }
+
+ /** Get the plane first canonical vector.
+ * <p>
+ * The frame defined by ({@link #getU u}, {@link #getV v},
+ * {@link #getW w}) is a right-handed orthonormalized frame).
+ * </p>
+ * @return normalized first canonical vector
+ * @see #getV
+ * @see #getW
+ * @see #getNormal
+ */
+ public Vector3D.Unit getU() {
+ return u;
+ }
+
+ /** Get the plane second canonical vector.
+ * <p>
+ * The frame defined by ({@link #getU u}, {@link #getV v},
+ * {@link #getW w}) is a right-handed orthonormalized frame).
+ * </p>
+ * @return normalized second canonical vector
+ * @see #getU
+ * @see #getW
+ * @see #getNormal
+ */
+ public Vector3D.Unit getV() {
+ return v;
+ }
+
+ /** Get the plane third canonical vector, ie, the plane normal. This
+ * method is simply an alias for {@link #getNormal()}.
+ * <p>
+ * The frame defined by {@link #getU() u}, {@link #getV() v},
+ * {@link #getW() w} is a right-handed orthonormalized frame.
+ * </p>
+ * @return normalized normal vector
+ * @see #getU()
+ * @see #getV()
+ * @see #getNormal()
+ */
+ public Vector3D.Unit getW() {
+ return getNormal();
+ }
+
+ /** Return the current instance.
+ */
+ @Override
+ public EmbeddingPlane getEmbedding() {
+ return this;
+ }
+
+ /** Transform a 3D space point into an in-plane point.
+ * @param point point of the space
+ * @return in-plane point
+ * @see #toSpace
+ */
+ @Override
+ public Vector2D toSubspace(final Vector3D point) {
+ return Vector2D.of(point.dot(u), point.dot(v));
+ }
+
+ /** Transform an in-plane point into a 3D space point.
+ * @param point in-plane point
+ * @return 3D space point
+ * @see #toSubspace(Vector3D)
+ */
+ @Override
+ public Vector3D toSpace(final Vector2D point) {
+ return Vector3D.linearCombination(
+ point.getX(), u,
+ point.getY(), v,
+ -getOriginOffset(), getNormal());
+ }
+
+ /** Get one point from the 3D-space.
+ * @param inPlane desired in-plane coordinates for the point in the plane
+ * @param offset desired offset for the point
+ * @return one point in the 3D-space, with given coordinates and offset relative
+ * to the plane
+ */
+ public Vector3D pointAt(final Vector2D inPlane, final double offset) {
+ return Vector3D.linearCombination(
+ inPlane.getX(), u,
+ inPlane.getY(), v,
+ offset - getOriginOffset(), getNormal());
+ }
+
+ /** Build a new reversed version of this plane, with opposite orientation.
+ * <p>
+ * The new plane frame is chosen in such a way that a 3D point that had
+ * {@code (x, y)} in-plane coordinates and {@code z} offset with respect to the
+ * plane and is unaffected by the change will have {@code (y, x)} in-plane
+ * coordinates and {@code -z} offset with respect to the new plane. This means
+ * that the {@code u} and {@code v} vectors returned by the {@link #getU} and
+ * {@link #getV} methods are exchanged, and the {@code w} vector returned by the
+ * {@link #getNormal} method is reversed.
+ * </p>
+ * @return a new reversed plane
+ */
+ @Override
+ public EmbeddingPlane reverse() {
+ return new EmbeddingPlane(v, u, getNormal().negate(), -getOriginOffset(), getPrecision());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public EmbeddingPlane transform(final Transform<Vector3D> transform) {
+ final Vector3D origin = getOrigin();
+ final Vector3D plusU = origin.add(u);
+ final Vector3D plusV = origin.add(v);
+
+ final Vector3D tOrigin = transform.apply(origin);
+ final Vector3D tPlusU = transform.apply(plusU);
+ final Vector3D tPlusV = transform.apply(plusV);
+
+ final Vector3D.Unit tU = tOrigin.directionTo(tPlusU);
+ final Vector3D.Unit tV = tOrigin.directionTo(tPlusV);
+ final Vector3D.Unit tW = tU.cross(tV).normalize();
+
+ final double tOriginOffset = -tOrigin.dot(tW);
+
+ return new EmbeddingPlane(tU, tV, tW, tOriginOffset, getPrecision());
+ }
+
+ /** Translate the plane by the specified amount.
+ * @param translation translation to apply
+ * @return a new plane
+ */
+ @Override
+ public EmbeddingPlane translate(final Vector3D translation) {
+ final Vector3D tOrigin = getOrigin().add(translation);
+
+ return Planes.fromPointAndPlaneVectors(tOrigin, u, v, getPrecision());
+ }
+
+ /** Rotate the plane around the specified point.
+ * @param center rotation center
+ * @param rotation 3-dimensional rotation
+ * @return a new rotated plane
+ */
+ @Override
+ public EmbeddingPlane rotate(final Vector3D center, final QuaternionRotation rotation) {
+ final Vector3D delta = getOrigin().subtract(center);
+ final Vector3D tOrigin = center.add(rotation.apply(delta));
+ final Vector3D.Unit tU = rotation.apply(u).normalize();
+ final Vector3D.Unit tV = rotation.apply(v).normalize();
+
+ return Planes.fromPointAndPlaneVectors(tOrigin, tU, tV, getPrecision());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(getNormal(), getOriginOffset(), u, v, getPrecision());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (obj == null || obj.getClass() != EmbeddingPlane.class) {
+ return false;
+ }
+
+ final EmbeddingPlane other = (EmbeddingPlane) obj;
+
+ return Objects.equals(this.getNormal(), other.getNormal()) &&
+ Double.compare(this.getOriginOffset(), other.getOriginOffset()) == 0 &&
+ Objects.equals(this.u, other.u) &&
+ Objects.equals(this.v, other.v) &&
+ Objects.equals(this.getPrecision(), other.getPrecision());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(getClass().getSimpleName())
+ .append("[origin= ")
+ .append(getOrigin())
+ .append(", u= ")
+ .append(u)
+ .append(", v= ")
+ .append(v)
+ .append(", w= ")
+ .append(getNormal())
+ .append(']');
+
+ return sb.toString();
+ }
+
+ /** Get an object containing the current plane transformed by the argument along with a
+ * 2D transform that can be applied to subspace points. The subspace transform transforms
+ * subspace points such that their 3D location in the transformed plane is the same as their
+ * 3D location in the original plane after the 3D transform is applied. For example, consider
+ * the code below:
+ * <pre>
+ * SubspaceTransform st = plane.subspaceTransform(transform);
+ *
+ * Vector2D subPt = Vector2D.of(1, 1);
+ *
+ * Vector3D a = transform.apply(plane.toSpace(subPt)); // transform in 3D space
+ * Vector3D b = st.getPlane().toSpace(st.getTransform().apply(subPt)); // transform in 2D space
+ * </pre>
+ * At the end of execution, the points {@code a} (which was transformed using the original
+ * 3D transform) and {@code b} (which was transformed in 2D using the subspace transform)
+ * are equivalent.
+ *
+ * @param transform the transform to apply to this instance
+ * @return an object containing the transformed plane along with a transform that can be applied
+ * to subspace points
+ * @see #transform(Transform)
+ */
+ public SubspaceTransform subspaceTransform(final Transform<Vector3D> transform) {
+ final Vector3D origin = getOrigin();
+
+ final Vector3D tOrigin = transform.apply(origin);
+ final Vector3D tPlusU = transform.apply(origin.add(u));
+ final Vector3D tPlusV = transform.apply(origin.add(v));
+
+ final EmbeddingPlane tPlane = Planes.fromPointAndPlaneVectors(
+ tOrigin,
+ tOrigin.vectorTo(tPlusU),
+ tOrigin.vectorTo(tPlusV),
+ getPrecision());
+
+ final Vector2D tSubspaceOrigin = tPlane.toSubspace(tOrigin);
+ final Vector2D tSubspaceU = tSubspaceOrigin.vectorTo(tPlane.toSubspace(tPlusU));
+ final Vector2D tSubspaceV = tSubspaceOrigin.vectorTo(tPlane.toSubspace(tPlusV));
+
+ final AffineTransformMatrix2D subspaceTransform =
+ AffineTransformMatrix2D.fromColumnVectors(tSubspaceU, tSubspaceV, tSubspaceOrigin);
+
+ return new SubspaceTransform(tPlane, subspaceTransform);
+ }
+
+ /** Class containing a transformed plane instance along with a subspace (2D) transform. The subspace
+ * transform produces the equivalent of the 3D transform in 2D.
+ */
+ public static final class SubspaceTransform {
+ /** The transformed plane. */
+ private final EmbeddingPlane plane;
+
+ /** The subspace transform instance. */
+ private final AffineTransformMatrix2D transform;
+
+ /** Simple constructor.
+ * @param plane the transformed plane
+ * @param transform 2D transform that can be applied to subspace points
+ */
+ public SubspaceTransform(final EmbeddingPlane plane, final AffineTransformMatrix2D transform) {
+ this.plane = plane;
+ this.transform = transform;
+ }
+
+ /** Get the transformed plane instance.
+ * @return the transformed plane instance
+ */
+ public EmbeddingPlane getPlane() {
+ return plane;
+ }
+
+ /** Get the 2D transform that can be applied to subspace points. This transform can be used
+ * to perform the equivalent of the 3D transform in 2D space.
+ * @return the subspace transform instance
+ */
+ public AffineTransformMatrix2D getTransform() {
+ return transform;
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java
index 4601680..ae08eb6 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java
@@ -20,344 +20,219 @@ import java.util.Objects;
import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
-import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.oned.Vector1D;
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.rotation.QuaternionRotation;
-import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
import org.apache.commons.geometry.euclidean.twod.ConvexArea;
-import org.apache.commons.geometry.euclidean.twod.Vector2D;
-/** Class representing a plane in 3 dimensional Euclidean space.
+/** Class representing a plane in 3 dimensional Euclidean space. Each plane is defined by a
+ * {@link #getNormal() normal} and an {@link #getOriginOffset() origin offset}. If \(\vec{n}\) is the plane normal,
+ * \(d\) is the origin offset, and \(p\) and \(q\) are any points in the plane, then the following are true:
+ * <ul>
+ * <li>\(\lVert \vec{n} \rVert\) = 1</li>
+ * <li>\(\vec{n} \cdot (p - q) = 0\)</li>
+ * <li>\(d = - (\vec{n} \cdot q)\)</li>
+ * </ul>
+ * In other words, the normal is a unit vector such that the dot product of the normal and the difference of
+ * any two points in the plane is always equal to \(0\). Similarly, the {@code origin offset} is equal to the
+ * negation of the dot product of the normal and any point in the plane. The projection of the origin onto the
+ * plane (given by {@link #getOrigin()}), is computed as \(-d \vec{n}\).
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
* @see Planes
*/
-public final class Plane extends AbstractHyperplane<Vector3D>
- implements EmbeddingHyperplane<Vector3D, Vector2D> {
- /** First normalized vector of the plane frame (in plane). */
- private final Vector3D u;
-
- /** Second normalized vector of the plane frame (in plane). */
- private final Vector3D v;
+public class Plane extends AbstractHyperplane<Vector3D> implements Hyperplane<Vector3D> {
- /** Normalized plane normal. */
- private final Vector3D w;
+ /** Plane normal. */
+ private final Vector3D.Unit normal;
/** Offset of the origin with respect to the plane. */
private final double originOffset;
- /**
- * Constructor to build a new plane with the given values.
- * Made private to prevent inheritance.
- * @param u u vector (on plane)
- * @param v v vector (on plane)
- * @param w unit normal vector
- * @param originOffset offset of the origin with respect to the plane.
+ /** Construct a plane from its component parts.
+ * @param normal unit normal vector
+ * @param originOffset offset of the origin with respect to the plane
* @param precision precision context used to compare floating point values
*/
- Plane(final Vector3D u, final Vector3D v, final Vector3D w, double originOffset,
+ Plane(final Vector3D.Unit normal, double originOffset,
final DoublePrecisionContext precision) {
super(precision);
- this.u = u;
- this.v = v;
- this.w = w;
-
+ this.normal = normal;
this.originOffset = originOffset;
}
- /**
- * Get the orthogonal projection of the 3D-space origin in the plane.
+ /** Get the orthogonal projection of the 3D-space origin in the plane.
* @return the origin point of the plane frame (point closest to the 3D-space
* origin)
*/
public Vector3D getOrigin() {
- return w.multiply(-originOffset);
+ return normal.multiply(-originOffset);
}
- /**
- * Get the offset of the spatial origin ({@code 0, 0, 0}) with respect to the plane.
- *
- * @return the offset of the origin with respect to the plane.
+ /** Get the offset of the spatial origin ({@code 0, 0, 0}) with respect to the plane.
+ * @return the offset of the origin with respect to the plane.
*/
public double getOriginOffset() {
return originOffset;
}
- /**
- * Get the plane first canonical vector.
- * <p>
- * The frame defined by ({@link #getU getU}, {@link #getV getV},
- * {@link #getNormal getNormal}) is a right-handed orthonormalized frame).
- * </p>
- *
- * @return normalized first canonical vector
- * @see #getV
- * @see #getNormal
+ /** Get the plane normal vector.
+ * @return plane normal vector
*/
- public Vector3D getU() {
- return u;
+ public Vector3D.Unit getNormal() {
+ return normal;
}
- /**
- * Get the plane second canonical vector.
- * <p>
- * The frame defined by ({@link #getU getU}, {@link #getV getV},
- * {@link #getNormal getNormal}) is a right-handed orthonormalized frame).
- * </p>
- *
- * @return normalized second canonical vector
- * @see #getU
- * @see #getNormal
+ /** Return an {@link EmbeddingPlane} instance suitable for embedding 2D geometric objects
+ * into this plane. Returned instances are guaranteed to be equal between invocations.
+ * @return a plane instance suitable for embedding 2D subspaces
*/
- public Vector3D getV() {
- return v;
- }
+ public EmbeddingPlane getEmbedding() {
+ final Vector3D.Unit u = normal.orthogonal();
+ final Vector3D.Unit v = normal.cross(u).normalize();
- /**
- * Get the normalized normal vector.
- * <p>
- * The frame defined by {@link #getU()}, {@link #getV()},
- * {@link #getW()} is a right-handed orthonormalized frame.
- * </p>
- *
- * @return normalized normal vector
- * @see #getU()
- * @see #getV()
- * @see #getNormal()
- */
- public Vector3D getW() {
- return w;
- }
-
- /**
- * Get the normalized normal vector. This method is an alias
- * for {@link #getW()}.
- * <p>
- * The frame defined by {@link #getU()}, {@link #getV()},
- * {@link #getW()} is a right-handed orthonormalized frame.
- * </p>
- *
- * @return normalized normal vector
- * @see #getU()
- * @see #getV()
- * @see #getW()
- */
- public Vector3D getNormal() {
- return getW();
+ return new EmbeddingPlane(u, v, normal, originOffset, getPrecision());
}
/** {@inheritDoc} */
@Override
- public Vector3D project(final Vector3D point) {
- return toSpace(toSubspace(point));
+ public double offset(final Vector3D point) {
+ return point.dot(normal) + originOffset;
}
- /**
- * Project a 3D line onto the plane.
- * @param line the line to project
- * @return the projection of the given line onto the plane.
+ /** Get the offset (oriented distance) of the given line with respect to the plane. The value
+ * closest to zero is returned, which will always be zero if the line is not parallel to the plane.
+ * @param line line to calculate the offset of
+ * @return the offset of the line with respect to the plane or 0.0 if the line
+ * is not parallel to the plane.
*/
- public Line3D project(final Line3D line) {
- final Vector3D direction = line.getDirection();
- final Vector3D projection = w.multiply(direction.dot(w) * (1 / w.normSq()));
-
- final Vector3D projectedLineDirection = direction.subtract(projection);
- final Vector3D p1 = project(line.getOrigin());
- final Vector3D p2 = p1.add(projectedLineDirection);
+ public double offset(final Line3D line) {
+ if (!isParallel(line)) {
+ return 0.0;
+ }
+ return offset(line.getOrigin());
+ }
- return Lines3D.fromPoints(p1, p2, getPrecision());
+ /** Get the offset (oriented distance) of the given plane with respect to this instance. The value
+ * closest to zero is returned, which will always be zero if the planes are not parallel.
+ * @param plane plane to calculate the offset of
+ * @return the offset of the plane with respect to this instance or 0.0 if the planes
+ * are not parallel.
+ */
+ public double offset(final Plane plane) {
+ if (!isParallel(plane)) {
+ return 0.0;
+ }
+ return originOffset + (similarOrientation(plane) ? -plane.originOffset : plane.originOffset);
}
- /**
- * Build a new reversed version of this plane, with opposite orientation.
- * <p>
- * The new plane frame is chosen in such a way that a 3D point that had
- * {@code (x, y)} in-plane coordinates and {@code z} offset with respect to the
- * plane and is unaffected by the change will have {@code (y, x)} in-plane
- * coordinates and {@code -z} offset with respect to the new plane. This means
- * that the {@code u} and {@code v} vectors returned by the {@link #getU} and
- * {@link #getV} methods are exchanged, and the {@code w} vector returned by the
- * {@link #getNormal} method is reversed.
- * </p>
- * @return a new reversed plane
+ /** Check if the instance contains a point.
+ * @param p point to check
+ * @return true if p belongs to the plane
*/
@Override
- public Plane reverse() {
- return new Plane(v, u, w.negate(), -originOffset, getPrecision());
+ public boolean contains(final Vector3D p) {
+ return getPrecision().eqZero(offset(p));
}
- /**
- * Transform a 3D space point into an in-plane point.
- *
- * @param point point of the space (must be a {@link Vector3D} instance)
- * @return in-plane point
- * @see #toSpace
+ /** Check if the instance contains a line.
+ * @param line line to check
+ * @return true if line is contained in this plane
*/
- @Override
- public Vector2D toSubspace(final Vector3D point) {
- return Vector2D.of(point.dot(u), point.dot(v));
+ public boolean contains(final Line3D line) {
+ return isParallel(line) && contains(line.getOrigin());
}
- /**
- * Transform an in-plane point into a 3D space point.
- *
- * @param point in-plane point (must be a {@link Vector2D} instance)
- * @return 3D space point
- * @see #toSubspace(Vector3D)
+ /** Check if the instance contains another plane. Planes are considered similar if they contain
+ * the same points. This does not mean they are equal since they can have opposite normals.
+ * @param plane plane to which the instance is compared
+ * @return true if the planes are similar
*/
- @Override
- public Vector3D toSpace(final Vector2D point) {
- return Vector3D.linearCombination(point.getX(), u, point.getY(), v, -originOffset, w);
+ public boolean contains(final Plane plane) {
+ final double angle = normal.angle(plane.normal);
+ final DoublePrecisionContext precision = getPrecision();
+
+ return ((precision.eqZero(angle)) && precision.eq(originOffset, plane.originOffset)) ||
+ ((precision.eq(angle, Math.PI)) && precision.eq(originOffset, -plane.originOffset));
}
/** {@inheritDoc} */
@Override
- public Plane transform(final Transform<Vector3D> transform) {
- final Vector3D origin = getOrigin();
-
- final Vector3D p1 = transform.apply(origin);
- final Vector3D p2 = transform.apply(origin.add(u));
- final Vector3D p3 = transform.apply(origin.add(v));
-
- return Planes.fromPoints(p1, p2, p3, getPrecision());
+ public Vector3D project(final Vector3D point) {
+ return getOrigin().add(point.reject(normal));
}
- /** Get an object containing the current plane transformed by the argument along with a
- * 2D transform that can be applied to subspace points. The subspace transform transforms
- * subspace points such that their 3D location in the transformed plane is the same as their
- * 3D location in the original plane after the 3D transform is applied. For example, consider
- * the code below:
- * <pre>
- * SubspaceTransform st = plane.subspaceTransform(transform);
- *
- * Vector2D subPt = Vector2D.of(1, 1);
- *
- * Vector3D a = transform.apply(plane.toSpace(subPt)); // transform in 3D space
- * Vector3D b = st.getPlane().toSpace(st.getTransform().apply(subPt)); // transform in 2D space
- * </pre>
- * At the end of execution, the points {@code a} (which was transformed using the original
- * 3D transform) and {@code b} (which was transformed in 2D using the subspace transform)
- * are equivalent.
- *
- * @param transform the transform to apply to this instance
- * @return an object containing the transformed plane along with a transform that can be applied
- * to subspace points
- * @see #transform(Transform)
+ /** Project a 3D line onto the plane.
+ * @param line the line to project
+ * @return the projection of the given line onto the plane.
*/
- public SubspaceTransform subspaceTransform(final Transform<Vector3D> transform) {
- final Vector3D origin = getOrigin();
-
- final Vector3D p1 = transform.apply(origin);
- final Vector3D p2 = transform.apply(origin.add(u));
- final Vector3D p3 = transform.apply(origin.add(v));
-
- final Plane tPlane = Planes.fromPoints(p1, p2, p3, getPrecision());
-
- final Vector2D tSubspaceOrigin = tPlane.toSubspace(p1);
- final Vector2D tSubspaceU = tSubspaceOrigin.vectorTo(tPlane.toSubspace(p2));
- final Vector2D tSubspaceV = tSubspaceOrigin.vectorTo(tPlane.toSubspace(p3));
+ public Line3D project(final Line3D line) {
+ final Vector3D direction = line.getDirection();
+ final Vector3D projection = normal.multiply(direction.dot(normal) * (1 / normal.normSq()));
- final AffineTransformMatrix2D subspaceTransform =
- AffineTransformMatrix2D.fromColumnVectors(tSubspaceU, tSubspaceV, tSubspaceOrigin);
+ final Vector3D projectedLineDirection = direction.subtract(projection);
+ final Vector3D p1 = project(line.getOrigin());
+ final Vector3D p2 = p1.add(projectedLineDirection);
- return new SubspaceTransform(tPlane, subspaceTransform);
+ return Lines3D.fromPoints(p1, p2, getPrecision());
}
- /**
- * Rotate the plane around the specified point.
- * <p>
- * The instance is not modified, a new instance is created.
- * </p>
- *
- * @param center rotation center
- * @param rotation 3-dimensional rotation
- * @return a new plane
- */
- public Plane rotate(final Vector3D center, final QuaternionRotation rotation) {
- final Vector3D delta = getOrigin().subtract(center);
- final Vector3D p = center.add(rotation.apply(delta));
- final Vector3D normal = rotation.apply(this.w);
- final Vector3D wTmp = normal.normalize();
-
- final double originOffsetTmp = -p.dot(wTmp);
- final Vector3D uTmp = rotation.apply(this.u);
- final Vector3D vTmp = rotation.apply(this.v);
-
- return new Plane(uTmp, vTmp, wTmp, originOffsetTmp, getPrecision());
+ /** {@inheritDoc} */
+ @Override
+ public PlaneConvexSubset span() {
+ return Planes.subsetFromConvexArea(getEmbedding(), ConvexArea.full());
}
- /**
- * Translate the plane by the specified amount.
- * <p>
- * The instance is not modified, a new instance is created.
- * </p>
- *
- * @param translation translation to apply
- * @return a new plane
+ /** Check if the line is parallel to the instance.
+ * @param line line to check.
+ * @return true if the line is parallel to the instance, false otherwise.
*/
- public Plane translate(final Vector3D translation) {
- final Vector3D p = getOrigin().add(translation);
- final Vector3D normal = this.w;
- final Vector3D wTmp = normal.normalize();
- final double originOffsetTmp = -p.dot(wTmp);
+ public boolean isParallel(final Line3D line) {
+ final double dot = normal.dot(line.getDirection());
- return new Plane(this.u, this.v, wTmp, originOffsetTmp, getPrecision());
+ return getPrecision().eqZero(dot);
}
- /**
- * Get one point from the 3D-space.
- *
- * @param inPlane desired in-plane coordinates for the point in the plane
- * @param offset desired offset for the point
- * @return one point in the 3D-space, with given coordinates and offset relative
- * to the plane
+ /** Check if the plane is parallel to the instance.
+ * @param plane plane to check.
+ * @return true if the plane is parallel to the instance, false otherwise.
*/
- public Vector3D pointAt(final Vector2D inPlane, final double offset) {
- return Vector3D.linearCombination(inPlane.getX(), u, inPlane.getY(), v, offset - originOffset, w);
+ public boolean isParallel(final Plane plane) {
+ return getPrecision().eqZero(normal.cross(plane.normal).norm());
}
- /**
- * Check if the instance contains another plane.
- * <p>
- * Planes are considered similar if they contain the same points. This does not
- * mean they are equal since they can have opposite normals.
- * </p>
- *
- * @param plane plane to which the instance is compared
- * @return true if the planes are similar
- */
- public boolean contains(final Plane plane) {
- final double angle = w.angle(plane.w);
- final DoublePrecisionContext precision = getPrecision();
-
- return ((precision.eqZero(angle)) && precision.eq(originOffset, plane.originOffset)) ||
- ((precision.eq(angle, Math.PI)) && precision.eq(originOffset, -plane.originOffset));
+ /** {@inheritDoc} */
+ @Override
+ public boolean similarOrientation(final Hyperplane<Vector3D> other) {
+ return (((Plane) other).normal).dot(normal) > 0;
}
- /**
- * Get the intersection of a line with the instance.
- *
+ /** Get the intersection of a line with this plane.
* @param line line intersecting the instance
* @return intersection point between between the line and the instance (null if
* the line is parallel to the instance)
*/
public Vector3D intersection(final Line3D line) {
final Vector3D direction = line.getDirection();
- final double dot = w.dot(direction);
+ final double dot = normal.dot(direction);
+
if (getPrecision().eqZero(dot)) {
return null;
}
- final Vector3D point = line.toSpace(Vector1D.ZERO);
- final double k = -(originOffset + w.dot(point)) / dot;
- return Vector3D.linearCombination(1.0, point, k, direction);
+
+ final Vector3D point = line.pointAt(0);
+ final double k = -(originOffset + normal.dot(point)) / dot;
+
+ return Vector3D.linearCombination(
+ 1.0, point,
+ k, direction);
}
- /**
- * Get the line formed by the intersection of this instance with the given plane.
+ /** Get the line formed by the intersection of this instance with the given plane.
* The returned line lies in both planes and points in the direction of
* the cross product <code>n<sub>1</sub> x n<sub>2</sub></code>, where <code>n<sub>1</sub></code>
* is the normal of the current instance and <code>n<sub>2</sub></code> is the normal
@@ -370,148 +245,113 @@ public final class Plane extends AbstractHyperplane<Vector3D>
* if no such line exists
*/
public Line3D intersection(final Plane other) {
- final Vector3D direction = w.cross(other.w);
+ final Vector3D direction = normal.cross(other.normal);
+
if (getPrecision().eqZero(direction.norm())) {
return null;
}
+
final Vector3D point = intersection(this, other, Planes.fromNormal(direction, getPrecision()));
+
return Lines3D.fromPointAndDirection(point, direction, getPrecision());
}
- /**
- * Get the intersection point of three planes. Returns null if no unique intersection point
- * exists (ie, there are no intersection points or an infinite number).
- *
- * @param plane1 first plane1
- * @param plane2 second plane2
- * @param plane3 third plane2
- * @return intersection point of the three planes or null if no unique intersection point exists
+ /** Build a new reversed version of this plane, with opposite orientation.
+ * @return a new reversed plane
*/
- public static Vector3D intersection(final Plane plane1, final Plane plane2, final Plane plane3) {
-
- // coefficients of the three planes linear equations
- final double a1 = plane1.w.getX();
- final double b1 = plane1.w.getY();
- final double c1 = plane1.w.getZ();
- final double d1 = plane1.originOffset;
-
- final double a2 = plane2.w.getX();
- final double b2 = plane2.w.getY();
- final double c2 = plane2.w.getZ();
- final double d2 = plane2.originOffset;
-
- final double a3 = plane3.w.getX();
- final double b3 = plane3.w.getY();
- final double c3 = plane3.w.getZ();
- final double d3 = plane3.originOffset;
-
- // direct Cramer resolution of the linear system
- // (this is still feasible for a 3x3 system)
- final double a23 = (b2 * c3) - (b3 * c2);
- final double b23 = (c2 * a3) - (c3 * a2);
- final double c23 = (a2 * b3) - (a3 * b2);
- final double determinant = (a1 * a23) + (b1 * b23) + (c1 * c23);
-
- // use the precision context of the first plane to determine equality
- if (plane1.getPrecision().eqZero(determinant)) {
- return null;
- }
-
- final double r = 1.0 / determinant;
- return Vector3D.of((-a23 * d1 - (c1 * b3 - c3 * b1) * d2 - (c2 * b1 - c1 * b2) * d3) * r,
- (-b23 * d1 - (c3 * a1 - c1 * a3) * d2 - (c1 * a2 - c2 * a1) * d3) * r,
- (-c23 * d1 - (b1 * a3 - b3 * a1) * d2 - (b2 * a1 - b1 * a2) * d3) * r);
-
- }
-
- /** {@inheritDoc} */
@Override
- public PlaneConvexSubset span() {
- return Planes.subsetFromConvexArea(this, ConvexArea.full());
+ public Plane reverse() {
+ return new Plane(normal.negate(), -originOffset, getPrecision());
}
- /**
- * Check if the instance contains a point.
+ /** {@inheritDoc}
*
- * @param p point to check
- * @return true if p belongs to the plane
+ * <p>Instances are transformed by selecting 3 representative points from the
+ * plane, transforming them, and constructing a new plane from the transformed points.
+ * Since the normal is not transformed directly, but rather is constructed new from the
+ * transformed points, the relative orientations of points in the plane are preserved,
+ * even for transforms that do not
+ * {@link Transform#preservesOrientation() preserve orientation}. The example below shows
+ * a plane being transformed by a non-orientation-preserving transform. The normal of the
+ * transformed plane retains its counterclockwise relationship to the points in the plane,
+ * in contrast with the normal that is transformed directly by the transform.
+ * </p>
+ * <pre>
+ * // construct a plane from 3 points; the normal will be selected such that the
+ * // points are ordered counterclockwise when looking down the plane normal.
+ * Vector3D p1 = Vector3D.of(0, 0, 0);
+ * Vector3D p2 = Vector3D.of(+1, 0, 0);
+ * Vector3D p3 = Vector3D.of(0, +1, 0);
+ *
+ * Plane plane = Planes.fromPoints(p1, p2, p3, precision); // normal is (0, 0, +1)
+ *
+ * // create a transform that negates all x-values; this transform does not
+ * // preserve orientation, i.e. it will convert a right-handed system into a left-handed
+ * // system and vice versa
+ * AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(-1, 1, 1);
+ *
+ * // transform the plane
+ * Plane transformedPlane = plane.transform(transform);
+ *
+ * // the plane normal is oriented such that transformed points are still ordered
+ * // counterclockwise when looking down the plane normal; since the point (1, 0, 0) has
+ * // now become (-1, 0, 0), the normal has flipped to (0, 0, -1)
+ * transformedPlane.getNormal();
+ *
+ * // directly transform the original plane normal; the normal is unchanged by the transform
+ * // since the target space of the transform is left-handed
+ * AffineTransformMatrix3D normalTransform = transform.normalTransform();
+ * Vector3D directlyTransformedNormal = normalTransform.apply(plane.getNormal()); // (0, 0, +1)
+ * </pre>
*/
@Override
- public boolean contains(final Vector3D p) {
- return getPrecision().eqZero(offset(p));
- }
+ public Plane transform(final Transform<Vector3D> transform) {
+ // create 3 representation points lying on the plane, transform them,
+ // and use the transformed points to create a new plane
- /**
- * Check if the instance contains a line.
- * @param line line to check
- * @return true if line is contained in this plane
- */
- public boolean contains(final Line3D line) {
- return isParallel(line) && contains(line.getOrigin());
- }
+ final Vector3D u = normal.orthogonal();
+ final Vector3D v = normal.cross(u);
- /** Check if the line is parallel to the instance.
- * @param line line to check.
- * @return true if the line is parallel to the instance, false otherwise.
- */
- public boolean isParallel(final Line3D line) {
- final double dot = w.dot(line.getDirection());
+ final Vector3D p1 = getOrigin();
+ final Vector3D p2 = p1.add(u);
+ final Vector3D p3 = p1.add(v);
- return getPrecision().eqZero(dot);
- }
+ final Vector3D t1 = transform.apply(p1);
+ final Vector3D t2 = transform.apply(p2);
+ final Vector3D t3 = transform.apply(p3);
- /** Check, if the plane is parallel to the instance.
- * @param plane plane to check.
- * @return true if the plane is parallel to the instance, false otherwise.
- */
- public boolean isParallel(final Plane plane) {
- return getPrecision().eqZero(w.cross(plane.w).norm());
+ return Planes.fromPoints(t1, t2, t3, getPrecision());
}
- /**
- * Get the offset (oriented distance) of the given plane with respect to this instance. The value
- * closest to zero is returned, which will always be zero if the planes are not parallel.
- * @param plane plane to calculate the offset of
- * @return the offset of the plane with respect to this instance or 0.0 if the planes
- * are not parallel.
+ /** Translate the plane by the specified amount.
+ * @param translation translation to apply
+ * @return a new plane
*/
- public double offset(final Plane plane) {
- if (!isParallel(plane)) {
- return 0.0;
- }
- return originOffset + (similarOrientation(plane) ? -plane.originOffset : plane.originOffset);
+ public Plane translate(final Vector3D translation) {
+ final Vector3D tOrigin = getOrigin().add(translation);
+
+ return Planes.fromPointAndNormal(tOrigin, normal, getPrecision());
}
- /**
- * Get the offset (oriented distance) of the given line with respect to the plane. The value
- * closest to zero is returned, which will always be zero if the line is not parallel to the plane.
- * @param line line to calculate the offset of
- * @return the offset of the line with respect to the plane or 0.0 if the line
- * is not parallel to the plane.
+ /** Rotate the plane around the specified point.
+ * @param center rotation center
+ * @param rotation 3-dimensional rotation
+ * @return a new plane
*/
- public double offset(final Line3D line) {
- if (!isParallel(line)) {
- return 0.0;
- }
- return offset(line.getOrigin());
- }
+ public Plane rotate(final Vector3D center, final QuaternionRotation rotation) {
+ final Vector3D delta = getOrigin().subtract(center);
+ final Vector3D tOrigin = center.add(rotation.apply(delta));
- /** {@inheritDoc} */
- @Override
- public double offset(final Vector3D point) {
- return point.dot(w) + originOffset;
- }
+ // we can directly apply the rotation to the normal since it will transform
+ // it properly (there is no translation or scaling involved)
+ final Vector3D.Unit tNormal = rotation.apply(normal).normalize();
- /** {@inheritDoc} */
- @Override
- public boolean similarOrientation(final Hyperplane<Vector3D> other) {
- return (((Plane) other).w).dot(w) > 0;
+ return Planes.fromPointAndNormal(tOrigin, tNormal, getPrecision());
}
-
/** Return true if this instance should be considered equivalent to the argument, using the
- * given precision context for comparison. Instances are considered equivalent if they
- * have equivalent {@code origin} points and {@code u} and {@code v} vectors.
+ * given precision context for comparison. Instances are considered equivalent if they contain
+ * the same points, which is determined by comparing the plane {@code origins} and {@code normals}.
* @param other the point to compare with
* @param precision precision context to use for the comparison
* @return true if this instance should be considered equivalent to the argument
@@ -519,14 +359,13 @@ public final class Plane extends AbstractHyperplane<Vector3D>
*/
public boolean eq(final Plane other, final DoublePrecisionContext precision) {
return getOrigin().eq(other.getOrigin(), precision) &&
- u.eq(other.u, precision) &&
- v.eq(other.v, precision);
+ normal.eq(other.normal, precision);
}
/** {@inheritDoc} */
@Override
public int hashCode() {
- return Objects.hash(u, v, w, originOffset, getPrecision());
+ return Objects.hash(normal, originOffset, getPrecision());
}
/** {@inheritDoc} */
@@ -534,15 +373,13 @@ public final class Plane extends AbstractHyperplane<Vector3D>
public boolean equals(Object obj) {
if (this == obj) {
return true;
- } else if (!(obj instanceof Plane)) {
+ } else if (obj == null || obj.getClass() != this.getClass()) {
return false;
}
final Plane other = (Plane) obj;
- return Objects.equals(this.u, other.u) &&
- Objects.equals(this.v, other.v) &&
- Objects.equals(this.w, other.w) &&
+ return Objects.equals(this.normal, other.normal) &&
Double.compare(this.originOffset, other.originOffset) == 0 &&
Objects.equals(this.getPrecision(), other.getPrecision());
}
@@ -554,49 +391,53 @@ public final class Plane extends AbstractHyperplane<Vector3D>
sb.append(getClass().getSimpleName())
.append("[origin= ")
.append(getOrigin())
- .append(", u= ")
- .append(u)
- .append(", v= ")
- .append(v)
- .append(", w= ")
- .append(w)
+ .append(", normal= ")
+ .append(normal)
.append(']');
return sb.toString();
}
- /** Class containing a transformed plane instance along with a subspace (2D) transform. The subspace
- * transform produces the equivalent of the 3D transform in 2D.
+ /** Get the intersection point of three planes. Returns null if no unique intersection point
+ * exists (ie, there are no intersection points or an infinite number).
+ * @param plane1 first plane1
+ * @param plane2 second plane2
+ * @param plane3 third plane2
+ * @return intersection point of the three planes or null if no unique intersection point exists
*/
- public static final class SubspaceTransform {
- /** The transformed plane. */
- private final Plane plane;
-
- /** The subspace transform instance. */
- private final AffineTransformMatrix2D transform;
-
- /** Simple constructor.
- * @param plane the transformed plane
- * @param transform 2D transform that can be applied to subspace points
- */
- public SubspaceTransform(final Plane plane, final AffineTransformMatrix2D transform) {
- this.plane = plane;
- this.transform = transform;
- }
+ public static Vector3D intersection(final Plane plane1, final Plane plane2, final Plane plane3) {
- /** Get the transformed plane instance.
- * @return the transformed plane instance
- */
- public Plane getPlane() {
- return plane;
- }
+ // coefficients of the three planes linear equations
+ final double a1 = plane1.normal.getX();
+ final double b1 = plane1.normal.getY();
+ final double c1 = plane1.normal.getZ();
+ final double d1 = plane1.originOffset;
+
+ final double a2 = plane2.normal.getX();
+ final double b2 = plane2.normal.getY();
+ final double c2 = plane2.normal.getZ();
+ final double d2 = plane2.originOffset;
- /** Get the 2D transform that can be applied to subspace points. This transform can be used
- * to perform the equivalent of the 3D transform in 2D space.
- * @return the subspace transform instance
- */
- public AffineTransformMatrix2D getTransform() {
- return transform;
+ final double a3 = plane3.normal.getX();
+ final double b3 = plane3.normal.getY();
+ final double c3 = plane3.normal.getZ();
+ final double d3 = plane3.originOffset;
+
+ // direct Cramer resolution of the linear system
+ // (this is still feasible for a 3x3 system)
+ final double a23 = (b2 * c3) - (b3 * c2);
+ final double b23 = (c2 * a3) - (c3 * a2);
+ final double c23 = (a2 * b3) - (a3 * b2);
+ final double determinant = (a1 * a23) + (b1 * b23) + (c1 * c23);
+
+ // use the precision context of the first plane to determine equality
+ if (plane1.getPrecision().eqZero(determinant)) {
+ return null;
}
+
+ final double r = 1.0 / determinant;
+ return Vector3D.of((-a23 * d1 - (c1 * b3 - c3 * b1) * d2 - (c2 * b1 - c1 * b2) * d3) * r,
+ (-b23 * d1 - (c3 * a1 - c1 * a3) * d2 - (c1 * a2 - c2 * a1) * d3) * r,
+ (-c23 * d1 - (b1 * a3 - b3 * a1) * d2 - (b2 * a1 - b1 * a2) * d3) * r);
}
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PlaneConvexSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PlaneConvexSubset.java
index 82573a5..26e9169 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PlaneConvexSubset.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PlaneConvexSubset.java
@@ -23,106 +23,60 @@ import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
import org.apache.commons.geometry.core.partitioning.Split;
-import org.apache.commons.geometry.euclidean.threed.Plane.SubspaceTransform;
-import org.apache.commons.geometry.euclidean.threed.line.Line3D;
-import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
-import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
import org.apache.commons.geometry.euclidean.twod.ConvexArea;
-import org.apache.commons.geometry.euclidean.twod.Vector2D;
-/** Class representing a convex subset of points in a plane. The subset may be finite
- * or infinite.
- * @see Planes
+/** Interface representing a finite or infinite convex subset of points in a plane in Euclidean 3D
+ * space.
*/
-public final class PlaneConvexSubset extends PlaneSubset
- implements HyperplaneConvexSubset<Vector3D> {
- /** The embedded 2D area. */
- private final ConvexArea area;
-
- /** Create a new instance from its component parts.
- * @param plane plane the the convex area is embedded in
- * @param area the embedded convex area
- */
- PlaneConvexSubset(final Plane plane, final ConvexArea area) {
- super(plane);
-
- this.area = area;
- }
+public interface PlaneConvexSubset extends PlaneSubset, HyperplaneConvexSubset<Vector3D> {
/** {@inheritDoc} */
@Override
- public List<PlaneConvexSubset> toConvex() {
- return Collections.singletonList(this);
- }
-
- /** {@inheritDoc} */
- @Override
- public PlaneConvexSubset reverse() {
- final Plane plane = getPlane();
- final Plane rPlane = plane.reverse();
-
- final Vector2D rU = rPlane.toSubspace(plane.toSpace(Vector2D.Unit.PLUS_X));
- final Vector2D rV = rPlane.toSubspace(plane.toSpace(Vector2D.Unit.PLUS_Y));
-
- final AffineTransformMatrix2D transform =
- AffineTransformMatrix2D.fromColumnVectors(rU, rV);
-
- return new PlaneConvexSubset(rPlane, area.transform(transform));
- }
+ PlaneConvexSubset reverse();
/** {@inheritDoc} */
@Override
- public PlaneConvexSubset transform(final Transform<Vector3D> transform) {
- final SubspaceTransform st = getPlane().subspaceTransform(transform);
- final ConvexArea tArea = area.transform(st.getTransform());
-
- return Planes.subsetFromConvexArea(st.getPlane(), tArea);
- }
+ PlaneConvexSubset transform(Transform<Vector3D> transform);
/** {@inheritDoc} */
@Override
- public ConvexArea getSubspaceRegion() {
- return area;
- }
+ Split<PlaneConvexSubset> split(Hyperplane<Vector3D> splitter);
/** {@inheritDoc} */
@Override
- public Split<PlaneConvexSubset> split(final Hyperplane<Vector3D> splitter) {
- return splitInternal(splitter, this, (p, r) -> new PlaneConvexSubset(p, (ConvexArea) r));
- }
-
- /** Get the unique intersection of this plane subset with the given line. Null is
- * returned if no unique intersection point exists (ie, the line and plane are
- * parallel or coincident) or the line does not intersect the plane subset.
- * @param line line to intersect with this plane subset
- * @return the unique intersection point between the line and this plane subset
- * or null if no such point exists.
- * @see Plane#intersection(Line3D)
+ PlaneConvexSubset.Embedded getEmbedded();
+
+ /** Get the vertices for the convex subset in a counter-clockwise order as viewed looking down the plane
+ * normal. Each vertex in the returned list is unique. If the boundary of the subset is closed, the start
+ * vertex is <em>not</em> repeated at the end of the list.
+ *
+ * <p>It is important to note that, in general, the list of vertices returned by this method
+ * is not sufficient to completely characterize the subset. For example, a simple triangle
+ * has 3 vertices, but an infinite area constructed from two parallel lines and two lines that
+ * intersect between them will also have 3 vertices. It is also possible for non-empty subsets to
+ * contain no vertices at all. For example, a subset with no boundaries (representing the full
+ * plane), a subset with a single boundary (ie, a half-plane), or a subset with two parallel boundaries will
+ * not contain any vertices.</p>
+ * @return the list of vertices for the plane convex subset in a counter-clockwise order as viewed looking
+ * down the plane normal
*/
- public Vector3D intersection(final Line3D line) {
- final Vector3D pt = getPlane().intersection(line);
- return (pt != null && contains(pt)) ? pt : null;
- }
+ List<Vector3D> getVertices();
- /** Get the unique intersection of this plane subset with the given line subset. Null
- * is returned if the underlying line and plane do not have a unique intersection
- * point (ie, they are parallel or coincident) or the intersection point is unique
- * but is not contained in both the line subset and plane subset.
- * @param lineSubset line subset to intersect with
- * @return the unique intersection point between this plane subset and the argument or
- * null if no such point exists.
- * @see Plane#intersection(Line3D)
+ /** {@inheritDoc}
+ *
+ * <p>This method simply returns a singleton list containing this object.</p>
*/
- public Vector3D intersection(final LineConvexSubset3D lineSubset) {
- final Vector3D pt = intersection(lineSubset.getLine());
- return (pt != null && lineSubset.contains(pt)) ? pt : null;
+ @Override
+ default List<PlaneConvexSubset> toConvex() {
+ return Collections.singletonList(this);
}
- /** Get the vertices for the plane subset. The vertices lie at the intersections of the
- * 2D area bounding lines.
- * @return the vertices for the plane subset
+ /** Interface used to represent plane convex subsets as embedded 2D subspace regions.
*/
- public List<Vector3D> getVertices() {
- return getPlane().toSpace(area.getVertices());
+ interface Embedded extends PlaneSubset.Embedded {
+
+ /** {@inheritDoc} */
+ @Override
+ ConvexArea getSubspaceRegion();
}
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PlaneSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PlaneSubset.java
index 945f87d..0583cf0 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PlaneSubset.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PlaneSubset.java
@@ -17,236 +17,87 @@
package org.apache.commons.geometry.euclidean.threed;
import java.util.List;
-import java.util.Objects;
-import java.util.function.BiFunction;
-import org.apache.commons.geometry.core.partitioning.AbstractRegionEmbeddingHyperplaneSubset;
-import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.RegionEmbedding;
import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
-import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
-import org.apache.commons.geometry.core.partitioning.Split;
-import org.apache.commons.geometry.core.partitioning.SplitLocation;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.euclidean.threed.line.Line3D;
-import org.apache.commons.geometry.euclidean.twod.Line;
-import org.apache.commons.geometry.euclidean.twod.Lines;
+import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
-/** Class representing a subset of points in a 3D Euclidean space. For example, triangles
- * and other polygons in 3D are plane subsets. Instances may be finite or infinite.
+/** Interface representing a subset of points in a plane in Euclidean 3D space. Instances
+ * may represent finite, infinite, convex, non-convex, and/or disjoint regions of the plane.
*/
-public abstract class PlaneSubset
- extends AbstractRegionEmbeddingHyperplaneSubset<Vector3D, Vector2D, Plane> {
- /** The plane defining this instance. */
- private final Plane plane;
+public interface PlaneSubset extends HyperplaneSubset<Vector3D> {
- /** Construct a new instance based on the given plane.
- * @param plane the plane defining the subset
- */
- PlaneSubset(final Plane plane) {
- this.plane = plane;
- }
-
- /** Get the plane that this subset lies on. This method is an alias
- * for {@link #getHyperplane()}.
- * @return the plane that this subset lies on
+ /** Get the plane containing this subset. This is equivalent to {@link #getHyperplane()}.
+ * @return the plane containing this subset
* @see #getHyperplane()
*/
- public Plane getPlane() {
- return getHyperplane();
- }
+ Plane getPlane();
/** {@inheritDoc} */
@Override
- public Plane getHyperplane() {
- return plane;
- }
+ Plane getHyperplane();
/** {@inheritDoc} */
@Override
- public abstract List<PlaneConvexSubset> toConvex();
-
- /** {@inheritDoc} */
- @Override
- public HyperplaneSubset.Builder<Vector3D> builder() {
- return new Builder(plane);
- }
-
- /** Return the object used to perform floating point comparisons, which is the
- * same object used by the underlying {@link Plane}).
- * @return precision object used to perform floating point comparisons.
+ List<PlaneConvexSubset> toConvex();
+
+ /** Return a list of triangles representing the same subset region as this instance. An
+ * {@link IllegalStateException} is thrown if the subset has infinite size and therefore
+ * cannot be converted to triangles. If the subset has zero size (is empty), an empty list is
+ * returned.
+ * @return a list of triangles representing the same subset region as this instance
+ * @throws IllegalStateException if the subset has infinite size and therefore cannot
+ * be converted to triangles
*/
- public DoublePrecisionContext getPrecision() {
- return plane.getPrecision();
- }
+ List<Triangle3D> toTriangles();
- /** {@inheritDoc} */
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder();
- sb.append(getClass().getSimpleName())
- .append("[plane= ")
- .append(getPlane())
- .append(", subspaceRegion= ")
- .append(getSubspaceRegion())
- .append(']');
-
-
- return sb.toString();
- }
-
- /** Generic, internal split method. Subclasses should call this from their
- * {@link #split(Hyperplane)} methods.
- * @param splitter splitting hyperplane
- * @param thisInstance a reference to the current instance; this is passed as
- * an argument in order to allow it to be a generic type
- * @param factory function used to create new hyperplane subset instances
- * @param <T> Plane subset implementation type
- * @return the result of the split operation
+ /** Get a {@link Bounds3D} object defining an axis-aligned bounding box containing all
+ * vertices for this subset. Null is returned if the subset is infinite or does not
+ * contain any vertices.
+ * @return the bounding box for this instance or null if no valid bounds could be determined
*/
- protected <T extends PlaneSubset> Split<T> splitInternal(final Hyperplane<Vector3D> splitter,
- final T thisInstance, final BiFunction<Plane, HyperplaneBoundedRegion<Vector2D>, T> factory) {
-
- final Plane thisPlane = thisInstance.getPlane();
- final Plane splitterPlane = (Plane) splitter;
- final DoublePrecisionContext precision = thisInstance.getPrecision();
-
- final Line3D intersection = thisPlane.intersection(splitterPlane);
- if (intersection == null) {
- // the planes are parallel or coincident; check which side of
- // the splitter we lie on
- final double offset = splitterPlane.offset(thisPlane);
- final int comp = precision.compare(offset, 0.0);
-
- if (comp < 0) {
- return new Split<>(thisInstance, null);
- } else if (comp > 0) {
- return new Split<>(null, thisInstance);
- } else {
- return new Split<>(null, null);
- }
- } else {
- // the lines intersect; split the subregion
- final Vector3D intersectionOrigin = intersection.getOrigin();
- final Vector2D subspaceP1 = thisPlane.toSubspace(intersectionOrigin);
- final Vector2D subspaceP2 = thisPlane.toSubspace(intersectionOrigin.add(intersection.getDirection()));
-
- final Line subspaceSplitter = Lines.fromPoints(subspaceP1, subspaceP2, getPrecision());
-
- final Split<? extends HyperplaneBoundedRegion<Vector2D>> split =
- thisInstance.getSubspaceRegion().split(subspaceSplitter);
- final SplitLocation subspaceSplitLoc = split.getLocation();
-
- if (SplitLocation.MINUS == subspaceSplitLoc) {
- return new Split<>(thisInstance, null);
- } else if (SplitLocation.PLUS == subspaceSplitLoc) {
- return new Split<>(null, thisInstance);
- }
-
- final T minus = (split.getMinus() != null) ? factory.apply(getPlane(), split.getMinus()) : null;
- final T plus = (split.getPlus() != null) ? factory.apply(getPlane(), split.getPlus()) : null;
-
- return new Split<>(minus, plus);
- }
- }
+ Bounds3D getBounds();
- /** Internal implementation of the {@link HyperplaneSubset.Builder} interface. In cases where only a single
- * convex subset is given to the builder, this class returns the convex subset instance directly. In all other
- * cases, an {@link EmbeddedTreePlaneSubset} is used to construct the final subset.
+ /** Return an object containing the plane subset as an embedded 2D subspace region.
+ * @return an object containing the plane subset as an embedded 2D subspace region
*/
- private static final class Builder implements HyperplaneSubset.Builder<Vector3D> {
- /** Plane that a subset is being constructed for. */
- private final Plane plane;
-
- /** Embedded tree subset. */
- private EmbeddedTreePlaneSubset treeSubset;
+ PlaneSubset.Embedded getEmbedded();
+
+ /** Get the unique intersection of this plane subset with the given line. Null is
+ * returned if no unique intersection point exists (ie, the line and plane are
+ * parallel or coincident) or the line does not intersect the plane subset.
+ * @param line line to intersect with this plane subset
+ * @return the unique intersection point between the line and this plane subset
+ * or null if no such point exists.
+ * @see Plane#intersection(Line3D)
+ */
+ Vector3D intersection(Line3D line);
+
+ /** Get the unique intersection of this plane subset with the given line subset. Null
+ * is returned if the underlying line and plane do not have a unique intersection
+ * point (ie, they are parallel or coincident) or the intersection point is unique
+ * but is not contained in both the line subset and plane subset.
+ * @param lineSubset line subset to intersect with
+ * @return the unique intersection point between this plane subset and the argument or
+ * null if no such point exists.
+ * @see Plane#intersection(Line3D)
+ */
+ Vector3D intersection(LineConvexSubset3D lineSubset);
- /** Convex subset added as the first subset to the builder. This is returned directly if
- * no other subsets are added.
- */
- private PlaneConvexSubset convexSubset;
+ /** Interface used to represent plane subsets as embedded 2D subspace regions.
+ */
+ interface Embedded extends RegionEmbedding<Vector3D, Vector2D> {
- /** Create a new subset builder for the given plane.
- * @param plane plane to build a subset for
+ /** Get the plane embedding the subspace region.
+ * @return the plane embedding the subspace region
*/
- Builder(final Plane plane) {
- this.plane = plane;
- }
+ EmbeddingPlane getPlane();
/** {@inheritDoc} */
@Override
- public void add(final HyperplaneSubset<Vector3D> sub) {
- addInternal(sub);
- }
-
- /** {@inheritDoc} */
- @Override
- public void add(final HyperplaneConvexSubset<Vector3D> sub) {
- addInternal(sub);
- }
-
- /** {@inheritDoc} */
- @Override
- public PlaneSubset build() {
- // return the convex subset directly if that was all we were given
- if (convexSubset != null) {
- return convexSubset;
- }
- return getTreeSubset();
- }
-
- /** Internal method for adding hyperplane subsets to this builder.
- * @param sub the hyperplane subset to add; may be either convex or non-convex
- */
- private void addInternal(final HyperplaneSubset<Vector3D> sub) {
- Objects.requireNonNull(sub, "Hyperplane subset must not be null");
-
- if (sub instanceof PlaneConvexSubset) {
- addConvexSubset((PlaneConvexSubset) sub);
- } else if (sub instanceof EmbeddedTreePlaneSubset) {
- addTreeSubset((EmbeddedTreePlaneSubset) sub);
- } else {
- throw new IllegalArgumentException("Unsupported hyperplane subset type: " + sub.getClass().getName());
- }
- }
-
- /** Add a convex subset to the builder.
- * @param convex convex subset to add
- */
- private void addConvexSubset(final PlaneConvexSubset convex) {
- Planes.validatePlanesEquivalent(plane, convex.getPlane());
-
- if (treeSubset == null && convexSubset == null) {
- convexSubset = convex;
- } else {
- getTreeSubset().add(convex);
- }
- }
-
- /** Add an embedded tree subset to the builder.
- * @param tree embedded tree subset to add
- */
- private void addTreeSubset(final EmbeddedTreePlaneSubset tree) {
- // no need to validate the line here since the add() method does that for us
- getTreeSubset().add(tree);
- }
-
- /** Get the tree subset for the builder, creating it if needed.
- * @return the tree subset for the builder
- */
- private EmbeddedTreePlaneSubset getTreeSubset() {
- if (treeSubset == null) {
- treeSubset = new EmbeddedTreePlaneSubset(plane);
-
- if (convexSubset != null) {
- treeSubset.add(convexSubset);
-
- convexSubset = null;
- }
- }
-
- return treeSubset;
- }
+ HyperplaneBoundedRegion<Vector2D> getSubspaceRegion();
}
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Planes.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Planes.java
index fcba291..9da6697 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Planes.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Planes.java
@@ -16,13 +16,23 @@
*/
package org.apache.commons.geometry.euclidean.threed;
+import java.text.MessageFormat;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
+import java.util.function.BiFunction;
+import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.line.Line3D;
+import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
import org.apache.commons.geometry.euclidean.twod.ConvexArea;
+import org.apache.commons.geometry.euclidean.twod.Line;
+import org.apache.commons.geometry.euclidean.twod.Lines;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
/** Class containing factory methods for constructing {@link Plane} and {@link PlaneSubset} instances.
@@ -33,8 +43,7 @@ public final class Planes {
private Planes() {
}
- /**
- * Build a plane from a point and two (on plane) vectors.
+ /** Build a plane from a point and two (on plane) vectors.
* @param p the provided point (on plane)
* @param u u vector (on plane)
* @param v v vector (on plane)
@@ -42,18 +51,17 @@ public final class Planes {
* @return a new plane
* @throws IllegalArgumentException if the norm of the given values is zero, NaN, or infinite.
*/
- public static Plane fromPointAndPlaneVectors(final Vector3D p, final Vector3D u, final Vector3D v,
+ public static EmbeddingPlane fromPointAndPlaneVectors(final Vector3D p, final Vector3D u, final Vector3D v,
final DoublePrecisionContext precision) {
- final Vector3D uNorm = u.normalize();
- final Vector3D vNorm = uNorm.orthogonal(v);
- final Vector3D wNorm = uNorm.cross(vNorm).normalize();
+ final Vector3D.Unit uNorm = u.normalize();
+ final Vector3D.Unit vNorm = uNorm.orthogonal(v);
+ final Vector3D.Unit wNorm = uNorm.cross(vNorm).normalize();
final double originOffset = -p.dot(wNorm);
- return new Plane(uNorm, vNorm, wNorm, originOffset, precision);
+ return new EmbeddingPlane(uNorm, vNorm, wNorm, originOffset, precision);
}
- /**
- * Build a plane from a normal.
+ /** Build a plane from a normal.
* Chooses origin as point on plane.
* @param normal normal direction to the plane
* @param precision precision context used to compare floating point values
@@ -64,8 +72,7 @@ public final class Planes {
return fromPointAndNormal(Vector3D.ZERO, normal, precision);
}
- /**
- * Build a plane from a point and a normal.
+ /** Build a plane from a point and a normal.
*
* @param p point belonging to the plane
* @param normal normal direction to the plane
@@ -75,17 +82,13 @@ public final class Planes {
*/
public static Plane fromPointAndNormal(final Vector3D p, final Vector3D normal,
final DoublePrecisionContext precision) {
- final Vector3D w = normal.normalize();
- final double originOffset = -p.dot(w);
+ final Vector3D.Unit unitNormal = normal.normalize();
+ final double originOffset = -p.dot(unitNormal);
- final Vector3D u = w.orthogonal();
- final Vector3D v = w.cross(u);
-
- return new Plane(u, v, w, originOffset, precision);
+ return new Plane(unitNormal, originOffset, precision);
}
- /**
- * Build a plane from three points.
+ /** Build a plane from three points.
* <p>
* The plane is oriented in the direction of {@code (p2-p1) ^ (p3-p1)}
* </p>
@@ -116,161 +119,607 @@ public final class Planes {
* points do not define a unique plane
*/
public static Plane fromPoints(final Collection<Vector3D> pts, final DoublePrecisionContext precision) {
+ return new PlaneBuilder(pts, precision).build();
+ }
- if (pts.size() < 3) {
- throw new IllegalArgumentException("At least 3 points are required to define a plane; " +
- "argument contains only " + pts.size() + ".");
+ /** Create a new plane subset from a plane and an embedded convex subspace area.
+ * @param plane embedding plane for the area
+ * @param area area embedded in the plane
+ * @return a new convex sub plane instance
+ */
+ public static PlaneConvexSubset subsetFromConvexArea(final EmbeddingPlane plane, final ConvexArea area) {
+ if (area.isFinite()) {
+ // prefer a vertex-based representation for finite areas
+ final List<Vector3D> vertices = plane.toSpace(area.getVertices());
+
+ return fromConvexPlanarVertices(plane, vertices);
}
- final Iterator<Vector3D> it = pts.iterator();
+ return new EmbeddedAreaPlaneConvexSubset(plane, area);
+ }
- final Vector3D startPt = it.next();
+ /** Create a new convex polygon from the given sequence of vertices. The vertices must define a unique
+ * plane, meaning that at least 3 unique vertices must be given. The given sequence is assumed to be closed,
+ * ie that an edge exists between the last vertex and the first.
+ * @param pts collection of points defining the convex polygon
+ * @param precision precision context used to compare floating point values
+ * @return a new convex polygon defined by the given sequence of vertices
+ * @throws IllegalArgumentException if fewer than 3 vertices are given or the vertices do not define a
+ * unique plane
+ * @see #fromPoints(Collection, DoublePrecisionContext)
+ */
+ public static ConvexPolygon3D convexPolygonFromVertices(final Collection<Vector3D> pts,
+ final DoublePrecisionContext precision) {
+ final List<Vector3D> vertices = new ArrayList<>(pts.size());
+ final Plane plane = new PlaneBuilder(pts, precision).buildForConvexPolygon(vertices);
+
+ // make sure that the first point is not repeated at the end
+ final Vector3D firstPt = vertices.get(0);
+ final Vector3D lastPt = vertices.get(vertices.size() - 1);
+ if (firstPt.eq(lastPt, precision)) {
+ vertices.remove(vertices.size() - 1);
+ }
- Vector3D u = null;
- Vector3D w = null;
+ if (vertices.size() == 3) {
+ return new SimpleTriangle3D(plane, vertices.get(0), vertices.get(1), vertices.get(2));
+ }
+ return new VertexListConvexPolygon3D(plane, vertices);
+ }
- Vector3D currentPt;
- Vector3D prevPt = startPt;
+ /** Construct a triangle from three vertices. The triangle plane is oriented such that the points
+ * are arranged in a counter-clockwise order when looking down the plane normal.
+ * @param p1 first vertex
+ * @param p2 second vertex
+ * @param p3 third vertex
+ * @param precision precision context used for floating point comparisons
+ * @return a triangle constructed from the three vertices
+ * @throws IllegalArgumentException if the points do not define a unique plane
+ */
+ public static Triangle3D triangleFromVertices(final Vector3D p1, final Vector3D p2, final Vector3D p3,
+ final DoublePrecisionContext precision) {
+ final Plane plane = fromPoints(p1, p2, p3, precision);
+ return new SimpleTriangle3D(plane, p1, p2, p3);
+ }
- Vector3D currentVector = null;
- Vector3D prevVector = null;
+ /** Construct a list of {@link Triangle3D} instances from a set of vertices and arrays of face indices.
+ * For example, the following code constructs a list of triangles forming a square pyramid.
+ * <pre>
+ * DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-10);
+ *
+ * Vector3D[] vertices = {
+ * Vector3D.ZERO,
+ * Vector3D.of(1, 0, 0),
+ * Vector3D.of(1, 1, 0),
+ * Vector3D.of(0, 1, 0),
+ * Vector3D.of(0.5, 0.5, 4)
+ * };
+ *
+ * int[][] faceIndices = {
+ * {0, 2, 1},
+ * {0, 3, 2},
+ * {0, 1, 4},
+ * {1, 2, 4},
+ * {2, 3, 4},
+ * {3, 0, 4}
+ * };
+ *
+ * List<Triangle3D> triangles = Planes.indexedTriangles(vertices, faceIndices, TEST_PRECISION);
+ * </pre>
+ * @param vertices vertices available for use in triangle construction
+ * @param faceIndices array of indices for each triangular face; each entry in the array is an array of
+ * 3 index values into {@code vertices}, defining the 3 vertices that will be used to construct the
+ * triangle
+ * @param precision precision context used for floating point comparisons
+ * @return a list of triangles constructed from the set of vertices and face indices
+ * @throws IllegalArgumentException if any face index array does not contain exactly 3 elements or a set
+ * of 3 vertices do not define a plane
+ * @throws IndexOutOfBoundsException if any index into {@code vertices} is out of bounds
+ */
+ public static List<Triangle3D> indexedTriangles(final Vector3D[] vertices, final int[][] faceIndices,
+ final DoublePrecisionContext precision) {
+ return indexedTriangles(Arrays.asList(vertices), faceIndices, precision);
+ }
- Vector3D cross = null;
- double crossNorm;
- double crossSumX = 0.0;
- double crossSumY = 0.0;
- double crossSumZ = 0.0;
+ /** Construct a list of {@link Triangle3D} instances from a set of vertices and arrays of face indices.
+ * @param vertices vertices available for use in triangle construction
+ * @param faceIndices array of indices for each triangular face; each entry in the array is an array of
+ * 3 index values into {@code vertices}, defining the 3 vertices that will be used to construct the
+ * triangle
+ * @param precision precision context used for floating point comparisons
+ * @return a list of triangles constructed from the set of vertices and face indices
+ * @throws IllegalArgumentException if any face index array does not contain exactly 3 elements or a set
+ * of 3 vertices do not define a plane
+ * @throws IndexOutOfBoundsException if any index into {@code vertices} is out of bounds
+ * @see #indexedTriangles(Vector3D[], int[][], DoublePrecisionContext)
+ */
+ public static List<Triangle3D> indexedTriangles(final List<Vector3D> vertices, final int[][] faceIndices,
+ final DoublePrecisionContext precision) {
- boolean nonPlanar = false;
+ final int numFaces = faceIndices.length;
+ final List<Triangle3D> triangles = new ArrayList<>(numFaces);
- while (it.hasNext()) {
- currentPt = it.next();
+ int[] face;
+ for (int i = 0; i < numFaces; ++i) {
+ face = faceIndices[i];
+ if (face.length != 3) {
+ throw new IllegalArgumentException(MessageFormat.format(
+ "Invalid number of vertex indices for face at index {0}: expected 3 but found {1}",
+ i, face.length));
+ }
- if (!currentPt.eq(prevPt, precision)) {
- currentVector = startPt.vectorTo(currentPt);
+ triangles.add(triangleFromVertices(
+ vertices.get(face[0]),
+ vertices.get(face[1]),
+ vertices.get(face[2]),
+ precision
+ ));
+ }
- if (u == null) {
- // save the first non-zero vector as our u vector
- u = currentVector.normalize();
- }
- if (prevVector != null) {
- cross = prevVector.cross(currentVector);
-
- crossSumX += cross.getX();
- crossSumY += cross.getY();
- crossSumZ += cross.getZ();
-
- crossNorm = cross.norm();
-
- if (!precision.eqZero(crossNorm)) {
- // the cross product has non-zero magnitude
- if (w == null) {
- // save the first non-zero cross product as our normal
- w = cross.normalize();
- } else if (!precision.eq(1.0, Math.abs(w.dot(cross) / crossNorm))) {
- // if the normalized dot product is not either +1 or -1, then
- // the points are not coplanar
- nonPlanar = true;
- break;
- }
- }
- }
+ return triangles;
+ }
- prevVector = currentVector;
- prevPt = currentPt;
+ /** Construct a list of {@link ConvexPolygon3D} instances from a set of vertices and arrays of face indices. Each
+ * face must contain at least 3 vertices but the number of vertices per face does not need to be constant.
+ * For example, the following code constructs a list of convex polygons forming a square pyramid.
+ * Note that the first face (the pyramid base) uses a different number of vertices than the other faces.
+ * <pre>
+ * DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-10);
+ *
+ * Vector3D[] vertices = {
+ * Vector3D.ZERO,
+ * Vector3D.of(1, 0, 0),
+ * Vector3D.of(1, 1, 0),
+ * Vector3D.of(0, 1, 0),
+ * Vector3D.of(0.5, 0.5, 4)
+ * };
+ *
+ * int[][] faceIndices = {
+ * {0, 3, 2, 1}, // square base
+ * {0, 1, 4},
+ * {1, 2, 4},
+ * {2, 3, 4},
+ * {3, 0, 4}
+ * };
+ *
+ * List<ConvexPolygon3D> polygons = Planes.indexedConvexPolygons(vertices, faceIndices, precision);
+ * </pre>
+ * @param vertices vertices available for use in convex polygon construction
+ * @param faceIndices array of indices for each triangular face; each entry in the array is an array of
+ * at least 3 index values into {@code vertices}, defining the vertices that will be used to construct the
+ * convex polygon
+ * @param precision precision context used for floating point comparisons
+ * @return a list of convex polygons constructed from the set of vertices and face indices
+ * @throws IllegalArgumentException if any face index array does not contain at least 3 elements or a set
+ * of vertices do not define a planar convex polygon
+ * @throws IndexOutOfBoundsException if any index into {@code vertices} is out of bounds
+ */
+ public static List<ConvexPolygon3D> indexedConvexPolygons(final Vector3D[] vertices, final int[][] faceIndices,
+ final DoublePrecisionContext precision) {
+ return indexedConvexPolygons(Arrays.asList(vertices), faceIndices, precision);
+ }
+
+ /** Construct a list of {@link ConvexPolygon3D} instances from a set of vertices and arrays of face indices. Each
+ * face must contain at least 3 vertices but the number of vertices per face does not need to be constant.
+ * @param vertices vertices available for use in convex polygon construction
+ * @param faceIndices array of indices for each triangular face; each entry in the array is an array of
+ * at least 3 index values into {@code vertices}, defining the vertices that will be used to construct the
+ * convex polygon
+ * @param precision precision context used for floating point comparisons
+ * @return a list of convex polygons constructed from the set of vertices and face indices
+ * @throws IllegalArgumentException if any face index array does not contain at least 3 elements or a set
+ * of vertices do not define a planar convex polygon
+ * @throws IndexOutOfBoundsException if any index into {@code vertices} is out of bounds
+ * @see #indexedConvexPolygons(Vector3D[], int[][], DoublePrecisionContext)
+ */
+ public static List<ConvexPolygon3D> indexedConvexPolygons(final List<Vector3D> vertices, final int[][] faceIndices,
+ final DoublePrecisionContext precision) {
+ final int numFaces = faceIndices.length;
+ final List<ConvexPolygon3D> polygons = new ArrayList<>(numFaces);
+ final List<Vector3D> faceVertices = new ArrayList<>();
+
+ int[] face;
+ for (int i = 0; i < numFaces; ++i) {
+ face = faceIndices[i];
+ if (face.length < 3) {
+ throw new IllegalArgumentException(MessageFormat.format(
+ "Invalid number of vertex indices for face at index {0}: required at least 3 but found {1}",
+ i, face.length));
+ }
+
+ for (int j = 0; j < face.length; ++j) {
+ faceVertices.add(vertices.get(face[j]));
}
- }
- if (u == null || w == null || nonPlanar) {
- throw new IllegalArgumentException("Points do not define a plane: " + pts);
+ polygons.add(convexPolygonFromVertices(
+ faceVertices,
+ precision
+ ));
+
+ faceVertices.clear();
}
- if (w.dot(Vector3D.of(crossSumX, crossSumY, crossSumZ)) < 0) {
- w = w.negate();
+ return polygons;
+ }
+
+ /** Get the unique intersection of the plane subset with the given line. Null is
+ * returned if no unique intersection point exists (ie, the line and plane are
+ * parallel or coincident) or the line does not intersect the plane subset.
+ * @param planeSubset plane subset to intersect with
+ * @param line line to intersect with this plane subset
+ * @return the unique intersection point between the line and this plane subset
+ * or null if no such point exists.
+ */
+ static Vector3D intersection(final PlaneSubset planeSubset, final Line3D line) {
+ final Vector3D pt = planeSubset.getPlane().intersection(line);
+ return (pt != null && planeSubset.contains(pt)) ? pt : null;
+ }
+
+ /** Get the unique intersection of the plane subset with the given line subset. Null
+ * is returned if the underlying line and plane do not have a unique intersection
+ * point (ie, they are parallel or coincident) or the intersection point is unique
+ * but is not contained in both the line subset and plane subset.
+ * @param planeSubset plane subset to intersect with
+ * @param lineSubset line subset to intersect with
+ * @return the unique intersection point between this plane subset and the argument or
+ * null if no such point exists.
+ */
+ static Vector3D intersection(final PlaneSubset planeSubset, final LineConvexSubset3D lineSubset) {
+ final Vector3D pt = intersection(planeSubset, lineSubset.getLine());
+ return (pt != null && lineSubset.contains(pt)) ? pt : null;
+ }
+
+ /** Validate that the actual plane contains the same points as the expected plane, throwing an exception if not.
+ * The subspace orientations of embedding planes are not considered.
+ * @param expected the expected plane
+ * @param actual the actual plane
+ * @throws IllegalArgumentException if the actual plane is not equivalent to the expected plane
+ */
+ static void validatePlanesEquivalent(final Plane expected, final Plane actual) {
+ if (!expected.eq(actual, expected.getPrecision())) {
+ throw new IllegalArgumentException("Arguments do not represent the same plane. Expected " +
+ expected + " but was " + actual + ".");
}
+ }
+
+ /** Generic split method that uses performs the split using the subspace region of the plane subset.
+ * @param splitter splitting hyperplane
+ * @param subset the plane subset being split
+ * @param factory function used to create new plane subset instances
+ * @param <T> Plane subset implementation type
+ * @return the result of the split operation
+ */
+ static <T extends PlaneSubset> Split<T> subspaceSplit(final Plane splitter, final T subset,
+ final BiFunction<EmbeddingPlane, HyperplaneBoundedRegion<Vector2D>, T> factory) {
+
+ final EmbeddingPlane thisPlane = subset.getPlane().getEmbedding();
+
+ final Line3D intersection = thisPlane.intersection(splitter);
+ if (intersection == null) {
+ return getNonIntersectingSplitResult(splitter, subset);
+ } else {
+ final EmbeddingPlane embeddingPlane = subset.getPlane().getEmbedding();
+
+ // the lines intersect; split the subregion
+ final Vector3D intersectionOrigin = intersection.getOrigin();
+ final Vector2D subspaceP1 = embeddingPlane.toSubspace(intersectionOrigin);
+ final Vector2D subspaceP2 = embeddingPlane.toSubspace(intersectionOrigin.add(intersection.getDirection()));
+
+ final Line subspaceSplitter = Lines.fromPoints(subspaceP1, subspaceP2, thisPlane.getPrecision());
+
+ final Split<? extends HyperplaneBoundedRegion<Vector2D>> split =
+ subset.getEmbedded().getSubspaceRegion().split(subspaceSplitter);
+ final SplitLocation subspaceSplitLoc = split.getLocation();
+
+ if (SplitLocation.MINUS == subspaceSplitLoc) {
+ return new Split<>(subset, null);
+ } else if (SplitLocation.PLUS == subspaceSplitLoc) {
+ return new Split<>(null, subset);
+ }
- final Vector3D v = w.cross(u);
- final double originOffset = -startPt.dot(w);
+ final T minus = (split.getMinus() != null) ? factory.apply(thisPlane, split.getMinus()) : null;
+ final T plus = (split.getPlus() != null) ? factory.apply(thisPlane, split.getPlus()) : null;
- return new Plane(u, v, w, originOffset, precision);
+ return new Split<>(minus, plus);
+ }
}
- /** Create a new plane subset from a plane and an embedded convex subspace area.
- * @param plane embedding plane for the area
- * @param area area embedded in the plane
- * @return a new convex sub plane instance
+ /** Get a split result for cases where the splitting plane and the plane containing the subset being split
+ * do not intersect. Callers are responsible for ensuring that the planes involved do not actually intersect.
+ * @param <T> Plane subset implementation type
+ * @param splitter plane performing the splitting
+ * @param subset subset being split
+ * @return the split result for the non-intersecting split
*/
- public static PlaneConvexSubset subsetFromConvexArea(final Plane plane, final ConvexArea area) {
- return new PlaneConvexSubset(plane, area);
+ private static <T extends PlaneSubset> Split<T> getNonIntersectingSplitResult(
+ final Plane splitter, final T subset) {
+ final Plane plane = subset.getPlane();
+
+ final double offset = splitter.offset(plane);
+ final int comp = plane.getPrecision().compare(offset, 0.0);
+
+ if (comp < 0) {
+ return new Split<>(subset, null);
+ } else if (comp > 0) {
+ return new Split<>(null, subset);
+ } else {
+ return new Split<>(null, null);
+ }
}
- /** Create a new plane subset from the given sequence of points. The points must define a unique plane,
- * meaning that at least 3 unique vertices must be given. In contrast with the
- * {@link #subsetFromVertices(Collection, DoublePrecisionContext)} method, the first point in the sequence
- * is included at the end if needed, in order to form a closed loop.
- * @param pts collection of points defining the plane subset
- * @param precision precision context used to compare floating point values
- * @return a new plane subset defined by the given sequence of vertices
- * @throws IllegalArgumentException if fewer than 3 vertices are given or the vertices do not define a
- * unique plane
- * @see #subsetFromVertices(Collection, boolean, DoublePrecisionContext)
- * @see #fromPoints(Collection, DoublePrecisionContext)
+ /** Construct a convex polygon 3D from a plane and a list of vertices lying in the plane. Callers are
+ * responsible for ensuring that the vertices lie in the plane and define a convex polygon.
+ * @param plane the plane containing the convex polygon
+ * @param vertices vertices defining the closed, convex polygon. The must must contain at least 3 unique
+ * vertices and should not include the start vertex at the end of the list.
+ * @return a new convex polygon instance
+ * @throws IllegalArgumentException if the size of {@code vertices} if less than 3
*/
- public static PlaneConvexSubset subsetFromVertexLoop(final Collection<Vector3D> pts,
- final DoublePrecisionContext precision) {
- return subsetFromVertices(pts, true, precision);
+ static ConvexPolygon3D fromConvexPlanarVertices(final Plane plane, final List<Vector3D> vertices) {
+ final int size = vertices.size();
+
+ if (size == 3) {
+ return new SimpleTriangle3D(plane, vertices.get(0), vertices.get(1), vertices.get(2));
+ }
+
+ return new VertexListConvexPolygon3D(plane, vertices);
}
- /** Create a new plane subset from the given sequence of points. The points must define a unique plane,
- * meaning that at least 3 unique vertices must be given.
- * @param pts collection of points defining the plane subset
- * @param precision precision context used to compare floating point values
- * @return a new plane subset defined by the given sequence of vertices
- * @throws IllegalArgumentException if fewer than 3 vertices are given or the vertices do not define a
- * unique plane
- * @see #subsetFromVertexLoop(Collection, DoublePrecisionContext)
- * @see #subsetFromVertices(Collection, boolean, DoublePrecisionContext)
- * @see #fromPoints(Collection, DoublePrecisionContext)
+ /** Convert a convex polygon defined by a plane and list of points into a triangle fan.
+ * @param plane plane containing the convex polygon
+ * @param vertices vertices defining the convex polygon
+ * @return a triangle fan representing the same area as the convex polygon
+ * @throws IllegalArgumentException if fewer than 3 vertices are given
*/
- public static PlaneConvexSubset subsetFromVertices(final Collection<Vector3D> pts,
- final DoublePrecisionContext precision) {
- return subsetFromVertices(pts, false, precision);
+ static List<Triangle3D> convexPolygonToTriangleFan(final Plane plane, final List<Vector3D> vertices) {
+ final int size = vertices.size();
+ if (size < 3) {
+ throw new IllegalArgumentException("Cannot create triangle fan: 3 or more vertices are required " +
+ "but found only " + vertices.size());
+ }
+
+ final List<Triangle3D> triangles = new ArrayList<>(size - 2);
+
+ int fanIdx = findBestTriangleFanIndex(vertices);
+ int vertexIdx = (fanIdx + 1) % size;
+
+ Vector3D fanBase = vertices.get(fanIdx);
+ Vector3D vertexA = vertices.get(vertexIdx);
+ Vector3D vertexB;
+
+ vertexIdx = (vertexIdx + 1) % size;
+ while (vertexIdx != fanIdx) {
+ vertexB = vertices.get(vertexIdx);
+
+ // add directly as a triangle instance to avoid computation of the plane again
+ triangles.add(new SimpleTriangle3D(plane, fanBase, vertexA, vertexB));
+
+ vertexA = vertexB;
+ vertexIdx = (vertexIdx + 1) % size;
+ }
+
+ return triangles;
}
- /** Create a new plane subset from the given sequence of points. The points must define a unique plane,
- * meaning that at least 3 unique vertices must be given. If {@code close} is true, the vertices are made
- * into a closed loop by including the start point at the end if needed.
- * @param pts collection of points
- * @param close if true, the point sequence will implicitly include the start point again at the end; otherwise
- * the vertex sequence is taken as-is
- * @param precision precision context used to compare floating point values
- * @return a new plane subset instance
- * @throws IllegalArgumentException if fewer than 3 vertices are given or the vertices do not define a
- * unique plane
- * @see #subsetFromVertexLoop(Collection, DoublePrecisionContext)
- * @see #subsetFromVertices(Collection, boolean, DoublePrecisionContext)
- * @see #fromPoints(Collection, DoublePrecisionContext)
+ /** Find the index of the best vertex to use as the base for a triangle fan split of the convex polygon
+ * defined by the given vertices. The best vertex is the one that forms the largest interior angle in the
+ * polygon since a split at that point will help prevent the creation of very thin triangles.
+ * @param vertices vertices defining the convex polygon; must not be empty
+ * @return the index of the best vertex to use as the base for a triangle fan split of the convex polygon
*/
- public static PlaneConvexSubset subsetFromVertices(final Collection<Vector3D> pts, final boolean close,
- final DoublePrecisionContext precision) {
+ private static int findBestTriangleFanIndex(final List<Vector3D> vertices) {
+ final Iterator<Vector3D> it = vertices.iterator();
+
+ Vector3D curPt = it.next();
+ Vector3D nextPt;
- final Plane plane = Planes.fromPoints(pts, precision);
+ Vector3D lastVec = vertices.get(vertices.size() - 1).directionTo(curPt);
+ Vector3D incomingVec = lastVec;
+ Vector3D outgoingVec;
- final List<Vector2D> subspacePts = plane.toSubspace(pts);
- final ConvexArea area = ConvexArea.fromVertices(subspacePts, close, precision);
+ int bestIdx = 0;
+ double bestDot = -1.0;
- return new PlaneConvexSubset(plane, area);
+ int idx = 0;
+ double dot;
+ while (it.hasNext()) {
+ nextPt = it.next();
+ outgoingVec = curPt.directionTo(nextPt);
+
+ dot = incomingVec.dot(outgoingVec);
+ if (dot > bestDot) {
+ bestIdx = idx;
+ bestDot = dot;
+ }
+
+ curPt = nextPt;
+ incomingVec = outgoingVec;
+
+ ++idx;
+ }
+
+ // handle the last vertex on its own
+ dot = incomingVec.dot(lastVec);
+ if (dot > bestDot) {
+ bestIdx = idx;
+ }
+
+ return bestIdx;
}
- /** Validate that the actual plane is equivalent to the expected plane, throwing an exception if not.
- * @param expected the expected plane
- * @param actual the actual plane
- * @throws IllegalArgumentException if the actual plane is not equivalent to the expected plane
+ /** Internal helper class used to construct planes from sequences of points. Instances can be also be
+ * configured to collect lists of unique points found during plane construction and validate that the
+ * defined region is convex.
*/
- static void validatePlanesEquivalent(final Plane expected, final Plane actual) {
- if (!expected.eq(actual, expected.getPrecision())) {
- throw new IllegalArgumentException("Arguments do not represent the same plane. Expected " +
- expected + " but was " + actual + ".");
+ private static final class PlaneBuilder {
+
+ /** The point sequence to build a plane for. */
+ private final Collection<Vector3D> pts;
+
+ /** Precision context used for floating point comparisons. */
+ private final DoublePrecisionContext precision;
+
+ /** The start point from the point sequence. */
+ private Vector3D startPt;
+
+ /** The previous point from the point sequence. */
+ private Vector3D prevPt;
+
+ /** The previous vector from the point sequence, preceeding from the {@code startPt} to {@code prevPt}. */
+ private Vector3D prevVector;
+
+ /** The computed {@code normal} vector for the plane. */
+ private Vector3D.Unit normal;
+
+ /** The x component of the sum of all cross products from adjacent vectors in the point sequence. */
+ private double crossSumX;
+
+ /** The y component of the sum of all cross products from adjacent vectors in the point sequence. */
+ private double crossSumY;
+
+ /** The z component of the sum of all cross products from adjacent vectors in the point sequence. */
+ private double crossSumZ;
+
+ /** If true, an exception will be thrown if the point sequence is discovered to be non-convex. */
+ private boolean requireConvex = false;
+
+ /** List that unique vertices discovered in the input sequence will be added to. */
+ private List<Vector3D> uniqueVertexOutput;
+
+ /** Construct a new build instance for the given point sequence and precision context.
+ * @param pts point sequence
+ * @param precision precision context used to perform floating point comparisons
+ */
+ PlaneBuilder(final Collection<Vector3D> pts, final DoublePrecisionContext precision) {
+ this.pts = pts;
+ this.precision = precision;
+ }
+
+ /** Build a plane from the configured point sequence.
+ * @return a plane built from the configured point sequence
+ * @throw IllegalArgumentException if the points do not define a plane
+ */
+ Plane build() {
+ if (pts.size() < 3) {
+ throw nonPlanar();
+ }
+
+ pts.forEach(this::processPoint);
+
+ return createPlane();
+ }
+
+ /** Build a plane from the configured point sequence, validating that the points form a convex region
+ * and adding all discovered unique points to the given list.
+ * @param vertexOutput list that unique points discovered in the point sequence will be added to
+ * @return a plane created from the configured point sequence
+ * @throw IllegalArgumentException if the points do not define a plane or the {@code requireConvex}
+ * flag is true and the points do not define a convex area
+ */
+ Plane buildForConvexPolygon(final List<Vector3D> vertexOutput) {
+ this.requireConvex = true;
+ this.uniqueVertexOutput = vertexOutput;
+
+ Plane plane = build();
+
+ return plane;
+ }
+
+ /** Process a point from the point sequence.
+ * @param pt
+ * @throw IllegalArgumentException if the points do not define a plane or the {@code requireConvex}
+ * flag is true and the points do not define a convex area
+ */
+ private void processPoint(final Vector3D pt) {
+ if (prevPt == null) {
+ startPt = pt;
+ prevPt = pt;
+
+ if (uniqueVertexOutput != null) {
+ uniqueVertexOutput.add(pt);
+ }
+
+ } else if (!prevPt.eq(pt, precision)) { // skip duplicate points
+ final Vector3D vec = startPt.vectorTo(pt);
+
+ if (prevVector != null) {
+ processCrossProduct(prevVector.cross(vec));
+ }
+
+ if (uniqueVertexOutput != null) {
+ uniqueVertexOutput.add(pt);
+ }
+
+ prevPt = pt;
+ prevVector = vec;
+ }
+ }
+
+ /** Process the computed cross product of two vectors from the input point sequence. The vectors
+ * start at the first point in the sequence and point to adjacent points later in the sequence.
+ * @param cross the cross product of two vectors from the input point sequence
+ * @throw IllegalArgumentException if the points do not define a plane or the {@code requireConvex}
+ * flag is true and the points do not define a convex area
+ */
+ private void processCrossProduct(final Vector3D cross) {
+ crossSumX += cross.getX();
+ crossSumY += cross.getY();
+ crossSumZ += cross.getZ();
+
+ final double crossNorm = cross.norm();
+
+ if (!precision.eqZero(crossNorm)) {
+ // the cross product has non-zero magnitude
+ if (normal == null) {
+ // save the first non-zero cross product as our normal
+ normal = cross.normalize();
+ } else {
+ final double crossDot = normal.dot(cross) / crossNorm;
+
+ // check non-planar before non-convex since the former is a more general type
+ // of issue
+ if (!precision.eq(1.0, Math.abs(crossDot))) {
+ throw nonPlanar();
+ } else if (requireConvex && crossDot < 0) {
+ throw nonConvex();
+ }
+ }
+ }
+ }
+
+ /** Construct the plane instance using the value gathered during point processing.
+ * @return the created plane instance
+ * @throw IllegalArgumentException if the point do not define a plane
+ */
+ private Plane createPlane() {
+ if (normal == null) {
+ throw nonPlanar();
+ }
+
+ // flip the normal if needed to match the overall orientation of the points
+ if (normal.dot(Vector3D.of(crossSumX, crossSumY, crossSumZ)) < 0) {
+ normal = normal.negate();
+ }
+
+ // construct the plane
+ final double originOffset = -startPt.dot(normal);
+
+ return new Plane(normal, originOffset, precision);
+ }
+
+ /** Return an exception with a message stating that the points given to this builder do not
+ * define a plane.
+ * @return an exception stating that the points do not define a plane
+ */
+ private IllegalArgumentException nonPlanar() {
+ return new IllegalArgumentException("Points do not define a plane: " + pts);
+ }
+
+ /** Return an exception with a message stating that the points given to this builder do not
+ * define a convex region.
+ * @return an exception stating that the points do not define a plane
+ */
+ private IllegalArgumentException nonConvex() {
+ return new IllegalArgumentException("Points do not define a convex region: " + pts);
}
}
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
index 0739d67..66126fe 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
@@ -22,7 +22,6 @@ import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
import org.apache.commons.geometry.core.partitioning.Split;
import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
@@ -32,7 +31,6 @@ import org.apache.commons.geometry.core.partitioning.bsp.RegionCutBoundary;
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.twod.Vector2D;
/** Binary space partitioning (BSP) tree representing a region in three dimensional
* Euclidean space.
@@ -205,7 +203,7 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
* @return a new tree instance constructed from the given boundaries
* @see #from(Iterable, boolean)
*/
- public static RegionBSPTree3D from(final Iterable<PlaneConvexSubset> boundaries) {
+ public static RegionBSPTree3D from(final Iterable<? extends PlaneConvexSubset> boundaries) {
return from(boundaries, false);
}
@@ -216,7 +214,7 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
* @param full if true, the initial tree will contain the entire space
* @return a new tree instance constructed from the given boundaries
*/
- public static RegionBSPTree3D from(final Iterable<PlaneConvexSubset> boundaries, final boolean full) {
+ public static RegionBSPTree3D from(final Iterable<? extends PlaneConvexSubset> boundaries, final boolean full) {
final RegionBSPTree3D tree = new RegionBSPTree3D(full);
tree.insert(boundaries);
@@ -354,29 +352,26 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
*/
private void addBoundaryContribution(final HyperplaneSubset<Vector3D> boundary, boolean reverse) {
final PlaneSubset boundarySubset = (PlaneSubset) boundary;
- final HyperplaneBoundedRegion<Vector2D> base = boundarySubset.getSubspaceRegion();
- final double area = base.getSize();
- final Vector2D baseBarycenter = base.getBarycenter();
+ final Plane boundaryPlane = boundarySubset.getPlane();
+ final double boundaryArea = boundarySubset.getSize();
+ final Vector3D boundaryBarycenter = boundarySubset.getBarycenter();
- if (Double.isInfinite(area)) {
+ if (Double.isInfinite(boundaryArea)) {
volumeSum = Double.POSITIVE_INFINITY;
- } else if (baseBarycenter != null) {
- final Plane plane = boundarySubset.getPlane();
- final Vector3D facetBarycenter = plane.toSpace(base.getBarycenter());
-
+ } else if (boundaryBarycenter != null) {
// the volume here is actually 3x the actual pyramid volume; we'll apply
// the final scaling all at once at the end
- double scaledVolume = area * facetBarycenter.dot(plane.getNormal());
+ double scaledVolume = boundaryArea * boundaryBarycenter.dot(boundaryPlane.getNormal());
if (reverse) {
scaledVolume = -scaledVolume;
}
volumeSum += scaledVolume;
- sumX += scaledVolume * facetBarycenter.getX();
- sumY += scaledVolume * facetBarycenter.getY();
- sumZ += scaledVolume * facetBarycenter.getZ();
+ sumX += scaledVolume * boundaryBarycenter.getX();
+ sumY += scaledVolume * boundaryBarycenter.getY();
+ sumZ += scaledVolume * boundaryBarycenter.getZ();
}
}
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SimpleTriangle3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SimpleTriangle3D.java
new file mode 100644
index 0000000..e5e8b39
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SimpleTriangle3D.java
@@ -0,0 +1,110 @@
+/*
+ * 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.euclidean.threed;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Transform;
+
+/** Simple implementation of {@link Triangle3D}.
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+final class SimpleTriangle3D extends AbstractConvexPolygon3D implements Triangle3D {
+
+ /** First point in the triangle. */
+ private final Vector3D p1;
+
+ /** Second point in the triangle. */
+ private final Vector3D p2;
+
+ /** Third point in the triangle. */
+ private final Vector3D p3;
+
+ /** Construct a new instance from a plane and 3 points. Callers are responsible for ensuring that
+ * the points lie on the plane and define a triangle. No validation is performed.
+ * @param plane the plane containing the triangle
+ * @param p1 first point in the triangle
+ * @param p2 second point in the triangle
+ * @param p3 third point in the triangle
+ */
+ SimpleTriangle3D(final Plane plane, final Vector3D p1, final Vector3D p2, final Vector3D p3) {
+ super(plane);
+
+ this.p1 = p1;
+ this.p2 = p2;
+ this.p3 = p3;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D getPoint1() {
+ return p1;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D getPoint2() {
+ return p2;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D getPoint3() {
+ return p3;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<Vector3D> getVertices() {
+ return Arrays.asList(p1, p2, p3);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getSize() {
+ final Vector3D v1 = p1.vectorTo(p2);
+ final Vector3D v2 = p1.vectorTo(p3);
+ return 0.5 * v1.cross(v2).norm();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D getBarycenter() {
+ return Vector3D.centroid(p1, p2, p3);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SimpleTriangle3D reverse() {
+ final Plane rPlane = getPlane().reverse();
+
+ return new SimpleTriangle3D(rPlane, p1, p3, p2); // reverse point ordering
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public SimpleTriangle3D transform(final Transform<Vector3D> transform) {
+ final Plane tPlane = getPlane().transform(transform);
+ final Vector3D t1 = transform.apply(p1);
+ final Vector3D t2 = transform.apply(p2);
+ final Vector3D t3 = transform.apply(p3);
+
+ return new SimpleTriangle3D(tPlane, t1, t2, t3);
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Triangle3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Triangle3D.java
new file mode 100644
index 0000000..bf49a72
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Triangle3D.java
@@ -0,0 +1,59 @@
+/*
+ * 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.euclidean.threed;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Transform;
+
+/** Interface representing a triangle in Euclidean 3D space.
+ */
+public interface Triangle3D extends ConvexPolygon3D {
+
+ /** The first point in the triangle.
+ * @return the first point in the triangle
+ */
+ Vector3D getPoint1();
+
+ /** The second point in the triangle.
+ * @return the second point in the triangle
+ */
+ Vector3D getPoint2();
+
+ /** The third point in the triangle.
+ * @return the third point in the triangle
+ */
+ Vector3D getPoint3();
+
+ /** {@inheritDoc} */
+ @Override
+ Triangle3D reverse();
+
+ /** {@inheritDoc} */
+ @Override
+ Triangle3D transform(Transform<Vector3D> transform);
+
+ /** {@inheritDoc}
+ *
+ * <p>This method simply returns a singleton list containing this object.</p>
+ */
+ @Override
+ default List<Triangle3D> toTriangles() {
+ return Collections.singletonList(this);
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java
index 2982ea8..388eb98 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java
@@ -16,7 +16,9 @@
*/
package org.apache.commons.geometry.euclidean.threed;
+import java.util.Arrays;
import java.util.Comparator;
+import java.util.Iterator;
import java.util.function.UnaryOperator;
import org.apache.commons.geometry.core.internal.DoubleFunction3N;
@@ -71,20 +73,20 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
return cmp;
};
- /** Abscissa (first coordinate value). */
+ /** X coordinate value (abscissa). */
private final double x;
- /** Ordinate (second coordinate value). */
+ /** Y coordinate value (ordinate). */
private final double y;
- /** Height (third coordinate value). */
+ /** Z coordinate value (height). */
private final double z;
/** Simple constructor.
* Build a vector from its coordinates
- * @param x abscissa
- * @param y ordinate
- * @param z height
+ * @param x x coordinate value
+ * @param y y coordinate value
+ * @param z z coordinate value
*/
private Vector3D(double x, double y, double z) {
this.x = x;
@@ -92,22 +94,22 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
this.z = z;
}
- /** Returns the abscissa (first coordinate) value of the instance.
- * @return the abscissa
+ /** Return the x coordinate value (abscissa) of the instance.
+ * @return the x coordinate value
*/
public double getX() {
return x;
}
- /** Returns the ordinate (second coordinate) value of the instance.
- * @return the ordinate
+ /** Return the y coordinate value (ordinate) of the instance.
+ * @return the y coordinate value
*/
public double getY() {
return y;
}
- /** Returns the height (third coordinate) value of the instance.
- * @return the height
+ /** Returns the z coordinate value (height) of the instance.
+ * @return the z coordinate value
*/
public double getZ() {
return z;
@@ -481,9 +483,9 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
}
/** Returns a vector with the given coordinate values.
- * @param x abscissa (first coordinate value)
- * @param y abscissa (second coordinate value)
- * @param z height (third coordinate value)
+ * @param x x coordinate value
+ * @param y y coordinate value
+ * @param z z coordinate value
* @return vector instance
*/
public static Vector3D of(final double x, final double y, final double z) {
@@ -512,6 +514,149 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
return SimpleTupleFormat.getDefault().parse(str, Vector3D::new);
}
+ /** Return a vector containing the maximum component values from all input vectors.
+ * @param first first vector
+ * @param more additional vectors
+ * @return a vector containing the maximum component values from all input vectors
+ */
+ public static Vector3D max(final Vector3D first, final Vector3D... more) {
+ return computeMax(first, Arrays.asList(more).iterator());
+ }
+
+ /** Return a vector containing the maximum component values from all input vectors.
+ * @param vecs input vectors
+ * @return a vector containing the maximum component values from all input vectors
+ * @throws IllegalArgumentException if the argument does not contain any vectors
+ */
+ public static Vector3D max(final Iterable<Vector3D> vecs) {
+ final Iterator<Vector3D> it = vecs.iterator();
+ if (!it.hasNext()) {
+ throw new IllegalArgumentException("Cannot compute vector max: no vectors given");
+ }
+
+ return computeMax(it.next(), it);
+ }
+
+ /** Internal method for computing a max vector.
+ * @param first first vector
+ * @param more iterator with additional vectors
+ * @return vector containing the maximum component values of all input vectors
+ */
+ private static Vector3D computeMax(final Vector3D first, final Iterator<Vector3D> more) {
+ double x = first.getX();
+ double y = first.getY();
+ double z = first.getZ();
+
+ Vector3D vec;
+ while (more.hasNext()) {
+ vec = more.next();
+
+ x = Math.max(x, vec.getX());
+ y = Math.max(y, vec.getY());
+ z = Math.max(z, vec.getZ());
+ }
+
+ return Vector3D.of(x, y, z);
+ }
+
+ /** Return a vector containing the minimum component values from all input vectors.
+ * @param first first vector
+ * @param more additional vectors
+ * @return a vector containing the minimum component values from all input vectors
+ */
+ public static Vector3D min(final Vector3D first, final Vector3D... more) {
+ return computeMin(first, Arrays.asList(more).iterator());
+ }
+
+ /** Return a vector containing the minimum component values from all input vectors.
+ * @param vecs input vectors
+ * @return a vector containing the minimum component values from all input vectors
+ * @throws IllegalArgumentException if the argument does not contain any vectors
+ */
+ public static Vector3D min(final Iterable<Vector3D> vecs) {
+ final Iterator<Vector3D> it = vecs.iterator();
+ if (!it.hasNext()) {
+ throw new IllegalArgumentException("Cannot compute vector min: no vectors given");
+ }
+
+ return computeMin(it.next(), it);
+ }
+
+ /** Internal method for computing a min vector.
+ * @param first first vector
+ * @param more iterator with additional vectors
+ * @return vector containing the minimum component values of all input vectors
+ */
+ private static Vector3D computeMin(final Vector3D first, final Iterator<Vector3D> more) {
+ double x = first.getX();
+ double y = first.getY();
+ double z = first.getZ();
+
+ Vector3D vec;
+ while (more.hasNext()) {
+ vec = more.next();
+
+ x = Math.min(x, vec.getX());
+ y = Math.min(y, vec.getY());
+ z = Math.min(z, vec.getZ());
+ }
+
+ return Vector3D.of(x, y, z);
+ }
+
+ /** Compute the centroid of the given points. The centroid is the arithmetic mean position of a set
+ * of points.
+ * @param first first point
+ * @param more additional points
+ * @return the centroid of the given points
+ */
+ public static Vector3D centroid(final Vector3D first, final Vector3D... more) {
+ return computeCentroid(first, Arrays.asList(more).iterator());
+ }
+
+ /** Compute the centroid of the given points. The centroid is the arithmetic mean position of a set
+ * of points.
+ * @param pts the points to compute the centroid of
+ * @return the centroid of the given points
+ * @throws IllegalArgumentException if the argument contains no points
+ */
+ public static Vector3D centroid(final Iterable<Vector3D> pts) {
+ final Iterator<Vector3D> it = pts.iterator();
+ if (!it.hasNext()) {
+ throw new IllegalArgumentException("Cannot compute centroid: no points given");
+ }
+
+ return computeCentroid(it.next(), it);
+ }
+
+ /** Internal method for computing the centroid of a set of points.
+ * @param first first point
+ * @param more iterator with additional points
+ * @return the centroid of the point set
+ */
+ private static Vector3D computeCentroid(final Vector3D first, final Iterator<Vector3D> more) {
+ double x = first.getX();
+ double y = first.getY();
+ double z = first.getZ();
+
+ int count = 1;
+
+ Vector3D pt;
+ while (more.hasNext()) {
+ pt = more.next();
+
+ x += pt.getX();
+ y += pt.getY();
+ z += pt.getZ();
+
+ ++count;
+ }
+
+ double invCount = 1.0 / count;
+
+ return new Vector3D(invCount * x, invCount * y, invCount * z);
+ }
+
/** Returns a vector consisting of the linear combination of the inputs.
* <p>
* A linear combination is the sum of all of the inputs multiplied by their
@@ -615,9 +760,9 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
/** Simple constructor. Callers are responsible for ensuring that the given
* values represent a normalized vector.
- * @param x abscissa (first coordinate value)
- * @param y ordinate (second coordinate value)
- * @param z height (third coordinate value)
+ * @param x x coordinate value
+ * @param y x coordinate value
+ * @param z x coordinate value
*/
private Unit(final double x, final double y, final double z) {
super(x, y, z);
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/VertexListConvexPolygon3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/VertexListConvexPolygon3D.java
new file mode 100644
index 0000000..218561f
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/VertexListConvexPolygon3D.java
@@ -0,0 +1,84 @@
+/*
+ * 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.euclidean.threed;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.geometry.core.Transform;
+
+/** Internal {@link ConvexPolygon3D} implementation class that uses a list of vertices
+ * to represent the plane subset.
+ */
+final class VertexListConvexPolygon3D extends AbstractConvexPolygon3D implements ConvexPolygon3D {
+
+ /** Vertex loop defining the convex polygon. */
+ private final List<Vector3D> vertices;
+
+ /** Construct a new instance with the given plane and list of vertices. Callers are responsible
+ * for ensuring that the given vertices form a convex subset lying in {@code plane}. The list of
+ * vertices should not contain the duplicated first endpoint. No validation is performed.
+ * @param plane plane containing convex polygon
+ * @param vertices vertices defining the convex polygon
+ * @throw IllegalArgumentException if fewer than 3 vertices are given
+ */
+ VertexListConvexPolygon3D(final Plane plane, final List<Vector3D> vertices) {
+ super(plane);
+
+ // sanity check
+ if (vertices.size() < 3) {
+ throw new IllegalArgumentException("Convex polygon requires at least 3 points; found " + vertices.size());
+ }
+
+ this.vertices = Collections.unmodifiableList(vertices);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<Vector3D> getVertices() {
+ return vertices;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List<Triangle3D> toTriangles() {
+ return Planes.convexPolygonToTriangleFan(getPlane(), vertices);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public VertexListConvexPolygon3D transform(final Transform<Vector3D> transform) {
+ final Plane tPlane = getPlane().transform(transform);
+ final List<Vector3D> tVertices = vertices.stream()
+ .map(transform)
+ .collect(Collectors.toList());
+
+ return new VertexListConvexPolygon3D(tPlane, tVertices);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public VertexListConvexPolygon3D reverse() {
+ final Plane rPlane = getPlane().reverse();
+ final List<Vector3D> rVertices = new ArrayList<>(vertices);
+ Collections.reverse(rVertices);
+
+ return new VertexListConvexPolygon3D(rPlane, rVertices);
+ }
+}
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 8d5188c..71e2fa1 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
@@ -22,6 +22,8 @@ import java.util.List;
import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.euclidean.oned.Interval;
import org.apache.commons.geometry.euclidean.oned.RegionBSPTree1D;
+import org.apache.commons.geometry.euclidean.oned.Vector1D;
+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.SubspaceTransform;
@@ -63,6 +65,45 @@ public final class EmbeddedTreeLineSubset3D extends LineSubset3D {
this.region = region;
}
+ /** {@inheritDoc} */
+ @Override
+ public double getSize() {
+ return region.getSize();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public RegionBSPTree1D getSubspaceRegion() {
+ return region;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector3D getBarycenter() {
+ final Vector1D subcenter = region.getBarycenter();
+ return subcenter != null ?
+ getLine().toSpace(subcenter) :
+ null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Bounds3D getBounds() {
+ final double min = region.getMin();
+ final double max = region.getMax();
+
+ if (Double.isFinite(min) && Double.isFinite(max)) {
+ final Line3D line = getLine();
+
+ return Bounds3D.builder()
+ .add(line.toSpace(min))
+ .add(line.toSpace(max))
+ .build();
+ }
+
+ return null;
+ }
+
/** Transform this instance.
* @param transform the transform to apply
* @return a new, transformed instance
@@ -97,12 +138,6 @@ public final class EmbeddedTreeLineSubset3D extends LineSubset3D {
/** {@inheritDoc} */
@Override
- public RegionBSPTree1D getSubspaceRegion() {
- return region;
- }
-
- /** {@inheritDoc} */
- @Override
public String toString() {
final Line3D line = getLine();
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 373e74b..0c75a9a 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
@@ -19,6 +19,7 @@ package org.apache.commons.geometry.euclidean.threed.line;
import java.text.MessageFormat;
import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.euclidean.threed.Bounds3D;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
/** Class representing the span of a line in 3D Euclidean space. This is the set of all points
@@ -98,6 +99,24 @@ final class LineSpanningSubset3D extends LineConvexSubset3D {
return Double.POSITIVE_INFINITY;
}
+ /** {@inheritDoc}
+ *
+ * <p>This method always returns {@code null}.</p>
+ */
+ @Override
+ public Vector3D getBarycenter() {
+ return null; // infinite; no center
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method always returns {@code null}.</p>
+ */
+ @Override
+ public Bounds3D getBounds() {
+ return null; // infinite; no bounds
+ }
+
/** {@inheritDoc} */
@Override
public LineSpanningSubset3D transform(final Transform<Vector3D> transform) {
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 0a27e98..0c3c110 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,14 +17,16 @@
package org.apache.commons.geometry.euclidean.threed.line;
import org.apache.commons.geometry.core.RegionEmbedding;
+import org.apache.commons.geometry.core.Sized;
import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
import org.apache.commons.geometry.euclidean.oned.Vector1D;
+import org.apache.commons.geometry.euclidean.threed.Bounds3D;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
/** Class representing a subset of a line in 3D Euclidean space. For example, line segments,
* rays, and disjoint combinations of the two are line subsets. Line subsets may be finite or infinite.
*/
-public abstract class LineSubset3D implements RegionEmbedding<Vector3D, Vector1D> {
+public abstract class LineSubset3D implements RegionEmbedding<Vector3D, Vector1D>, Sized {
/** The line containing this instance. */
private final Line3D line;
@@ -54,6 +56,20 @@ public abstract class LineSubset3D implements RegionEmbedding<Vector3D, Vector1D
return line.toSubspace(pt);
}
+ /** Get the center of the line subset, or null if the subset is empty of
+ * infinite.
+ * @return the center of the line subset, or null if the subset is empty of
+ * infinite
+ */
+ public abstract Vector3D getBarycenter();
+
+ /** Get the 3D bounding box of the line subset or null if the subset is
+ * empty or infinite.
+ * @return the 3D bounding box the line subset or null if the subset is
+ * empty or infinite
+ */
+ public abstract Bounds3D getBounds();
+
/** Get the subspace region for the instance.
* @return the subspace region for the instance
*/
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 4975d50..1448efd 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
@@ -17,6 +17,7 @@
package org.apache.commons.geometry.euclidean.threed.line;
import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.euclidean.threed.Bounds3D;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
/** Class representing a ray in 3D Euclidean space. A ray is a portion of a line consisting of
@@ -107,6 +108,24 @@ public final class Ray3D extends LineConvexSubset3D {
return Double.POSITIVE_INFINITY;
}
+ /** {@inheritDoc}
+ *
+ * <p>This method always returns {@code null}.</p>
+ */
+ @Override
+ public Vector3D getBarycenter() {
+ return null; // infinite; no center
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method always returns {@code null}.</p>
+ */
+ @Override
+ public Bounds3D getBounds() {
+ return null; // infinite; no bounds
+ }
+
/** Get the direction of the ray. This is a convenience method for {@code ray.getLine().getDirection()}.
* @return the direction of the ray
*/
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 480efec..3fb8aa2 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
@@ -17,6 +17,7 @@
package org.apache.commons.geometry.euclidean.threed.line;
import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.euclidean.threed.Bounds3D;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
/** Class representing a portion of a line in 3D Euclidean space that starts at infinity and
@@ -108,6 +109,24 @@ public final class ReverseRay3D extends LineConvexSubset3D {
return end;
}
+ /** {@inheritDoc}
+ *
+ * <p>This method always returns {@code null}.</p>
+ */
+ @Override
+ public Vector3D getBarycenter() {
+ return null; // infinite; no center
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method always returns {@code null}.</p>
+ */
+ @Override
+ public Bounds3D getBounds() {
+ return null; // infinite; no bounds
+ }
+
/** {@inheritDoc} */
@Override
public ReverseRay3D transform(final Transform<Vector3D> transform) {
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 c351c43..62f0a4b 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
@@ -18,6 +18,7 @@ package org.apache.commons.geometry.euclidean.threed.line;
import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.Bounds3D;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
/** Class representing a line segment in 3D Euclidean space. A line segment is a portion of
@@ -108,6 +109,21 @@ public final class Segment3D extends LineConvexSubset3D {
/** {@inheritDoc} */
@Override
+ public Vector3D getBarycenter() {
+ return getLine().toSpace((0.5 * (end - start)) + start);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Bounds3D getBounds() {
+ return Bounds3D.builder()
+ .add(getStartPoint())
+ .add(getEndPoint())
+ .build();
+ }
+
+ /** {@inheritDoc} */
+ @Override
public Segment3D transform(final Transform<Vector3D> transform) {
final Vector3D t1 = transform.apply(getStartPoint());
final Vector3D t2 = transform.apply(getEndPoint());
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/shape/Parallelepiped.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/shape/Parallelepiped.java
index fc99ec2..a2b5888 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/shape/Parallelepiped.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/shape/Parallelepiped.java
@@ -200,7 +200,7 @@ public final class Parallelepiped extends ConvexVolume {
Arrays.asList(pd, pc, pb, pa) :
Arrays.asList(pa, pb, pc, pd);
- return Planes.subsetFromVertexLoop(loop, precision);
+ return Planes.convexPolygonFromVertices(loop, precision);
}
/** Ensure that the given points defining one side of a parallelepiped face are separated by a non-zero
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java
index 45cfe7e..ffca9a1 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java
@@ -50,6 +50,15 @@ public interface BoundarySource2D extends BoundarySource<LineConvexSubset>, Line
return new BoundarySourceLinecaster2D(this).linecastFirst(subset);
}
+ /** Get a {@link Bounds2D} object defining the axis-aligned box containing all vertices
+ * in the boundaries for this instance. Null is returned if any boundaries are infinite
+ * or no vertices were found.
+ * @return the bounding box for this instance or null if no valid bounds could be determined
+ */
+ default Bounds2D getBounds() {
+ return new BoundarySourceBoundsBuilder2D().getBounds(this);
+ }
+
/** Return a {@link BoundarySource2D} instance containing the given boundaries.
* @param boundaries line subsets to include in the boundary source
* @return a boundary source containing the given boundaries
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceBoundsBuilder2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceBoundsBuilder2D.java
new file mode 100644
index 0000000..44f2e6d
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceBoundsBuilder2D.java
@@ -0,0 +1,58 @@
+/*
+ * 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.euclidean.twod;
+
+import java.util.Iterator;
+import java.util.stream.Stream;
+
+/** Class used to construct {@link Bounds2D} instances representing the min and
+ * max points present in a {@link BoundarySource2D}. The implementation examines
+ * the vertices of each boundary in turn. Null is returned if any boundaries are
+ * infinite or no vertices are present.
+ */
+class BoundarySourceBoundsBuilder2D {
+
+ /** Get a {@link Bounds3D} instance containing all vertices in the given boundary source.
+ * Null is returned if any encountered boundaries were not finite or no vertices were found.
+ * @param src boundary source to compute the bounds of
+ * @return the bounds of the argument or null if no valid bounds could be determined
+ */
+ public Bounds2D getBounds(final BoundarySource2D src) {
+
+ final Bounds2D.Builder builder = Bounds2D.builder();
+
+ try (Stream<LineConvexSubset> stream = src.boundaryStream()) {
+ final Iterator<LineConvexSubset> it = stream.iterator();
+
+ LineConvexSubset boundary;
+ while (it.hasNext()) {
+ boundary = it.next();
+
+ if (boundary.isInfinite()) {
+ return null; // break out early
+ }
+
+ builder.add(boundary.getStartPoint());
+ builder.add(boundary.getEndPoint());
+ }
+ }
+
+ return builder.containsBounds() ?
+ builder.build() :
+ null;
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecaster2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecaster2D.java
index 69a265c..92b25d2 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecaster2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecaster2D.java
@@ -42,20 +42,22 @@ final class BoundarySourceLinecaster2D implements Linecastable2D {
/** {@inheritDoc} */
@Override
public List<LinecastPoint2D> linecast(final LineConvexSubset subset) {
- final List<LinecastPoint2D> results = getIntersectionStream(subset)
- .collect(Collectors.toCollection(ArrayList::new));
+ try (Stream<LinecastPoint2D> stream = getIntersectionStream(subset)) {
- LinecastPoint2D.sortAndFilter(results);
+ final List<LinecastPoint2D> results = stream.collect(Collectors.toCollection(ArrayList::new));
+ LinecastPoint2D.sortAndFilter(results);
- return results;
+ return results;
+ }
}
/** {@inheritDoc} */
@Override
public LinecastPoint2D linecastFirst(final LineConvexSubset subset) {
- return getIntersectionStream(subset)
- .min(LinecastPoint2D.ABSCISSA_ORDER)
- .orElse(null);
+ try (Stream<LinecastPoint2D> stream = getIntersectionStream(subset)) {
+ return stream.min(LinecastPoint2D.ABSCISSA_ORDER)
+ .orElse(null);
+ }
}
/** Return a stream containing intersections between the boundary source and the
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
new file mode 100644
index 0000000..0613d7f
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Bounds2D.java
@@ -0,0 +1,271 @@
+/*
+ * 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.euclidean.twod;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.AbstractBounds;
+import org.apache.commons.geometry.euclidean.twod.shape.Parallelogram;
+
+/** Class containing minimum and maximum points defining a 2D axis-aligned bounding box. Unless otherwise
+ * noted, floating point comparisons used in this class are strict, meaning that values are considered equal
+ * if and only if they match exactly.
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public final class Bounds2D extends AbstractBounds<Vector2D, Bounds2D> {
+
+ /** Simple constructor. Callers are responsible for ensuring the min is not greater than max.
+ * @param min minimum point
+ * @param max maximum point
+ */
+ private Bounds2D(final Vector2D min, final Vector2D max) {
+ super(min, max);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasSize(final DoublePrecisionContext precision) {
+ final Vector2D diag = getDiagonal();
+
+ return !precision.eqZero(diag.getX()) &&
+ !precision.eqZero(diag.getY());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean contains(final Vector2D pt) {
+ final double x = pt.getX();
+ final double y = pt.getY();
+
+ final Vector2D min = getMin();
+ final Vector2D max = getMax();
+
+ return x >= min.getX() && x <= max.getX() &&
+ y >= min.getY() && y <= max.getY();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean contains(final Vector2D pt, final DoublePrecisionContext precision) {
+ final double x = pt.getX();
+ final double y = pt.getY();
+
+ final Vector2D min = getMin();
+ final Vector2D max = getMax();
+
+ return precision.gte(x, min.getX()) && precision.lte(x, max.getX()) &&
+ precision.gte(y, min.getY()) && precision.lte(y, max.getY());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean intersects(final Bounds2D other) {
+ final Vector2D aMin = getMin();
+ final Vector2D aMax = getMax();
+
+ final Vector2D bMin = other.getMin();
+ final Vector2D bMax = other.getMax();
+
+ return aMin.getX() <= bMax.getX() && aMax.getX() >= bMin.getX() &&
+ aMin.getY() <= bMax.getY() && aMax.getY() >= bMin.getY();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Bounds2D intersection(final Bounds2D other) {
+ if (intersects(other)) {
+ final Vector2D aMin = getMin();
+ final Vector2D aMax = getMax();
+
+ final Vector2D bMin = other.getMin();
+ final Vector2D bMax = other.getMax();
+
+ // get the max of the mins and the mins of the maxes
+ final double minX = Math.max(aMin.getX(), bMin.getX());
+ final double minY = Math.max(aMin.getY(), bMin.getY());
+
+ final double maxX = Math.min(aMax.getX(), bMax.getX());
+ final double maxY = Math.min(aMax.getY(), bMax.getY());
+
+ return new Bounds2D(
+ Vector2D.of(minX, minY),
+ Vector2D.of(maxX, maxY));
+ }
+
+ return null; // no intersection
+ }
+
+ /** {@inheritDoc}
+ *
+ * @throws IllegalArgumentException if any dimension of the bounding box is zero
+ * as evaluated by the given precision context
+ */
+ @Override
+ public Parallelogram toRegion(final DoublePrecisionContext precision) {
+ return Parallelogram.axisAligned(getMin(), getMax(), precision);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(getMin(), getMax());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == this) {
+ return true;
+ } else if (!(obj instanceof Bounds2D)) {
+ return false;
+ }
+
+ final Bounds2D other = (Bounds2D) obj;
+
+ return getMin().equals(other.getMin()) &&
+ getMax().equals(other.getMax());
+ }
+
+ /** Construct a new instance from the given points.
+ * @param first first point
+ * @param more additional points
+ * @return a new instance containing the min and max coordinates values from the input points
+ */
+ public static Bounds2D from(final Vector2D first, final Vector2D... more) {
+ final Builder builder = builder();
+
+ builder.add(first);
+ builder.addAll(Arrays.asList(more));
+
+ return builder.build();
+ }
+
+ /** Construct a new instance from the given points.
+ * @param points input points
+ * @return a new instance containing the min and max coordinates values from the input points
+ */
+ public static Bounds2D from(final Iterable<Vector2D> points) {
+ final Builder builder = builder();
+
+ builder.addAll(points);
+
+ return builder.build();
+ }
+
+ /** Construct a new {@link Builder} instance for creating bounds.
+ * @return a new builder instance for creating bounds
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Class used to construct {@link Bounds2D} instances.
+ */
+ public static final class Builder {
+
+ /** Minimum x coordinate. */
+ private double minX = Double.POSITIVE_INFINITY;
+
+ /** Minimum y coordinate. */
+ private double minY = Double.POSITIVE_INFINITY;
+
+ /** Maximum x coordinate. */
+ private double maxX = Double.NEGATIVE_INFINITY;
+
+ /** Maximum y coordinate. */
+ private double maxY = Double.NEGATIVE_INFINITY;
+
+ /** Private constructor; instantiate through factory method. */
+ private Builder() { }
+
+ /** Add a point to this instance.
+ * @param pt point to add
+ * @return this instance
+ */
+ public Builder add(final Vector2D pt) {
+ final double x = pt.getX();
+ final double y = pt.getY();
+
+ minX = Math.min(x, minX);
+ minY = Math.min(y, minY);
+
+ maxX = Math.max(x, maxX);
+ maxY = Math.max(y, maxY);
+
+ return this;
+ }
+
+ /** Add a collection of points to this instance.
+ * @param pts points to add
+ * @return this instance
+ */
+ public Builder addAll(final Iterable<Vector2D> pts) {
+ for (final Vector2D pt : pts) {
+ add(pt);
+ }
+
+ return this;
+ }
+
+ /** Add the min and max points from the given bounds to this instance.
+ * @param bounds bounds containing the min and max points to add
+ * @return this instance
+ */
+ public Builder add(final Bounds2D bounds) {
+ add(bounds.getMin());
+ add(bounds.getMax());
+
+ return this;
+ }
+
+ /** Return true if this builder contains valid min and max coordinate values.
+ * @return true if this builder contains valid min and max coordinate values
+ */
+ public boolean containsBounds() {
+ return Double.isFinite(minX) &&
+ Double.isFinite(minY) &&
+ Double.isFinite(maxX) &&
+ Double.isFinite(maxY);
+ }
+
+ /** Create a new {@link Bounds2D} instance from the values in this builder.
+ * The builder can continue to be used to create other instances.
+ * @return a new bounds instance
+ * @throws IllegalStateException if no points were given to the builder or any of the computed
+ * min and max coordinate values are NaN or infinite
+ * @see #containsBounds()
+ */
+ public Bounds2D build() {
+ final Vector2D min = Vector2D.of(minX, minY);
+ final Vector2D max = Vector2D.of(maxX, maxY);
+
+ if (!containsBounds()) {
+ if (Double.isInfinite(minX) && minX > 0 &&
+ Double.isInfinite(maxX) && maxX < 0) {
+ throw new IllegalStateException("Cannot construct bounds: no points given");
+ }
+
+ throw new IllegalStateException("Invalid bounds: min= " + min + ", max= " + max);
+ }
+
+ return new Bounds2D(min, max);
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ConvexArea.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ConvexArea.java
index 67d0566..0aecbeb 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ConvexArea.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ConvexArea.java
@@ -16,12 +16,10 @@
*/
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.List;
-import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.geometry.core.Transform;
@@ -39,6 +37,9 @@ import org.apache.commons.geometry.euclidean.twod.path.LinePath;
public class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vector2D, LineConvexSubset>
implements BoundarySource2D {
+ /** Error message used when attempting to construct a convex polygon from a non-convex line path. */
+ private static final String NON_CONVEX_PATH_ERROR = "Cannot construct convex polygon from non-convex path: ";
+
/** Instance representing the full 2D plane. */
private static final ConvexArea FULL = new ConvexArea(Collections.emptyList());
@@ -74,12 +75,18 @@ public class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vector2D,
return InteriorAngleLinePathConnector.connectMinimized(getBoundaries());
}
- /** Get the vertices defining the area. The vertices lie at the intersections of the
- * area bounding lines. Note that it is possible for areas to contain no vertices at
- * all. For example, an area with no boundaries (representing the full space), an area
- * with a single boundary, or an area one with two parallel boundaries will not contain
- * vertices.
- * @return the vertices defining the area
+ /** Get the vertices for the area in a counter-clockwise order. Each vertex in the
+ * returned list is unique. If the boundary of the area is closed, the start vertex is
+ * <em>not</em> repeated at the end of the list.
+ *
+ * <p>It is important to note that, in general, the list of vertices returned by this method
+ * is not sufficient to completely characterize the area. For example, a simple triangle
+ * has 3 vertices, but an infinite area constructed from two parallel lines and two lines that
+ * intersect between them will also have 3 vertices. It is also possible for non-empty areas to
+ * contain no vertices at all. For example, an area with no boundaries (representing the full
+ * space), an area with a single boundary, or an area with two parallel boundaries will not
+ * contain any vertices.</p>
+ * @return the list of vertices for the area in a counter-clockwise order
*/
public List<Vector2D> getVertices() {
final List<LinePath> paths = getBoundaryPaths();
@@ -191,89 +198,74 @@ public class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vector2D,
return FULL;
}
- /** Construct a convex area by creating lines between adjacent vertices. The vertices must be given in a
- * counter-clockwise around order the interior of the shape. If the area is intended to be closed, the
- * beginning point must be repeated at the end of the path.
- * @param vertices vertices to use to construct the area
- * @param precision precision context used to create new line instances
- * @return a convex area constructed using lines between adjacent vertices
- * @see #fromVertexLoop(Collection, DoublePrecisionContext)
- * @see #fromVertices(Collection, boolean, DoublePrecisionContext)
+ /** Construct a convex polygon from the given vertices.
+ * @param vertices vertices to use to construct the polygon
+ * @param precision precision context used for floating point comparisons
+ * @return a convex polygon constructed using the given vertices
+ * @throws IllegalStateException if {@code vertices} contains only a single unique vertex
+ * @throws IllegalArgumentException if the constructed path does not define a closed, convex polygon
+ * @see LinePath#fromVertexLoop(Collection, DoublePrecisionContext)
*/
- public static ConvexArea fromVertices(final Collection<Vector2D> vertices,
+ public static ConvexArea convexPolygonFromVertices(final Collection<Vector2D> vertices,
final DoublePrecisionContext precision) {
- return fromVertices(vertices, false, precision);
+ return convexPolygonFromPath(LinePath.fromVertexLoop(vertices, precision));
}
- /** Construct a convex area by creating lines between adjacent vertices. An implicit line is created between the
- * last vertex given and the first one. The vertices must be given in a counter-clockwise around order the interior
- * of the shape.
- * @param vertices vertices to use to construct the area
- * @param precision precision context used to create new line instances
- * @return a convex area constructed using lines between adjacent vertices
- * @see #fromVertices(Collection, DoublePrecisionContext)
- * @see #fromVertices(Collection, boolean, DoublePrecisionContext)
+ /** Construct a convex polygon from a line path.
+ * @param path path to construct the polygon from
+ * @return a convex polygon constructed from the given line path
+ * @throws IllegalArgumentException if the path does not define a closed, convex polygon
*/
- public static ConvexArea fromVertexLoop(final Collection<Vector2D> vertices,
- final DoublePrecisionContext precision) {
- return fromVertices(vertices, true, precision);
- }
+ public static ConvexArea convexPolygonFromPath(final LinePath path) {
+ // ensure that the path is closed; this also ensures that we do not have any infinite elements
+ if (!path.isClosed()) {
+ throw new IllegalArgumentException("Cannot construct convex polygon from unclosed path: " + path);
+ }
- /** Construct a convex area from lines between adjacent vertices.
- * @param vertices vertices to use to construct the area
- * @param close if true, an additional line will be created between the last and first vertex
- * @param precision precision context used to create new line instances
- * @return a convex area constructed using lines between adjacent vertices
- */
- public static ConvexArea fromVertices(final Collection<Vector2D> vertices, boolean close,
- final DoublePrecisionContext precision) {
- if (vertices.isEmpty()) {
- return full();
+ final List<LineConvexSubset> elements = path.getElements();
+ if (elements.size() < 3) {
+ throw new IllegalArgumentException(
+ "Cannot construct convex polygon from path with less than 3 elements: " + path);
}
- final List<Line> lines = new ArrayList<>();
+ // go through the elements and validate that the produced area is convex and finite;
+ // use the precision context from the first path element
+ LineConvexSubset startElement = elements.get(0);
+ Vector2D startVertex = startElement.getStartPoint();
+ DoublePrecisionContext precision = startElement.getPrecision();
- Vector2D first = null;
- Vector2D prev = null;
- Vector2D cur = null;
+ Vector2D curVector;
+ Vector2D prevVector = null;
- for (final Vector2D vertex : vertices) {
- cur = vertex;
+ double signedArea;
+ double totalSignedArea = 0.0;
- if (first == null) {
- first = cur;
- }
+ LineConvexSubset element;
- if (prev != null && !cur.eq(prev, precision)) {
- lines.add(Lines.fromPoints(prev, cur, precision));
- }
+ // we can skip the last element since the we know that the path is closed, meaning that the
+ // last element's end point is equal to our start point
+ for (int i = 0; i < elements.size() - 1; ++i) {
+ element = elements.get(i);
- prev = cur;
- }
+ curVector = startVertex.vectorTo(element.getEndPoint());
- if (close && cur != null && !cur.eq(first, precision)) {
- lines.add(Lines.fromPoints(cur, first, precision));
- }
+ if (prevVector != null) {
+ signedArea = prevVector.signedArea(curVector);
+ if (precision.lt(signedArea, 0.0)) {
+ throw new IllegalArgumentException(NON_CONVEX_PATH_ERROR + path);
+ }
- if (!vertices.isEmpty() && lines.isEmpty()) {
- throw new IllegalStateException("Unable to create convex area: only a single unique vertex provided");
- }
+ totalSignedArea += signedArea;
+ }
- return fromBounds(lines);
- }
+ prevVector = curVector;
+ }
- /** Construct a convex area from a line subset path. The area represents the intersection of all of the
- * negative half-spaces of the lines in the path. The boundaries of the returned area may therefore not
- * match the line subsets in the path.
- * @param path path to construct the area from
- * @return a convex area constructed from the line subsets in the given path
- */
- public static ConvexArea fromPath(final LinePath path) {
- final List<Line> lines = path.boundaryStream()
- .map(LineConvexSubset::getLine)
- .collect(Collectors.toList());
+ if (precision.lte(totalSignedArea, 0.0)) {
+ throw new IllegalArgumentException(NON_CONVEX_PATH_ERROR + path);
+ }
- return fromBounds(lines);
+ return new ConvexArea(elements);
}
/** Create a convex area formed by the intersection of the negative half-spaces of the
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 ab557dd..8a56114 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
@@ -21,6 +21,7 @@ import java.util.List;
import org.apache.commons.geometry.core.RegionLocation;
import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.internal.HyperplaneSubsets;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
import org.apache.commons.geometry.core.partitioning.Split;
import org.apache.commons.geometry.core.partitioning.SplitLocation;
@@ -29,6 +30,7 @@ import org.apache.commons.geometry.euclidean.oned.Interval;
import org.apache.commons.geometry.euclidean.oned.OrientedPoint;
import org.apache.commons.geometry.euclidean.oned.OrientedPoints;
import org.apache.commons.geometry.euclidean.oned.RegionBSPTree1D;
+import org.apache.commons.geometry.euclidean.oned.Vector1D;
import org.apache.commons.geometry.euclidean.twod.Line.SubspaceTransform;
/** Class representing an arbitrary subset of a line using a {@link RegionBSPTree1D}.
@@ -71,6 +73,58 @@ public final class EmbeddedTreeLineSubset extends LineSubset {
/** {@inheritDoc} */
@Override
+ public boolean isFull() {
+ return region.isFull();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isEmpty() {
+ return region.isEmpty();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public double getSize() {
+ return region.getSize();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector2D getBarycenter() {
+ final Vector1D subspaceBarycenter = region.getBarycenter();
+ if (subspaceBarycenter != null) {
+ return getLine().toSpace(subspaceBarycenter);
+ }
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Bounds2D getBounds() {
+ final double min = region.getMin();
+ final double max = region.getMax();
+
+ if (Double.isFinite(min) && Double.isFinite(max)) {
+ final Line line = getLine();
+
+ return Bounds2D.builder()
+ .add(line.toSpace(min))
+ .add(line.toSpace(max))
+ .build();
+ }
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector2D closest(final Vector2D pt) {
+ return HyperplaneSubsets.closestToEmbeddedRegion(pt, getLine(), region);
+ }
+
+ /** {@inheritDoc} */
+ @Override
public EmbeddedTreeLineSubset transform(final Transform<Vector2D> transform) {
final SubspaceTransform st = getLine().subspaceTransform(transform);
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
index b7a0d93..7593089 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
@@ -63,7 +63,7 @@ public final class Line extends AbstractHyperplane<Vector2D>
static final String TO_STRING_FORMAT = "{0}[origin= {1}, direction= {2}]";
/** The direction of the line as a normalized vector. */
- private final Vector2D direction;
+ private final Vector2D.Unit direction;
/** The distance between the origin and the line. */
private final double originOffset;
@@ -73,7 +73,7 @@ public final class Line extends AbstractHyperplane<Vector2D>
* @param originOffset The signed distance between the line and the origin.
* @param precision Precision context used to compare floating point numbers.
*/
- Line(final Vector2D direction, final double originOffset, final DoublePrecisionContext precision) {
+ Line(final Vector2D.Unit direction, final double originOffset, final DoublePrecisionContext precision) {
super(precision);
this.direction = direction;
@@ -93,7 +93,7 @@ public final class Line extends AbstractHyperplane<Vector2D>
/** Get the direction of the line.
* @return the direction of the line
*/
- public Vector2D getDirection() {
+ public Vector2D.Unit getDirection() {
return direction;
}
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 ab16ae3..8c504f5 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
@@ -73,6 +73,15 @@ final class LineSpanningSubset extends LineConvexSubset {
}
/** {@inheritDoc}
+ *
+ * <p>This method always returns {@code null}.</p>
+ */
+ @Override
+ public Vector2D getBarycenter() {
+ return null;
+ }
+
+ /** {@inheritDoc}
*
* <p>This method always returns {@code null}.</p>
*/
@@ -108,6 +117,15 @@ final class LineSpanningSubset extends LineConvexSubset {
return Double.POSITIVE_INFINITY;
}
+ /** {@inheritDoc}
+ *
+ * <p>This method always returns {@code null}.</p>
+ */
+ @Override
+ public Bounds2D getBounds() {
+ return null; // infinite; no bounds
+ }
+
/** {@inheritDoc} */
@Override
public LineSpanningSubset transform(final Transform<Vector2D> transform) {
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 ba05313..2e8a2c3 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
@@ -19,8 +19,9 @@ package org.apache.commons.geometry.euclidean.twod;
import java.util.List;
import java.util.Objects;
+import org.apache.commons.geometry.core.RegionEmbedding;
import org.apache.commons.geometry.core.RegionLocation;
-import org.apache.commons.geometry.core.partitioning.AbstractRegionEmbeddingHyperplaneSubset;
+import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
import org.apache.commons.geometry.core.partitioning.Split;
@@ -30,7 +31,7 @@ import org.apache.commons.geometry.euclidean.oned.Vector1D;
/** Class representing a subset of points on a line in 2D Euclidean space. For example, line segments
* and rays are line subsets. Line subsets may be finite or infinite.
*/
-public abstract class LineSubset extends AbstractRegionEmbeddingHyperplaneSubset<Vector2D, Vector1D, Line> {
+public abstract class LineSubset implements HyperplaneSubset<Vector2D>, RegionEmbedding<Vector2D, Vector1D> {
/** The line containing this instance. */
private final Line line;
@@ -58,8 +59,31 @@ public abstract class LineSubset extends AbstractRegionEmbeddingHyperplaneSubset
/** {@inheritDoc} */
@Override
+ public Vector1D toSubspace(final Vector2D pt) {
+ return line.toSubspace(pt);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector2D toSpace(final Vector1D pt) {
+ return line.toSpace(pt);
+ }
+
+ /** {@inheritDoc} */
+ @Override
public abstract List<LineConvexSubset> toConvex();
+ /** Get a {@link Bounds2D} object defining an axis-aligned bounding box containing all
+ * vertices for this subset. Null is returned if the subset is infinite or does not
+ * contain any vertices.
+ * @return the bounding box for this instance or null if no valid bounds could be determined
+ */
+ public abstract Bounds2D getBounds();
+
+ /** {@inheritDoc} */
+ @Override
+ public abstract HyperplaneBoundedRegion<Vector1D> getSubspaceRegion();
+
/** {@inheritDoc} */
@Override
public HyperplaneSubset.Builder<Vector2D> builder() {
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Lines.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Lines.java
index 1ac2ebe..f8d5251 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Lines.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Lines.java
@@ -57,7 +57,7 @@ public final class Lines {
throw new IllegalArgumentException("Line direction cannot be zero");
}
- final Vector2D normalizedDir = dir.normalize();
+ final Vector2D.Unit normalizedDir = dir.normalize();
final double originOffset = normalizedDir.signedArea(pt);
return new Line(normalizedDir, originOffset, precision);
@@ -102,7 +102,10 @@ public final class Lines {
* @throws IllegalArgumentException if any coordinate in {@code startPoint} is NaN or infinite
*/
public static Ray rayFromPoint(final Line line, final Vector2D startPoint) {
- return rayFromLocation(line, line.abscissa(startPoint));
+ if (!startPoint.isFinite()) {
+ throw new IllegalArgumentException("Invalid ray start point: " + startPoint);
+ }
+ return new Ray(line, line.project(startPoint));
}
/** Construct a ray starting at the given 1D location on {@code line} and continuing in the
@@ -117,8 +120,7 @@ public final class Lines {
if (!Double.isFinite(startLocation)) {
throw new IllegalArgumentException("Invalid ray start location: " + Double.toString(startLocation));
}
-
- return new Ray(line, startLocation);
+ return new Ray(line, line.toSpace(startLocation));
}
/** Construct a reverse ray from an end point and a line direction.
@@ -145,7 +147,10 @@ public final class Lines {
* @throws IllegalArgumentException if any coordinate in {@code endPoint} is NaN or infinite
*/
public static ReverseRay reverseRayFromPoint(final Line line, final Vector2D endPoint) {
- return reverseRayFromLocation(line, line.abscissa(endPoint));
+ if (!endPoint.isFinite()) {
+ throw new IllegalArgumentException("Invalid reverse ray end point: " + endPoint);
+ }
+ return new ReverseRay(line, line.project(endPoint));
}
/** Construct a reverse ray starting at infinity and continuing in the direction of {@code line}
@@ -161,7 +166,7 @@ public final class Lines {
throw new IllegalArgumentException("Invalid reverse ray end location: " + Double.toString(endLocation));
}
- return new ReverseRay(line, endLocation);
+ return new ReverseRay(line, line.toSpace(endLocation));
}
/** Construct a new line segment from two points. A new line is created for the segment and points in the
@@ -212,7 +217,7 @@ public final class Lines {
final double min = Math.min(a, b);
final double max = Math.max(a, b);
- return new Segment(line, min, max);
+ return new Segment(line, line.toSpace(min), line.toSpace(max));
}
throw new IllegalArgumentException(
@@ -260,13 +265,13 @@ public final class Lines {
if (hasMin) {
if (hasMax) {
// has both
- return new Segment(line, min, max);
+ return new Segment(line, line.toSpace(min), line.toSpace(max));
}
// min only
- return new Ray(line, min);
+ return new Ray(line, line.toSpace(min));
} else if (hasMax) {
// max only
- return new ReverseRay(line, max);
+ return new ReverseRay(line, line.toSpace(max));
} else if (Double.isInfinite(min) && Double.isInfinite(max) && Double.compare(min, max) < 0) {
return new LineSpanningSubset(line);
}
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 c817727..304b8a7 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
@@ -19,6 +19,7 @@ package org.apache.commons.geometry.euclidean.twod;
import org.apache.commons.geometry.core.RegionLocation;
import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
/** Class representing a ray in 2D Euclidean space. A ray is a portion of a line consisting of
* a single start point and extending to infinity along the direction of the line.
@@ -29,26 +30,18 @@ import org.apache.commons.geometry.core.partitioning.Split;
*/
public final class Ray extends LineConvexSubset {
- /** The start abscissa value for the ray. */
- private final double start;
+ /** The start point for the ray. */
+ private final Vector2D startPoint;
- /** Construct a ray from a line and a start point. The start point is projected
- * onto the line. No validation is performed.
+ /** Construct a ray from a line and a start point. Callers are responsible for ensuring that the
+ * given point lies on the line. No validation is performed.
* @param line line for the ray
* @param startPoint start point for the ray
*/
Ray(final Line line, final Vector2D startPoint) {
- this(line, line.abscissa(startPoint));
- }
-
- /** Construct a ray from a line and a 1D start location. No validation is performed.
- * @param line line for the ray
- * @param start 1D start location
- */
- Ray(final Line line, final double start) {
super(line);
- this.start = start;
+ this.startPoint = startPoint;
}
/** {@inheritDoc}
@@ -87,15 +80,24 @@ public final class Ray extends LineConvexSubset {
return Double.POSITIVE_INFINITY;
}
+ /** {@inheritDoc}
+ *
+ * <p>This method always returns {@code null}.</p>
+ */
+ @Override
+ public Vector2D getBarycenter() {
+ return null;
+ }
+
@Override
public Vector2D getStartPoint() {
- return getLine().toSpace(start);
+ return startPoint;
}
/** {@inheritDoc} */
@Override
public double getSubspaceStart() {
- return start;
+ return getLine().abscissa(startPoint);
}
/** {@inheritDoc}
@@ -108,14 +110,23 @@ public final class Ray extends LineConvexSubset {
}
/** {@inheritDoc}
- *
- * <p>This method always returns {@link Double#POSITIVE_INFINITY}.</p>
- */
+ *
+ * <p>This method always returns {@link Double#POSITIVE_INFINITY}.</p>
+ */
@Override
public double getSubspaceEnd() {
return Double.POSITIVE_INFINITY;
}
+ /** {@inheritDoc}
+ *
+ * <p>This method always returns {@code null}.</p>
+ */
+ @Override
+ public Bounds2D getBounds() {
+ return null; // infinite; no bounds
+ }
+
/** Get the direction of the ray. This is a convenience method for {@code ray.getLine().getDirection()}.
* @return the direction of the ray
*/
@@ -135,7 +146,7 @@ public final class Ray extends LineConvexSubset {
/** {@inheritDoc} */
@Override
public ReverseRay reverse() {
- return new ReverseRay(getLine().reverse(), -start);
+ return new ReverseRay(getLine().reverse(), startPoint);
}
/** {@inheritDoc} */
@@ -155,7 +166,7 @@ public final class Ray extends LineConvexSubset {
/** {@inheritDoc} */
@Override
RegionLocation classifyAbscissa(double abscissa) {
- int cmp = getPrecision().compare(abscissa, start);
+ int cmp = getPrecision().compare(abscissa, getSubspaceStart());
if (cmp > 0) {
return RegionLocation.INSIDE;
} else if (cmp == 0) {
@@ -168,27 +179,33 @@ public final class Ray extends LineConvexSubset {
/** {@inheritDoc} */
@Override
double closestAbscissa(double abscissa) {
- return Math.max(start, abscissa);
+ return Math.max(getSubspaceStart(), abscissa);
}
/** {@inheritDoc} */
@Override
Split<LineConvexSubset> splitOnIntersection(final Line splitter, final Vector2D intersection) {
-
final Line line = getLine();
- final double splitAbscissa = line.abscissa(intersection);
+ final DoublePrecisionContext splitterPrecision = splitter.getPrecision();
- LineConvexSubset low = null;
- LineConvexSubset high = null;
+ final int startCmp = splitterPrecision.compare(splitter.offset(startPoint), 0.0);
+ final boolean pointsTowardPlus = splitter.getOffsetDirection().dot(line.getDirection()) >= 0.0;
- int cmp = getPrecision().compare(splitAbscissa, start);
- if (cmp > 0) {
- low = new Segment(line, start, splitAbscissa);
- high = new Ray(line, splitAbscissa);
- } else {
- high = this;
+ if (pointsTowardPlus && startCmp > -1) {
+ // entirely on plus side
+ return new Split<>(null, this);
+ } else if (!pointsTowardPlus && startCmp < 1) {
+ // entirely on minus side
+ return new Split<>(this, null);
}
- return createSplitResult(splitter, low, high);
+ // we're going to be split
+ final Segment splitSeg = new Segment(line, startPoint, intersection);
+ final Ray splitRay = new Ray(line, intersection);
+
+ final LineConvexSubset minus = (startCmp > 0) ? splitRay : splitSeg;
+ final LineConvexSubset plus = (startCmp > 0) ? splitSeg : splitRay;
+
+ return new Split<>(minus, plus);
}
}
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 766b912..6abec6a 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
@@ -19,6 +19,7 @@ package org.apache.commons.geometry.euclidean.twod;
import org.apache.commons.geometry.core.RegionLocation;
import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
/** Class representing a portion of a line in 2D Euclidean space that starts at infinity and
* continues in the direction of the line up to a single end point. This is equivalent to taking a
@@ -30,26 +31,18 @@ import org.apache.commons.geometry.core.partitioning.Split;
*/
public final class ReverseRay extends LineConvexSubset {
- /** The abscissa of the endpoint. */
- private final double end;
+ /** The end point of the reverse ray. */
+ private final Vector2D endPoint;
- /** Construct a new instance from the given line and end point. The end point is projected onto
- * the line. No validation is performed.
+ /** Construct a new instance from the given line and end point. Callers are responsible for ensuring that
+ * the given end point lies on the line. No validation is performed.
* @param line line for the instance
* @param endPoint end point for the instance
*/
ReverseRay(final Line line, final Vector2D endPoint) {
- this(line, line.abscissa(endPoint));
- }
-
- /** Construct a new instance from the given line and 1D end location. No validation is performed.
- * @param line line for the instance
- * @param end end location for the instance
- */
- ReverseRay(final Line line, final double end) {
super(line);
- this.end = end;
+ this.endPoint = endPoint;
}
/** {@inheritDoc}
@@ -62,36 +55,45 @@ public final class ReverseRay extends LineConvexSubset {
}
/** {@inheritDoc}
- *
- * <p>This method always returns {@code true}.</p>
- */
+ *
+ * <p>This method always returns {@code true}.</p>
+ */
@Override
public boolean isInfinite() {
return true;
}
/** {@inheritDoc}
- *
- * <p>This method always returns {@code false}.</p>
- */
+ *
+ * <p>This method always returns {@code false}.</p>
+ */
@Override
public boolean isFinite() {
return false;
}
/** {@inheritDoc}
- *
- * <p>This method always returns {@link Double#POSITIVE_INFINITY}.</p>
- */
+ *
+ * <p>This method always returns {@link Double#POSITIVE_INFINITY}.</p>
+ */
@Override
public double getSize() {
return Double.POSITIVE_INFINITY;
}
/** {@inheritDoc}
- *
- * <p>This method always returns {@code null}.</p>
- */
+ *
+ * <p>This method always returns {@code null}.</p>
+ */
+ @Override
+ public Vector2D getBarycenter() {
+ return null;
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method always returns {@code null}.</p>
+ */
@Override
public Vector2D getStartPoint() {
return null;
@@ -109,13 +111,22 @@ public final class ReverseRay extends LineConvexSubset {
/** {@inheritDoc} */
@Override
public Vector2D getEndPoint() {
- return getLine().toSpace(end);
+ return endPoint;
}
/** {@inheritDoc} */
@Override
public double getSubspaceEnd() {
- return end;
+ return getLine().abscissa(endPoint);
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>This method always returns {@code null}.</p>
+ */
+ @Override
+ public Bounds2D getBounds() {
+ return null; // infinite; no bounds
}
/** {@inheritDoc} */
@@ -130,7 +141,7 @@ public final class ReverseRay extends LineConvexSubset {
/** {@inheritDoc} */
@Override
public Ray reverse() {
- return new Ray(getLine().reverse(), -end);
+ return new Ray(getLine().reverse(), endPoint);
}
/** {@inheritDoc} */
@@ -150,7 +161,7 @@ public final class ReverseRay extends LineConvexSubset {
/** {@inheritDoc} */
@Override
RegionLocation classifyAbscissa(double abscissa) {
- int cmp = getPrecision().compare(abscissa, end);
+ int cmp = getPrecision().compare(abscissa, getSubspaceEnd());
if (cmp < 0) {
return RegionLocation.INSIDE;
} else if (cmp == 0) {
@@ -163,27 +174,33 @@ public final class ReverseRay extends LineConvexSubset {
/** {@inheritDoc} */
@Override
double closestAbscissa(double abscissa) {
- return Math.min(end, abscissa);
+ return Math.min(getSubspaceEnd(), abscissa);
}
/** {@inheritDoc} */
@Override
protected Split<LineConvexSubset> splitOnIntersection(final Line splitter, final Vector2D intersection) {
-
final Line line = getLine();
- final double splitAbscissa = line.abscissa(intersection);
+ final DoublePrecisionContext splitterPrecision = splitter.getPrecision();
- LineConvexSubset low = null;
- LineConvexSubset high = null;
+ final int endCmp = splitterPrecision.compare(splitter.offset(endPoint), 0.0);
+ final boolean pointsTowardPlus = splitter.getOffsetDirection().dot(line.getDirection()) >= 0.0;
- int cmp = getPrecision().compare(splitAbscissa, end);
- if (cmp < 0) {
- low = new ReverseRay(line, splitAbscissa);
- high = new Segment(line, splitAbscissa, end);
- } else {
- low = this;
+ if (pointsTowardPlus && endCmp < 1) {
+ // entirely on minus side
+ return new Split<>(this, null);
+ } else if (!pointsTowardPlus && endCmp > -1) {
+ // entirely on plus side
+ return new Split<>(null, this);
}
- return createSplitResult(splitter, low, high);
+ // we're going to be split
+ final Segment splitSeg = new Segment(line, intersection, endPoint);
+ final ReverseRay splitRevRay = new ReverseRay(line, intersection);
+
+ final LineConvexSubset minus = (endCmp > 0) ? splitRevRay : splitSeg;
+ final LineConvexSubset plus = (endCmp > 0) ? splitSeg : splitRevRay;
+
+ return new Split<>(minus, plus);
}
}
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 0464ccd..8fa77b0 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
@@ -30,48 +30,39 @@ import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
*/
public final class Segment extends LineConvexSubset {
- /** Start abscissa for the segment. */
- private final double start;
+ /** Start point for the segment. */
+ private final Vector2D startPoint;
- /** End abscissa for the segment. */
- private final double end;
+ /** End point for the segment. */
+ private final Vector2D endPoint;
- /** Construct a new instance from a line and two points on the line. The points are projected onto
- * the line and must be in order of increasing abscissa. No validation is performed.
+ /** Construct a new instance from a line and two points on the line. Callers are responsible for
+ * ensuring that the given points lie on the line and are in order of increasing abscissa.
+ * No validation is performed.
* @param line line for the segment
* @param startPoint segment start point
* @param endPoint segment end point
*/
Segment(final Line line, final Vector2D startPoint, final Vector2D endPoint) {
- this(line, line.abscissa(startPoint), line.abscissa(endPoint));
- }
-
- /** Construct a new instance from a line and two abscissa locations on the line.
- * The abscissa locations must be in increasing order. No validation is performed.
- * @param line line for the segment
- * @param start abscissa start location
- * @param end abscissa end location
- */
- Segment(final Line line, final double start, final double end) {
super(line);
- this.start = start;
- this.end = end;
+ this.startPoint = startPoint;
+ this.endPoint = endPoint;
}
/** {@inheritDoc}
- *
- * <p>This method always returns {@code false}.</p>
- */
+ *
+ * <p>This method always returns {@code false}.</p>
+ */
@Override
public boolean isFull() {
return false;
}
/** {@inheritDoc}
- *
- * <p>This method always returns {@code false}.</p>
- */
+ *
+ * <p>This method always returns {@code false}.</p>
+ */
@Override
public boolean isInfinite() {
return false;
@@ -89,31 +80,46 @@ public final class Segment extends LineConvexSubset {
/** {@inheritDoc} */
@Override
public double getSize() {
- return end - start;
+ return startPoint.distance(endPoint);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Vector2D getBarycenter() {
+ return startPoint.lerp(endPoint, 0.5);
}
/** {@inheritDoc} */
@Override
public Vector2D getStartPoint() {
- return getLine().toSpace(start);
+ return startPoint;
}
/** {@inheritDoc} */
@Override
public double getSubspaceStart() {
- return start;
+ return getLine().abscissa(startPoint);
}
/** {@inheritDoc} */
@Override
public Vector2D getEndPoint() {
- return getLine().toSpace(end);
+ return endPoint;
}
/** {@inheritDoc} */
@Override
public double getSubspaceEnd() {
- return end;
+ return getLine().abscissa(endPoint);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Bounds2D getBounds() {
+ return Bounds2D.builder()
+ .add(startPoint)
+ .add(endPoint)
+ .build();
}
/** {@inheritDoc} */
@@ -130,7 +136,7 @@ public final class Segment extends LineConvexSubset {
/** {@inheritDoc} */
@Override
public Segment reverse() {
- return new Segment(getLine().reverse(), -end, -start);
+ return new Segment(getLine().reverse(), endPoint, startPoint);
}
/** {@inheritDoc} */
@@ -151,9 +157,9 @@ public final class Segment extends LineConvexSubset {
@Override
RegionLocation classifyAbscissa(final double abscissa) {
final DoublePrecisionContext precision = getPrecision();
- int startCmp = precision.compare(abscissa, start);
+ int startCmp = precision.compare(abscissa, getSubspaceStart());
if (startCmp > 0) {
- int endCmp = precision.compare(abscissa, end);
+ int endCmp = precision.compare(abscissa, getSubspaceEnd());
if (endCmp < 0) {
return RegionLocation.INSIDE;
} else if (endCmp == 0) {
@@ -169,32 +175,37 @@ public final class Segment extends LineConvexSubset {
/** {@inheritDoc} */
@Override
double closestAbscissa(final double abscissa) {
- return Math.max(start, Math.min(end, abscissa));
+ return Math.max(getSubspaceStart(), Math.min(getSubspaceEnd(), abscissa));
}
/** {@inheritDoc} */
@Override
Split<LineConvexSubset> splitOnIntersection(final Line splitter, final Vector2D intersection) {
final Line line = getLine();
- final double splitAbscissa = line.abscissa(intersection);
- Segment low = null;
- Segment high = null;
+ final DoublePrecisionContext splitterPrecision = splitter.getPrecision();
- final DoublePrecisionContext precision = getPrecision();
- int startCmp = precision.compare(splitAbscissa, start);
- if (startCmp <= 0) {
- high = this;
- } else {
- int endCmp = precision.compare(splitAbscissa, end);
- if (endCmp >= 0) {
- low = this;
- } else {
- low = new Segment(line, start, splitAbscissa);
- high = new Segment(line, splitAbscissa, end);
- }
+ final int startCmp = splitterPrecision.compare(splitter.offset(startPoint), 0.0);
+ final int endCmp = splitterPrecision.compare(splitter.offset(endPoint), 0.0);
+
+ if (startCmp == 0 && endCmp == 0) {
+ // the entire segment is directly on the splitter line
+ return new Split<>(null, null);
+ } else if (startCmp < 1 && endCmp < 1) {
+ // the entire segment is on the minus side
+ return new Split<>(this, null);
+ } else if (startCmp > -1 && endCmp > -1) {
+ // the entire segment is on the plus side
+ return new Split<>(null, this);
}
- return createSplitResult(splitter, low, high);
+ // we need to split the line
+ final Segment startSegment = new Segment(line, startPoint, intersection);
+ final Segment endSegment = new Segment(line, intersection, endPoint);
+
+ final Segment minus = (startCmp > 0) ? endSegment : startSegment;
+ final Segment plus = (startCmp > 0) ? startSegment : endSegment;
+
+ return new Split<>(minus, plus);
}
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java
index 369a90e..2d9df38 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java
@@ -16,7 +16,9 @@
*/
package org.apache.commons.geometry.euclidean.twod;
+import java.util.Arrays;
import java.util.Comparator;
+import java.util.Iterator;
import java.util.function.UnaryOperator;
import org.apache.commons.geometry.core.internal.DoubleFunction2N;
@@ -453,6 +455,143 @@ public class Vector2D extends MultiDimensionalEuclideanVector<Vector2D> {
return SimpleTupleFormat.getDefault().parse(str, Vector2D::new);
}
+ /** Return a vector containing the maximum component values from all input vectors.
+ * @param first first vector
+ * @param more additional vectors
+ * @return a vector containing the maximum component values from all input vectors
+ */
+ public static Vector2D max(final Vector2D first, final Vector2D... more) {
+ return computeMax(first, Arrays.asList(more).iterator());
+ }
+
+ /** Return a vector containing the maximum component values from all input vectors.
+ * @param vecs input vectors
+ * @return a vector containing the maximum component values from all input vectors
+ * @throws IllegalArgumentException if the argument does not contain any vectors
+ */
+ public static Vector2D max(final Iterable<Vector2D> vecs) {
+ final Iterator<Vector2D> it = vecs.iterator();
+ if (!it.hasNext()) {
+ throw new IllegalArgumentException("Cannot compute vector max: no vectors given");
+ }
+
+ return computeMax(it.next(), it);
+ }
+
+ /** Internal method for computing a max vector.
+ * @param first first vector
+ * @param more iterator with additional vectors
+ * @return vector containing the maximum component values of all input vectors
+ */
+ private static Vector2D computeMax(final Vector2D first, final Iterator<Vector2D> more) {
+ double x = first.getX();
+ double y = first.getY();
+
+ Vector2D vec;
+ while (more.hasNext()) {
+ vec = more.next();
+
+ x = Math.max(x, vec.getX());
+ y = Math.max(y, vec.getY());
+ }
+
+ return Vector2D.of(x, y);
+ }
+
+ /** Return a vector containing the minimum component values from all input vectors.
+ * @param first first vector
+ * @param more more vectors
+ * @return a vector containing the minimum component values from all input vectors
+ */
+ public static Vector2D min(final Vector2D first, final Vector2D... more) {
+ return computeMin(first, Arrays.asList(more).iterator());
+ }
+
+ /** Return a vector containing the minimum component values from all input vectors.
+ * @param vecs input vectors
+ * @return a vector containing the minimum component values from all input vectors
+ * @throws IllegalArgumentException if the argument does not contain any vectors
+ */
+ public static Vector2D min(final Iterable<Vector2D> vecs) {
+ final Iterator<Vector2D> it = vecs.iterator();
+ if (!it.hasNext()) {
+ throw new IllegalArgumentException("Cannot compute vector min: no vectors given");
+ }
+
+ return computeMin(it.next(), it);
+ }
+
+ /** Internal method for computing a min vector.
+ * @param first first vector
+ * @param more iterator with additional vectors
+ * @return vector containing the minimum component values of all input vectors
+ */
+ private static Vector2D computeMin(final Vector2D first, final Iterator<Vector2D> more) {
+ double x = first.getX();
+ double y = first.getY();
+
+ Vector2D vec;
+ while (more.hasNext()) {
+ vec = more.next();
+
+ x = Math.min(x, vec.getX());
+ y = Math.min(y, vec.getY());
+ }
+
+ return Vector2D.of(x, y);
+ }
+
+ /** Compute the centroid of the given points. The centroid is the arithmetic mean position of a set
+ * of points.
+ * @param first first point
+ * @param more additional points
+ * @return the centroid of the given points
+ */
+ public static Vector2D centroid(final Vector2D first, final Vector2D... more) {
+ return computeCentroid(first, Arrays.asList(more).iterator());
+ }
+
+ /** Compute the centroid of the given points. The centroid is the arithmetic mean position of a set
+ * of points.
+ * @param pts the points to compute the centroid of
+ * @return the centroid of the given points
+ * @throws IllegalArgumentException if the argument contains no points
+ */
+ public static Vector2D centroid(final Iterable<Vector2D> pts) {
+ final Iterator<Vector2D> it = pts.iterator();
+ if (!it.hasNext()) {
+ throw new IllegalArgumentException("Cannot compute centroid: no points given");
+ }
+
+ return computeCentroid(it.next(), it);
+ }
+
+ /** Internal method for computing the centroid of a set of points.
+ * @param first first point
+ * @param more iterator with additional points
+ * @return the centroid of the point set
+ */
+ private static Vector2D computeCentroid(final Vector2D first, final Iterator<Vector2D> more) {
+ double x = first.getX();
+ double y = first.getY();
+
+ int count = 1;
+
+ Vector2D pt;
+ while (more.hasNext()) {
+ pt = more.next();
+
+ x += pt.getX();
+ y += pt.getY();
+
+ ++count;
+ }
+
+ double invCount = 1.0 / count;
+
+ return new Vector2D(invCount * x, invCount * y);
+ }
+
/** Returns a vector consisting of the linear combination of the inputs.
* <p>
* A linear combination is the sum of all of the inputs multiplied by their
@@ -602,6 +741,12 @@ public class Vector2D extends MultiDimensionalEuclideanVector<Vector2D> {
/** {@inheritDoc} */
@Override
+ public Vector2D.Unit orthogonal() {
+ return new Unit(-getY(), getX());
+ }
+
+ /** {@inheritDoc} */
+ @Override
public Vector2D withNorm(final double mag) {
return multiply(mag);
}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/path/LinePath.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/path/LinePath.java
index 14c09d0..0b78b40 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/path/LinePath.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/path/LinePath.java
@@ -324,7 +324,7 @@ public class LinePath implements BoundarySource2D, Sized {
.append(", ");
}
- sb.append("vertices= ")
+ sb.append("vertexSequence= ")
.append(getVertexSequence());
final LineConvexSubset endElement = getEnd();
@@ -392,6 +392,7 @@ public class LinePath implements BoundarySource2D, Sized {
* @param precision precision context used to construct the line segment
* instances for the path
* @return new closed path constructed from the given vertices
+ * @throws IllegalStateException if {@code vertices} contains only a single unique vertex
* @see #fromVertices(Collection, boolean, DoublePrecisionContext)
*/
public static LinePath fromVertexLoop(final Collection<Vector2D> vertices,
@@ -408,6 +409,7 @@ public class LinePath implements BoundarySource2D, Sized {
* @param precision precision context used to construct the line segment
* instances for the path
* @return new path constructed from the given vertices
+ * @throws IllegalStateException if {@code vertices} contains only a single unique vertex
* @see #fromVertices(Collection, boolean, DoublePrecisionContext)
*/
public static LinePath fromVertices(final Collection<Vector2D> vertices,
@@ -424,6 +426,7 @@ public class LinePath implements BoundarySource2D, Sized {
* @param precision precision context used to construct the line segment
* instances for the path
* @return new path constructed from the given vertices
+ * @throws IllegalStateException if {@code vertices} contains only a single unique vertex
*/
public static LinePath fromVertices(final Collection<Vector2D> vertices,
final boolean close, final DoublePrecisionContext precision) {
@@ -666,6 +669,7 @@ public class LinePath implements BoundarySource2D, Sized {
/** Close the current path and build a new {@link LinePath} instance. This method is equivalent
* to {@code builder.build(true)}.
* @return new closed path instance
+ * @throws IllegalStateException if the builder was given only a single unique vertex
*/
public LinePath close() {
return build(true);
@@ -674,6 +678,7 @@ public class LinePath implements BoundarySource2D, Sized {
/** Build a {@link LinePath} instance from the configured path. This method is equivalent
* to {@code builder.build(false)}.
* @return new path instance
+ * @throws IllegalStateException if the builder was given only a single unique vertex
*/
public LinePath build() {
return build(false);
@@ -683,6 +688,7 @@ public class LinePath implements BoundarySource2D, Sized {
* @param close if true, the path will be closed by adding an end point equivalent to the
* start point
* @return new path instance
+ * @throws IllegalStateException if the builder was given only a single unique vertex
*/
public LinePath build(final boolean close) {
if (close) {
@@ -711,7 +717,7 @@ public class LinePath implements BoundarySource2D, Sized {
if (result.isEmpty() && startVertex != null) {
throw new IllegalStateException(
- MessageFormat.format("Unable to create line path; only a single vertex provided: {0} ",
+ MessageFormat.format("Unable to create line path; only a single unique vertex provided: {0} ",
startVertex));
}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
index 9476733..0051704 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
@@ -29,6 +29,7 @@ import org.apache.commons.geometry.euclidean.oned.Interval;
import org.apache.commons.geometry.euclidean.oned.RegionBSPTree1D;
import org.apache.commons.geometry.euclidean.oned.Vector1D;
import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D;
+import org.apache.commons.geometry.euclidean.threed.ConvexPolygon3D;
import org.apache.commons.geometry.euclidean.threed.Plane;
import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
import org.apache.commons.geometry.euclidean.threed.Planes;
@@ -407,27 +408,27 @@ public class DocumentationExamplesTest {
public void testRegionBSPTree3DExample() {
DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
- // create the faces of a pyrmaid with a square base and its apex pointing along the
+ // create the faces of a pyramid with a square base and its apex pointing along the
// positive z axis
- Vector3D a1 = Vector3D.Unit.PLUS_Z;
- Vector3D b1 = Vector3D.of(0.5, 0.5, 0.0);
- Vector3D b2 = Vector3D.of(0.5, -0.5, 0.0);
- Vector3D b3 = Vector3D.of(-0.5, -0.5, 0.0);
- Vector3D b4 = Vector3D.of(-0.5, 0.5, 0.0);
-
- Vector3D[][] faceIndices = {
- {b1, a1, b2},
- {b2, a1, b3},
- {b3, a1, b4},
- {b4, a1, b1},
- {b1, b2, b3, b4}
+ Vector3D[] vertices = {
+ Vector3D.Unit.PLUS_Z,
+ Vector3D.of(0.5, 0.5, 0.0),
+ Vector3D.of(0.5, -0.5, 0.0),
+ Vector3D.of(-0.5, -0.5, 0.0),
+ Vector3D.of(-0.5, 0.5, 0.0)
};
- // convert the vertices to convex subplanes and insert into a bsp tree
- RegionBSPTree3D tree = RegionBSPTree3D.empty();
- Arrays.stream(faceIndices)
- .map(vertices -> Planes.subsetFromVertexLoop(Arrays.asList(vertices), precision))
- .forEach(tree::insert);
+ int[][] faceIndices = {
+ {1, 0, 2},
+ {2, 0, 3},
+ {3, 0, 4},
+ {4, 0, 1},
+ {1, 2, 3, 4}
+ };
+
+ // convert the vertices and faces to convex polygons and use to construct a BSP tree
+ List<ConvexPolygon3D> faces = Planes.indexedConvexPolygons(vertices, faceIndices, precision);
+ RegionBSPTree3D tree = RegionBSPTree3D.from(faces);
// split the region through its barycenter along a diagonal of the base
Plane cutter = Planes.fromPointAndNormal(tree.getBarycenter(), Vector3D.Unit.from(1, 1, 0), precision);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java
index 99d8f0e..33fa04c 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java
@@ -16,9 +16,12 @@
*/
package org.apache.commons.geometry.euclidean;
+import java.util.List;
+
import org.apache.commons.geometry.core.Region;
import org.apache.commons.geometry.core.RegionLocation;
import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.euclidean.oned.Vector1D;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
@@ -186,6 +189,46 @@ public final class EuclideanTestUtils {
}
/**
+ * Assert that the given lists represent equivalent vertex loops. The loops must contain the same sequence
+ * of vertices but do not need to start at the same point.
+ * @param expected
+ * @param actual
+ * @param precision
+ */
+ public static void assertVertexLoopSequence(List<Vector3D> expected, List<Vector3D> actual,
+ DoublePrecisionContext precision) {
+ Assert.assertEquals("Vertex sequences have different sizes", expected.size(), actual.size());
+
+ if (expected.size() > 0) {
+
+ int offset = -1;
+ Vector3D start = expected.get(0);
+ for (int i = 0; i < actual.size(); ++i) {
+ if (actual.get(i).eq(start, precision)) {
+ offset = i;
+ break;
+ }
+ }
+
+ if (offset < 0) {
+ Assert.fail("Vertex loops do not share any points: expected " + expected + " but was " + actual);
+ }
+
+ Vector3D expectedVertex;
+ Vector3D actualVertex;
+ for (int i = 0; i < expected.size(); ++i) {
+ expectedVertex = expected.get(i);
+ actualVertex = actual.get((i + offset) % actual.size());
+
+ if (!expectedVertex.eq(actualVertex, precision)) {
+ Assert.fail("Unexpected vertex at index " + i + ": expected " + expectedVertex +
+ " but was " + actualVertex);
+ }
+ }
+ }
+ }
+
+ /**
* Asserts that the given value is negative infinity..
*
* @param value
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/OrientedPointTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/OrientedPointTest.java
index e9b19f9..249d89b 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/OrientedPointTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/OrientedPointTest.java
@@ -424,7 +424,7 @@ public class OrientedPointTest {
@Test
public void testSubset_simpleMethods() {
// arrange
- OrientedPoint pt = OrientedPoints.createPositiveFacing(0, TEST_PRECISION);
+ OrientedPoint pt = OrientedPoints.createPositiveFacing(2, TEST_PRECISION);
HyperplaneConvexSubset<Vector1D> sub = pt.span();
// act/assert
@@ -434,6 +434,7 @@ public class OrientedPointTest {
Assert.assertFalse(sub.isInfinite());
Assert.assertTrue(sub.isFinite());
Assert.assertEquals(0.0, sub.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(2), sub.getBarycenter(), TEST_EPS);
List<? extends HyperplaneConvexSubset<Vector1D>> list = sub.toConvex();
Assert.assertEquals(1, list.size());
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneSubsetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubsetTest.java
similarity index 77%
rename from commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneSubsetTest.java
rename to commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubsetTest.java
index 12308fe..846a95e 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneSubsetTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubsetTest.java
@@ -23,25 +23,25 @@ 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.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
import org.apache.commons.geometry.core.partitioning.Split;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
import org.apache.commons.geometry.euclidean.twod.ConvexArea;
+import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
import org.junit.Assert;
import org.junit.Test;
-public class PlaneSubsetTest {
+public class AbstractPlaneSubsetTest {
private static final double TEST_EPS = 1e-10;
private static final DoublePrecisionContext TEST_PRECISION =
new EpsilonDoublePrecisionContext(TEST_EPS);
- private static final Plane XY_PLANE = Planes.fromPointAndPlaneVectors(Vector3D.ZERO,
+ private static final EmbeddingPlane XY_PLANE = Planes.fromPointAndPlaneVectors(Vector3D.ZERO,
Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
@Test
@@ -67,7 +67,7 @@ public class PlaneSubsetTest {
@Test
public void testBuilder_addSingleConvex_returnsSameInstance() {
// arrange
- PlaneConvexSubset convex = Planes.subsetFromVertexLoop(Arrays.asList(
+ PlaneConvexSubset convex = Planes.convexPolygonFromVertices(Arrays.asList(
Vector3D.ZERO,
Vector3D.of(1, 0, 0),
Vector3D.of(0, 1, 0)
@@ -87,7 +87,7 @@ public class PlaneSubsetTest {
@Test
public void testBuilder_addSingleTreeSubset() {
// arrange
- ConvexArea area = ConvexArea.fromVertexLoop(Arrays.asList(
+ ConvexArea area = ConvexArea.convexPolygonFromVertices(Arrays.asList(
Vector2D.ZERO,
Vector2D.of(1, 0),
Vector2D.of(0, 1)
@@ -122,17 +122,17 @@ public class PlaneSubsetTest {
@Test
public void testBuilder_addMixed_convexFirst() {
// arrange
- Plane mainPlane = Planes.fromPointAndPlaneVectors(
+ EmbeddingPlane mainPlane = Planes.fromPointAndPlaneVectors(
Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
- ConvexArea a = ConvexArea.fromVertexLoop(
+ ConvexArea a = ConvexArea.convexPolygonFromVertices(
Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(1, 1).normalize()), TEST_PRECISION);
- ConvexArea b = ConvexArea.fromVertexLoop(
+ ConvexArea b = ConvexArea.convexPolygonFromVertices(
Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 1).normalize(), Vector2D.of(0, 1)), TEST_PRECISION);
- ConvexArea c = ConvexArea.fromVertexLoop(
+ ConvexArea c = ConvexArea.convexPolygonFromVertices(
Arrays.asList(Vector2D.Unit.PLUS_X, Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y), TEST_PRECISION);
- Plane closePlane = Planes.fromPointAndPlaneVectors(
+ EmbeddingPlane closePlane = Planes.fromPointAndPlaneVectors(
Vector3D.of(1e-16, 0, 1), Vector3D.of(1, 1e-16, 0), Vector3D.Unit.PLUS_Y, TEST_PRECISION);
// act
@@ -161,18 +161,18 @@ public class PlaneSubsetTest {
@Test
public void testBuilder_addMixed_treeSubsetFirst() {
- // arrange
- Plane mainPlane = Planes.fromPointAndPlaneVectors(
+ // arrange
+ EmbeddingPlane mainPlane = Planes.fromPointAndPlaneVectors(
Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
- ConvexArea a = ConvexArea.fromVertexLoop(
+ ConvexArea a = ConvexArea.convexPolygonFromVertices(
Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(1, 1).normalize()), TEST_PRECISION);
- ConvexArea b = ConvexArea.fromVertexLoop(
+ ConvexArea b = ConvexArea.convexPolygonFromVertices(
Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 1).normalize(), Vector2D.of(0, 1)), TEST_PRECISION);
- ConvexArea c = ConvexArea.fromVertexLoop(
+ ConvexArea c = ConvexArea.convexPolygonFromVertices(
Arrays.asList(Vector2D.Unit.PLUS_X, Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y), TEST_PRECISION);
- Plane closePlane = Planes.fromPointAndPlaneVectors(
+ EmbeddingPlane closePlane = Planes.fromPointAndPlaneVectors(
Vector3D.of(1e-16, 0, 1), Vector3D.of(1, 1e-16, 0), Vector3D.Unit.PLUS_Y, TEST_PRECISION);
// act
@@ -200,7 +200,7 @@ public class PlaneSubsetTest {
}
@Test
- public void testBuilder_nullArguments() {
+ public void testBuilder_add_nullArguments() {
// arrange
HyperplaneSubset.Builder<Vector3D> builder = XY_PLANE.span().builder();
@@ -215,9 +215,9 @@ public class PlaneSubsetTest {
}
@Test
- public void testBuilder_argumentsFromDifferentPlanes() {
+ public void testBuilder_add_argumentsFromDifferentPlanes() {
// arrange
- PlaneConvexSubset convex = Planes.subsetFromVertexLoop(Arrays.asList(
+ ConvexPolygon3D convex = Planes.convexPolygonFromVertices(Arrays.asList(
Vector3D.ZERO,
Vector3D.of(1, 0, 1),
Vector3D.of(0, 1, 1)
@@ -225,24 +225,27 @@ public class PlaneSubsetTest {
HyperplaneSubset.Builder<Vector3D> builder = XY_PLANE.span().builder();
+ RegionBSPTree2D tree = RegionBSPTree2D.empty();
+ tree.add(convex.getEmbedded().getSubspaceRegion());
+
// act/assert
GeometryTestUtils.assertThrows(() -> {
builder.add(convex);
}, IllegalArgumentException.class);
GeometryTestUtils.assertThrows(() -> {
- builder.add(new EmbeddedTreePlaneSubset(convex.getPlane(), convex.getSubspaceRegion().toTree()));
+ builder.add(new EmbeddedTreePlaneSubset(convex.getPlane().getEmbedding(), tree));
}, IllegalArgumentException.class);
}
@Test
- public void testBuilder_addUnknownType() {
+ public void testBuilder_add_addUnknownType() {
// arrange
HyperplaneSubset.Builder<Vector3D> builder = XY_PLANE.span().builder();
// act/assert
GeometryTestUtils.assertThrows(() -> {
- builder.add(new StubSubPlane(Planes.fromNormal(Vector3D.Unit.PLUS_Y, TEST_PRECISION)));
+ builder.add(new StubPlaneSubset(XY_PLANE));
}, IllegalArgumentException.class);
}
@@ -252,14 +255,46 @@ public class PlaneSubsetTest {
}
}
- private static class StubSubPlane extends PlaneSubset implements HyperplaneSubset<Vector3D> {
+ private static class StubPlaneSubset extends AbstractPlaneSubset {
+
+ private final Plane plane;
- StubSubPlane(Plane plane) {
- super(plane);
+ StubPlaneSubset(final Plane plane) {
+ this.plane = plane;
}
@Override
- public Split<? extends HyperplaneSubset<Vector3D>> split(Hyperplane<Vector3D> splitter) {
+ public Plane getPlane() {
+ return plane;
+ }
+
+ @Override
+ public List<PlaneConvexSubset> toConvex() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<Triangle3D> toTriangles() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isFull() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public RegionLocation classify(Vector3D pt) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Vector3D closest(Vector3D pt) {
throw new UnsupportedOperationException();
}
@@ -269,12 +304,27 @@ public class PlaneSubsetTest {
}
@Override
- public List<PlaneConvexSubset> toConvex() {
+ public Split<? extends HyperplaneSubset<Vector3D>> split(Hyperplane<Vector3D> splitter) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public double getSize() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Vector3D getBarycenter() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public PlaneSubset.Embedded getEmbedded() {
throw new UnsupportedOperationException();
}
@Override
- public HyperplaneBoundedRegion<Vector2D> getSubspaceRegion() {
+ public Bounds3D getBounds() {
throw new UnsupportedOperationException();
}
}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3DTest.java
index 55cc960..7c22343 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3DTest.java
@@ -36,9 +36,9 @@ public class BoundarySource3DTest {
@Test
public void testToTree() {
// act
- PlaneConvexSubset a = Planes.subsetFromVertexLoop(
+ PlaneConvexSubset a = Planes.convexPolygonFromVertices(
Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION);
- PlaneConvexSubset b = Planes.subsetFromVertexLoop(
+ PlaneConvexSubset b = Planes.convexPolygonFromVertices(
Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_Z), TEST_PRECISION);
BoundarySource3D src = BoundarySource3D.from(a, b);
@@ -79,9 +79,9 @@ public class BoundarySource3DTest {
@Test
public void testFrom_varargs() {
// act
- PlaneConvexSubset a = Planes.subsetFromVertexLoop(
+ PlaneConvexSubset a = Planes.convexPolygonFromVertices(
Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION);
- PlaneConvexSubset b = Planes.subsetFromVertexLoop(
+ PlaneConvexSubset b = Planes.convexPolygonFromVertices(
Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_Z), TEST_PRECISION);
BoundarySource3D src = BoundarySource3D.from(a, b);
@@ -110,9 +110,9 @@ public class BoundarySource3DTest {
@Test
public void testFrom_list() {
// act
- PlaneConvexSubset a = Planes.subsetFromVertexLoop(
+ PlaneConvexSubset a = Planes.convexPolygonFromVertices(
Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION);
- PlaneConvexSubset b = Planes.subsetFromVertexLoop(
+ PlaneConvexSubset b = Planes.convexPolygonFromVertices(
Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_Z), TEST_PRECISION);
List<PlaneConvexSubset> input = new ArrayList<>();
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceBoundsBuilder3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceBoundsBuilder3DTest.java
new file mode 100644
index 0000000..30f86d5
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceBoundsBuilder3DTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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.euclidean.threed;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class BoundarySourceBoundsBuilder3DTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ @Test
+ public void testGetBounds_noBoundaries() {
+ // arrange
+ BoundarySource3D src = BoundarySource3D.from(new ArrayList<>());
+ BoundarySourceBoundsBuilder3D builder = new BoundarySourceBoundsBuilder3D();
+
+ // act
+ Bounds3D b = builder.getBounds(src);
+
+ // assert
+ Assert.assertNull(b);
+ }
+
+ @Test
+ public void testGetBounds_singleFiniteBoundary() {
+ // arrange
+ ConvexPolygon3D poly = Planes.convexPolygonFromVertices(Arrays.asList(
+ Vector3D.of(1, 1, 1),
+ Vector3D.of(1, 0, 2),
+ Vector3D.of(3, 4, 5)), TEST_PRECISION);
+
+ BoundarySource3D src = BoundarySource3D.from(poly);
+ BoundarySourceBoundsBuilder3D builder = new BoundarySourceBoundsBuilder3D();
+
+ // act
+ Bounds3D b = builder.getBounds(src);
+
+ // assert
+ checkBounds(b, Vector3D.of(1, 0, 1), Vector3D.of(3, 4, 5));
+ for (Vector3D pt : poly.getVertices()) {
+ Assert.assertTrue(b.contains(pt));
+ }
+ }
+
+ @Test
+ public void testGetBounds_multipleFiniteBoundaries() {
+ // arrange
+ ConvexPolygon3D poly1 = Planes.convexPolygonFromVertices(Arrays.asList(
+ Vector3D.of(1, 1, 1),
+ Vector3D.of(1, 0, 2),
+ Vector3D.of(3, 4, 5)), TEST_PRECISION);
+
+ ConvexPolygon3D poly2 = Planes.convexPolygonFromVertices(Arrays.asList(
+ Vector3D.of(-1, 1, 1),
+ Vector3D.of(1, 4, 4),
+ Vector3D.of(7, 4, 5)), TEST_PRECISION);
+
+ ConvexPolygon3D poly3 = Planes.convexPolygonFromVertices(Arrays.asList(
+ Vector3D.of(-2, 1, 1),
+ Vector3D.of(1, 7, 2),
+ Vector3D.of(5, 4, 10)), TEST_PRECISION);
+
+ BoundarySource3D src = BoundarySource3D.from(poly1, poly2, poly3);
+ BoundarySourceBoundsBuilder3D builder = new BoundarySourceBoundsBuilder3D();
+
+ // act
+ Bounds3D b = builder.getBounds(src);
+
+ // assert
+ checkBounds(b, Vector3D.of(-2, 0, 1), Vector3D.of(7, 7, 10));
+
+ src.boundaryStream().forEach(boundary -> {
+ for (Vector3D pt : boundary.getVertices()) {
+ Assert.assertTrue(b.contains(pt));
+ }
+ });
+ }
+
+ @Test
+ public void testGetBounds_singleInfiniteBoundary() {
+ // arrange
+ PlaneConvexSubset boundary = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION)
+ .span();
+ BoundarySource3D src = BoundarySource3D.from(boundary);
+ BoundarySourceBoundsBuilder3D builder = new BoundarySourceBoundsBuilder3D();
+
+ // act
+ Bounds3D b = builder.getBounds(src);
+
+ // assert
+ Assert.assertNull(b);
+ }
+
+ @Test
+ public void testGetBounds_mixedFiniteAndInfiniteBoundaries() {
+ // arrange
+ PlaneConvexSubset inf = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION)
+ .span()
+ .split(Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, TEST_PRECISION))
+ .getMinus();
+
+ ConvexPolygon3D poly1 = Planes.convexPolygonFromVertices(Arrays.asList(
+ Vector3D.of(1, 1, 1),
+ Vector3D.of(1, 0, 2),
+ Vector3D.of(3, 4, 5)), TEST_PRECISION);
+
+ ConvexPolygon3D poly2 = Planes.convexPolygonFromVertices(Arrays.asList(
+ Vector3D.of(-1, 1, 1),
+ Vector3D.of(1, 4, 4),
+ Vector3D.of(7, 4, 5)), TEST_PRECISION);
+
+ ConvexPolygon3D poly3 = Planes.convexPolygonFromVertices(Arrays.asList(
+ Vector3D.of(-2, 1, 1),
+ Vector3D.of(1, 7, 2),
+ Vector3D.of(5, 4, 10)), TEST_PRECISION);
+
+ BoundarySource3D src = BoundarySource3D.from(poly1, poly2, inf, poly3);
+ BoundarySourceBoundsBuilder3D builder = new BoundarySourceBoundsBuilder3D();
+
+ // act
+ Bounds3D b = builder.getBounds(src);
+
+ // assert
+ Assert.assertNull(b);
+ }
+
+ private static void checkBounds(Bounds3D b, Vector3D min, Vector3D max) {
+ EuclideanTestUtils.assertCoordinatesEqual(min, b.getMin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(max, b.getMax(), TEST_EPS);
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecaster3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecaster3DTest.java
index a38b244..c62bacd 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecaster3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecaster3DTest.java
@@ -102,8 +102,8 @@ public class BoundarySourceLinecaster3DTest {
public void testLinecast_line_removesDuplicatePoints() {
// arrange
BoundarySource3D src = BoundarySource3D.from(
- Planes.subsetFromVertexLoop(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION),
- Planes.subsetFromVertexLoop(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X), TEST_PRECISION)
+ Planes.convexPolygonFromVertices(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION),
+ Planes.convexPolygonFromVertices(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X), TEST_PRECISION)
);
BoundarySourceLinecaster3D linecaster = new BoundarySourceLinecaster3D(src);
@@ -235,8 +235,8 @@ public class BoundarySourceLinecaster3DTest {
public void testLinecast_segment_removesDuplicatePoints() {
// arrange
BoundarySource3D src = BoundarySource3D.from(
- Planes.subsetFromVertexLoop(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION),
- Planes.subsetFromVertexLoop(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X), TEST_PRECISION)
+ Planes.convexPolygonFromVertices(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION),
+ Planes.convexPolygonFromVertices(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X), TEST_PRECISION)
);
BoundarySourceLinecaster3D linecaster = new BoundarySourceLinecaster3D(src);
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
new file mode 100644
index 0000000..7693870
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Bounds3DTest.java
@@ -0,0 +1,538 @@
+/*
+ * 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.euclidean.threed;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+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.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.threed.shape.Parallelepiped;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class Bounds3DTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ private static final String NO_POINTS_MESSAGE = "Cannot construct bounds: no points given";
+
+ private static final Pattern INVALID_BOUNDS_PATTERN =
+ Pattern.compile("^Invalid bounds: min= \\([^\\)]+\\), max= \\([^\\)]+\\)");
+
+ @Test
+ public void testFrom_varargs_singlePoint() {
+ // arrange
+ Vector3D p1 = Vector3D.of(-1, 2, -3);
+
+ // act
+ Bounds3D b = Bounds3D.from(p1);
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMax(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, b.getDiagonal(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, b.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testFrom_varargs_multiplePoints() {
+ // arrange
+ Vector3D p1 = Vector3D.of(1, 6, 7);
+ Vector3D p2 = Vector3D.of(0, 5, 11);
+ Vector3D p3 = Vector3D.of(3, 6, 8);
+
+ // act
+ Bounds3D b = Bounds3D.from(p1, p2, p3);
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 5, 7), b.getMin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 6, 11), b.getMax(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 1, 4), b.getDiagonal(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.5, 5.5, 9), b.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testFrom_iterable_singlePoint() {
+ // arrange
+ Vector3D p1 = Vector3D.of(-1, 2, -3);
+
+ // act
+ Bounds3D b = Bounds3D.from(Arrays.asList(p1));
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMax(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, b.getDiagonal(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(p1, b.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testFrom_iterable_multiplePoints() {
+ // arrange
+ Vector3D p1 = Vector3D.of(1, 6, 7);
+ Vector3D p2 = Vector3D.of(2, 5, 9);
+ Vector3D p3 = Vector3D.of(3, 4, 8);
+
+ // act
+ Bounds3D b = Bounds3D.from(Arrays.asList(p1, p2, p3));
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 4, 7), b.getMin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 6, 9), b.getMax(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 2, 2), b.getDiagonal(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 5, 8), b.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testFrom_iterable_noPoints() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ Bounds3D.from(new ArrayList<>());
+ }, IllegalStateException.class, NO_POINTS_MESSAGE);
+ }
+
+ @Test
+ public void testFrom_invalidBounds() {
+ // arrange
+ Vector3D good = Vector3D.of(1, 1, 1);
+
+ Vector3D nan = Vector3D.of(Double.NaN, 1, 1);
+ Vector3D posInf = Vector3D.of(1, Double.POSITIVE_INFINITY, 1);
+ Vector3D negInf = Vector3D.of(1, 1, Double.NEGATIVE_INFINITY);
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ Bounds3D.from(Vector3D.NaN);
+ }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Bounds3D.from(Vector3D.POSITIVE_INFINITY);
+ }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Bounds3D.from(Vector3D.NEGATIVE_INFINITY);
+ }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Bounds3D.from(good, nan);
+ }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Bounds3D.from(posInf, good);
+ }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
+
+ GeometryTestUtils.assertThrows(() -> {
+ Bounds3D.from(good, negInf, good);
+ }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
+ }
+
+ @Test
+ public void testHasSize() {
+ // arrange
+ DoublePrecisionContext low = new EpsilonDoublePrecisionContext(1e-2);
+ DoublePrecisionContext high = new EpsilonDoublePrecisionContext(1e-10);
+
+ Vector3D p1 = Vector3D.ZERO;
+
+ Vector3D p2 = Vector3D.of(1e-5, 1, 1);
+ Vector3D p3 = Vector3D.of(1, 1e-5, 1);
+ Vector3D p4 = Vector3D.of(1, 1, 1e-5);
+
+ Vector3D p5 = Vector3D.of(1, 1, 1);
+
+ // act/assert
+ Assert.assertFalse(Bounds3D.from(p1).hasSize(high));
+ Assert.assertFalse(Bounds3D.from(p1).hasSize(low));
+
+ Assert.assertTrue(Bounds3D.from(p1, p2).hasSize(high));
+ Assert.assertFalse(Bounds3D.from(p1, p2).hasSize(low));
+
+ Assert.assertTrue(Bounds3D.from(p1, p3).hasSize(high));
+ Assert.assertFalse(Bounds3D.from(p1, p3).hasSize(low));
+
+ Assert.assertTrue(Bounds3D.from(p1, p4).hasSize(high));
+ Assert.assertFalse(Bounds3D.from(p1, p4).hasSize(low));
+
+ Assert.assertTrue(Bounds3D.from(p1, p5).hasSize(high));
+ Assert.assertTrue(Bounds3D.from(p1, p5).hasSize(low));
+ }
+
+ @Test
+ public void testContains_strict() {
+ // arrange
+ Bounds3D b = Bounds3D.from(
+ Vector3D.of(0, 4, 8),
+ Vector3D.of(2, 6, 10));
+
+ // act/assert
+ assertContainsStrict(b, true,
+ b.getBarycenter(),
+ Vector3D.of(0, 4, 8), Vector3D.of(2, 6, 10),
+ Vector3D.of(1, 5, 9),
+ Vector3D.of(0, 5, 9), Vector3D.of(2, 5, 9),
+ Vector3D.of(1, 4, 9), Vector3D.of(1, 6, 9),
+ Vector3D.of(1, 5, 8), Vector3D.of(1, 5, 10));
+
+ assertContainsStrict(b, false,
+ Vector3D.ZERO,
+ Vector3D.of(-1, 5, 9), Vector3D.of(3, 5, 9),
+ Vector3D.of(1, 3, 9), Vector3D.of(1, 7, 9),
+ Vector3D.of(1, 5, 7), Vector3D.of(1, 5, 11),
+ Vector3D.of(-1e-15, 4, 8), Vector3D.of(2, 6 + 1e-15, 10), Vector3D.of(0, 4, 10 + 1e-15));
+ }
+
+ @Test
+ public void testContains_precision() {
+ // arrange
+ Bounds3D b = Bounds3D.from(
+ Vector3D.of(0, 4, 8),
+ Vector3D.of(2, 6, 10));
+
+ // act/assert
+ assertContainsWithPrecision(b, true,
+ b.getBarycenter(),
+ Vector3D.of(0, 4, 8), Vector3D.of(2, 6, 10),
+ Vector3D.of(1, 5, 9),
+ Vector3D.of(0, 5, 9), Vector3D.of(2, 5, 9),
+ Vector3D.of(1, 4, 9), Vector3D.of(1, 6, 9),
+ Vector3D.of(1, 5, 8), Vector3D.of(1, 5, 10),
+ Vector3D.of(-1e-15, 4, 8), Vector3D.of(2, 6 + 1e-15, 10), Vector3D.of(0, 4, 10 + 1e-15));
+
+ assertContainsWithPrecision(b, false,
+ Vector3D.ZERO,
+ Vector3D.of(-1, 5, 9), Vector3D.of(3, 5, 9),
+ Vector3D.of(1, 3, 9), Vector3D.of(1, 7, 9),
+ Vector3D.of(1, 5, 7), Vector3D.of(1, 5, 11));
+ }
+
+ @Test
+ public void testIntersects() {
+ // arrange
+ Bounds3D b = Bounds3D.from(Vector3D.ZERO, Vector3D.of(1, 1, 1));
+
+ // act/assert
+ checkIntersects(b, Vector3D::getX, (v, x) -> Vector3D.of(x, v.getY(), v.getZ()));
+ checkIntersects(b, Vector3D::getY, (v, y) -> Vector3D.of(v.getX(), y, v.getZ()));
+ checkIntersects(b, Vector3D::getZ, (v, z) -> Vector3D.of(v.getX(), v.getY(), z));
+ }
+
+ private void checkIntersects(Bounds3D b, ToDoubleFunction<Vector3D> getter,
+ BiFunction<Vector3D, Double, Vector3D> setter) {
+
+ Vector3D min = b.getMin();
+ Vector3D max = b.getMax();
+
+ double minValue = getter.applyAsDouble(min);
+ double maxValue = getter.applyAsDouble(max);
+ double midValue = (0.5 * (maxValue - minValue)) + minValue;
+
+ // check all possible interval relationships
+
+ // start below minValue
+ Assert.assertFalse(b.intersects(Bounds3D.from(
+ setter.apply(min, minValue - 2), setter.apply(max, minValue - 1))));
+
+ Assert.assertTrue(b.intersects(Bounds3D.from(
+ setter.apply(min, minValue - 2), setter.apply(max, minValue))));
+ Assert.assertTrue(b.intersects(Bounds3D.from(
+ setter.apply(min, minValue - 2), setter.apply(max, midValue))));
+ Assert.assertTrue(b.intersects(Bounds3D.from(
+ setter.apply(min, minValue - 2), setter.apply(max, maxValue))));
+ Assert.assertTrue(b.intersects(Bounds3D.from(
+ setter.apply(min, minValue - 2), setter.apply(max, maxValue + 1))));
+
+ // start on minValue
+ Assert.assertTrue(b.intersects(Bounds3D.from(
+ setter.apply(min, minValue), setter.apply(max, minValue))));
+ Assert.assertTrue(b.intersects(Bounds3D.from(
+ setter.apply(min, minValue), setter.apply(max, midValue))));
+ Assert.assertTrue(b.intersects(Bounds3D.from(
+ setter.apply(min, minValue), setter.apply(max, maxValue))));
+ Assert.assertTrue(b.intersects(Bounds3D.from(
+ setter.apply(min, minValue), setter.apply(max, maxValue + 1))));
+
+ // start on midValue
+ Assert.assertTrue(b.intersects(Bounds3D.from(
+ setter.apply(min, midValue), setter.apply(max, midValue))));
+ Assert.assertTrue(b.intersects(Bounds3D.from(
+ setter.apply(min, midValue), setter.apply(max, maxValue))));
+ Assert.assertTrue(b.intersects(Bounds3D.from(
+ setter.apply(min, midValue), setter.apply(max, maxValue + 1))));
+
+ // start on maxValue
+ Assert.assertTrue(b.intersects(Bounds3D.from(
+ setter.apply(min, maxValue), setter.apply(max, maxValue))));
+ Assert.assertTrue(b.intersects(Bounds3D.from(
+ setter.apply(min, maxValue), setter.apply(max, maxValue + 1))));
+
+ // start above maxValue
+ Assert.assertFalse(b.intersects(Bounds3D.from(
+ setter.apply(min, maxValue + 1), setter.apply(max, maxValue + 2))));
+ }
+
+ @Test
+ public void testIntersection() {
+ // -- arrange
+ Bounds3D b = Bounds3D.from(Vector3D.ZERO, Vector3D.of(1, 1, 1));
+
+ // -- act/assert
+
+ // move along x-axis
+ Assert.assertNull(b.intersection(Bounds3D.from(Vector3D.of(-2, 0, 0), Vector3D.of(-1, 1, 1))));
+ checkIntersection(b, Vector3D.of(-1, 0, 0), Vector3D.of(0, 1, 1),
+ Vector3D.of(0, 0, 0), Vector3D.of(0, 1, 1));
+ checkIntersection(b, Vector3D.of(-1, 0, 0), Vector3D.of(0.5, 1, 1),
+ Vector3D.of(0, 0, 0), Vector3D.of(0.5, 1, 1));
+ checkIntersection(b, Vector3D.of(-1, 0, 0), Vector3D.of(1, 1, 1),
+ Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+ checkIntersection(b, Vector3D.of(-1, 0, 0), Vector3D.of(2, 1, 1),
+ Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+ checkIntersection(b, Vector3D.of(0, 0, 0), Vector3D.of(2, 1, 1),
+ Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+ checkIntersection(b, Vector3D.of(0.5, 0, 0), Vector3D.of(2, 1, 1),
+ Vector3D.of(0.5, 0, 0), Vector3D.of(1, 1, 1));
+ checkIntersection(b, Vector3D.of(1, 0, 0), Vector3D.of(2, 1, 1),
+ Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 1));
+ Assert.assertNull(b.intersection(Bounds3D.from(Vector3D.of(2, 0, 0), Vector3D.of(3, 1, 1))));
+
+ // move along y-axis
+ Assert.assertNull(b.intersection(Bounds3D.from(Vector3D.of(0, -2, 0), Vector3D.of(1, -1, 1))));
+ checkIntersection(b, Vector3D.of(0, -1, 0), Vector3D.of(1, 0, 1),
+ Vector3D.of(0, 0, 0), Vector3D.of(1, 0, 1));
+ checkIntersection(b, Vector3D.of(0, -1, 0), Vector3D.of(1, 0.5, 1),
+ Vector3D.of(0, 0, 0), Vector3D.of(1, 0.5, 1));
+ checkIntersection(b, Vector3D.of(0, -1, 0), Vector3D.of(1, 1, 1),
+ Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+ checkIntersection(b, Vector3D.of(0, -1, 0), Vector3D.of(1, 2, 1),
+ Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+ checkIntersection(b, Vector3D.of(0, 0, 0), Vector3D.of(1, 2, 1),
+ Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+ checkIntersection(b, Vector3D.of(0, 0.5, 0), Vector3D.of(1, 2, 1),
+ Vector3D.of(0, 0.5, 0), Vector3D.of(1, 1, 1));
+ checkIntersection(b, Vector3D.of(0, 1, 0), Vector3D.of(1, 2, 1),
+ Vector3D.of(0, 1, 0), Vector3D.of(1, 1, 1));
+ Assert.assertNull(b.intersection(Bounds3D.from(Vector3D.of(0, 2, 0), Vector3D.of(1, 3, 1))));
+
+ // move along z-axis
+ Assert.assertNull(b.intersection(Bounds3D.from(Vector3D.of(0, 0, -2), Vector3D.of(1, 1, -1))));
+ checkIntersection(b, Vector3D.of(0, 0, -1), Vector3D.of(1, 1, 0),
+ Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 0));
+ checkIntersection(b, Vector3D.of(0, 0, -1), Vector3D.of(1, 1, 0.5),
+ Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 0.5));
+ checkIntersection(b, Vector3D.of(0, 0, -1), Vector3D.of(1, 1, 1),
+ Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+ checkIntersection(b, Vector3D.of(0, 0, -1), Vector3D.of(1, 1, 2),
+ Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+ checkIntersection(b, Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 2),
+ Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+ checkIntersection(b, Vector3D.of(0, 0, 0.5), Vector3D.of(1, 1, 2),
+ Vector3D.of(0, 0, 0.5), Vector3D.of(1, 1, 1));
+ checkIntersection(b, Vector3D.of(0, 0, 1), Vector3D.of(1, 1, 2),
+ Vector3D.of(0, 0, 1), Vector3D.of(1, 1, 1));
+ Assert.assertNull(b.intersection(Bounds3D.from(Vector3D.of(0, 0, 2), Vector3D.of(1, 1, 3))));
+ }
+
+ private void checkIntersection(Bounds3D b, Vector3D a1, Vector3D a2, Vector3D r1, Vector3D r2) {
+ Bounds3D a = Bounds3D.from(a1, a2);
+ Bounds3D result = b.intersection(a);
+
+ checkBounds(result, r1, r2);
+ }
+
+ @Test
+ public void toRegion() {
+ // arrange
+ Bounds3D b = Bounds3D.from(
+ Vector3D.of(0, 4, 8),
+ Vector3D.of(2, 6, 10));
+
+ // act
+ Parallelepiped p = b.toRegion(TEST_PRECISION);
+
+ // assert
+ Assert.assertEquals(8, p.getSize(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 5, 9), p.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void toRegion_boundingBoxTooSmall() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ Bounds3D.from(Vector3D.ZERO, Vector3D.of(1e-12, 1e-12, 1e-12))
+ .toRegion(TEST_PRECISION);
+ }, IllegalArgumentException.class);
+ }
+
+ @Test
+ public void testEq() {
+ // arrange
+ DoublePrecisionContext low = new EpsilonDoublePrecisionContext(1e-2);
+ DoublePrecisionContext high = new EpsilonDoublePrecisionContext(1e-10);
+
+ Bounds3D b1 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
+
+ Bounds3D b2 = Bounds3D.from(Vector3D.of(1.1, 1, 1), Vector3D.of(2, 2, 2));
+ Bounds3D b3 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(1.9, 2, 2));
+
+ Bounds3D b4 = Bounds3D.from(Vector3D.of(1.001, 1.001, 1.001), Vector3D.of(2.001, 2.001, 2.001));
+
+ // act/assert
+ Assert.assertTrue(b1.eq(b1, low));
+
+ Assert.assertFalse(b1.eq(b2, low));
+ Assert.assertFalse(b1.eq(b3, low));
+
+ Assert.assertTrue(b1.eq(b4, low));
+ Assert.assertTrue(b4.eq(b1, low));
+
+ Assert.assertFalse(b1.eq(b4, high));
+ Assert.assertFalse(b4.eq(b1, high));
+ }
+
+ @Test
+ public void testHashCode() {
+ // arrange
+ Bounds3D b1 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
+
+ Bounds3D b2 = Bounds3D.from(Vector3D.of(-2, 1, 1), Vector3D.of(2, 2, 2));
+ Bounds3D b3 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(3, 2, 2));
+ Bounds3D b4 = Bounds3D.from(Vector3D.of(1 + 1e-15, 1, 1), Vector3D.of(2, 2, 2));
+ Bounds3D b5 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2 + 1e-15, 2, 2));
+
+ Bounds3D b6 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
+
+ // act
+ int hash = b1.hashCode();
+
+ // assert
+ Assert.assertEquals(hash, b1.hashCode());
+
+ Assert.assertNotEquals(hash, b2.hashCode());
+ Assert.assertNotEquals(hash, b3.hashCode());
+ Assert.assertNotEquals(hash, b4.hashCode());
+ Assert.assertNotEquals(hash, b5.hashCode());
+
+ Assert.assertEquals(hash, b6.hashCode());
+ }
+
+ @Test
+ public void testEquals() {
+ // arrange
+ Bounds3D b1 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
+
+ Bounds3D b2 = Bounds3D.from(Vector3D.of(-1, 1, 1), Vector3D.of(2, 2, 2));
+ Bounds3D b3 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(3, 2, 2));
+ Bounds3D b4 = Bounds3D.from(Vector3D.of(1 + 1e-15, 1, 1), Vector3D.of(2, 2, 2));
+ Bounds3D b5 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2 + 1e-15, 2, 2));
+
+ Bounds3D b6 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
+
+ // act/assert
+ Assert.assertTrue(b1.equals(b1));
+
+ Assert.assertFalse(b1.equals(null));
+ Assert.assertFalse(b1.equals(new Object()));
+
+ Assert.assertFalse(b1.equals(b2));
+ Assert.assertFalse(b1.equals(b3));
+ Assert.assertFalse(b1.equals(b4));
+ Assert.assertFalse(b1.equals(b5));
+
+ Assert.assertTrue(b1.equals(b6));
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ Bounds3D b = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
+
+ // act
+ String str = b.toString();
+
+ // assert
+ GeometryTestUtils.assertContains("Bounds3D[min= (1", str);
+ GeometryTestUtils.assertContains(", max= (2", str);
+ }
+
+ @Test
+ public void testBuilder_addMethods() {
+ // arrange
+ Vector3D p1 = Vector3D.of(1, 10, 11);
+ Vector3D p2 = Vector3D.of(2, 9, 12);
+ Vector3D p3 = Vector3D.of(3, 8, 13);
+ Vector3D p4 = Vector3D.of(4, 7, 14);
+ Vector3D p5 = Vector3D.of(5, 6, 15);
+
+ // act
+ Bounds3D b = Bounds3D.builder()
+ .add(p1)
+ .addAll(Arrays.asList(p2, p3))
+ .add(Bounds3D.from(p4, p5))
+ .build();
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 6, 11), b.getMin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(5, 10, 15), b.getMax(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 8, 13), b.getBarycenter(), TEST_EPS);
+ }
+
+ @Test
+ public void testBuilder_containsBounds() {
+ // act/assert
+ Assert.assertFalse(Bounds3D.builder().containsBounds());
+
+ Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(Double.NaN, 1, 1)).containsBounds());
+ Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(1, Double.NaN, 1)).containsBounds());
+ Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(1, 1, Double.NaN)).containsBounds());
+
+ Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(Double.POSITIVE_INFINITY, 1, 1)).containsBounds());
+ Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(1, Double.POSITIVE_INFINITY, 1)).containsBounds());
+ Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(1, 1, Double.POSITIVE_INFINITY)).containsBounds());
+
+ Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(Double.NEGATIVE_INFINITY, 1, 1)).containsBounds());
+ Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(1, Double.NEGATIVE_INFINITY, 1)).containsBounds());
+ Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(1, 1, Double.NEGATIVE_INFINITY)).containsBounds());
+
+ Assert.assertTrue(Bounds3D.builder().add(Vector3D.ZERO).containsBounds());
+ }
+
+ private static void checkBounds(Bounds3D b, Vector3D min, Vector3D max) {
+ EuclideanTestUtils.assertCoordinatesEqual(min, b.getMin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(max, b.getMax(), TEST_EPS);
+ }
+
+ private static void assertContainsStrict(Bounds3D bounds, boolean contains, Vector3D... pts) {
+ for (Vector3D pt : pts) {
+ Assert.assertEquals("Unexpected location for point " + pt, contains, bounds.contains(pt));
+ }
+ }
+
+ private static void assertContainsWithPrecision(Bounds3D bounds, boolean contains, Vector3D... pts) {
+ for (Vector3D pt : pts) {
+ Assert.assertEquals("Unexpected location for point " + pt, contains, bounds.contains(pt, TEST_PRECISION));
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexVolumeTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexVolumeTest.java
index b6220de..a0a83c1 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexVolumeTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexVolumeTest.java
@@ -18,6 +18,7 @@ package org.apache.commons.geometry.euclidean.threed;
import java.util.Arrays;
import java.util.List;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.geometry.core.GeometryTestUtils;
@@ -69,8 +70,9 @@ public class ConvexVolumeTest {
Assert.assertEquals(1, boundaries.size());
PlaneConvexSubset sp = boundaries.get(0);
- Assert.assertEquals(0, sp.getSubspaceRegion().getBoundaries().size());
- Assert.assertSame(plane, sp.getPlane());
+ Assert.assertEquals(0, sp.getEmbedded().getSubspaceRegion().getBoundaries().size());
+ EuclideanTestUtils.assertCoordinatesEqual(plane.getOrigin(), sp.getPlane().getOrigin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(plane.getNormal(), sp.getPlane().getNormal(), TEST_EPS);
}
@Test
@@ -86,6 +88,97 @@ public class ConvexVolumeTest {
}
@Test
+ public void testTriangleStream_noBoundaries() {
+ // arrange
+ ConvexVolume full = ConvexVolume.full();
+
+ // act
+ List<Triangle3D> tris = full.triangleStream().collect(Collectors.toList());
+
+ // act/assert
+ Assert.assertEquals(0, tris.size());
+ }
+
+ @Test
+ public void testTriangleStream_infinite() {
+ // arrange
+ Pattern pattern = Pattern.compile("^Cannot convert infinite plane subset to triangles: .*");
+
+ ConvexVolume half = ConvexVolume.fromBounds(
+ Planes.fromNormal(Vector3D.Unit.MINUS_X, TEST_PRECISION)
+ );
+
+ ConvexVolume quadrant = ConvexVolume.fromBounds(
+ Planes.fromNormal(Vector3D.Unit.MINUS_X, TEST_PRECISION),
+ Planes.fromNormal(Vector3D.Unit.MINUS_Y, TEST_PRECISION),
+ Planes.fromNormal(Vector3D.Unit.MINUS_Z, TEST_PRECISION)
+ );
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ half.triangleStream().collect(Collectors.toList());
+ }, IllegalStateException.class, pattern);
+
+ GeometryTestUtils.assertThrows(() -> {
+ quadrant.triangleStream().collect(Collectors.toList());
+ }, IllegalStateException.class, pattern);
+ }
+
+ @Test
+ public void testTriangleStream_finite() {
+ // arrange
+ Vector3D min = Vector3D.ZERO;
+ Vector3D max = Vector3D.of(1, 1, 1);
+
+ ConvexVolume box = ConvexVolume.fromBounds(
+ Planes.fromPointAndNormal(min, Vector3D.Unit.MINUS_X, TEST_PRECISION),
+ Planes.fromPointAndNormal(min, Vector3D.Unit.MINUS_Y, TEST_PRECISION),
+ Planes.fromPointAndNormal(min, Vector3D.Unit.MINUS_Z, TEST_PRECISION),
+
+ Planes.fromPointAndNormal(max, Vector3D.Unit.PLUS_X, TEST_PRECISION),
+ Planes.fromPointAndNormal(max, Vector3D.Unit.PLUS_Y, TEST_PRECISION),
+ Planes.fromPointAndNormal(max, Vector3D.Unit.PLUS_Z, TEST_PRECISION)
+ );
+
+ // act
+ List<Triangle3D> tris = box.triangleStream().collect(Collectors.toList());
+
+ // assert
+ Assert.assertEquals(12, tris.size());
+
+ Bounds3D.Builder boundsBuilder = Bounds3D.builder();
+ tris.forEach(t -> boundsBuilder.addAll(t.getVertices()));
+
+ Bounds3D bounds = boundsBuilder.build();
+ EuclideanTestUtils.assertCoordinatesEqual(min, bounds.getMin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(max, bounds.getMax(), TEST_EPS);
+ }
+
+ @Test
+ public void testGetBounds_noBounds() {
+ // arrange
+ ConvexVolume full = ConvexVolume.full();
+ ConvexVolume halfFull = ConvexVolume.fromBounds(Planes.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION));
+
+ // act/assert
+ Assert.assertNull(full.getBounds());
+ Assert.assertNull(halfFull.getBounds());
+ }
+
+ @Test
+ public void testGetBounds_hasBounds() {
+ // arrange
+ ConvexVolume vol = rect(Vector3D.of(1, 1, 1), 0.5, 1, 2);
+
+ // act
+ Bounds3D bounds = vol.getBounds();
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0, -1), bounds.getMin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.5, 2, 3), bounds.getMax(), TEST_EPS);
+ }
+
+ @Test
public void testToTree_full() {
// arrange
ConvexVolume volume = ConvexVolume.full();
@@ -188,7 +281,7 @@ public class ConvexVolumeTest {
ConvexVolume vol = rect(Vector3D.ZERO, 0.5, 0.5, 0.5);
PlaneConvexSubset subplane = Planes.subsetFromConvexArea(
- Planes.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION), ConvexArea.full());
+ Planes.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION).getEmbedding(), ConvexArea.full());
// act
PlaneConvexSubset trimmed = vol.trim(subplane);
@@ -196,15 +289,13 @@ public class ConvexVolumeTest {
// assert
Assert.assertEquals(1, trimmed.getSize(), TEST_EPS);
- List<Vector3D> vertices = trimmed.getPlane().toSpace(
- trimmed.getSubspaceRegion().getBoundaryPaths().get(0).getVertexSequence());
+ List<Vector3D> vertices = trimmed.getVertices();
- Assert.assertEquals(5, vertices.size());
+ Assert.assertEquals(4, vertices.size());
EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0.5, -0.5), vertices.get(0), TEST_EPS);
EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0.5, 0.5), vertices.get(1), TEST_EPS);
EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, -0.5, 0.5), vertices.get(2), TEST_EPS);
EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, -0.5, -0.5), vertices.get(3), TEST_EPS);
- EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0.5, -0.5), vertices.get(4), TEST_EPS);
}
@Test
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedAreaPlaneConvexSubsetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedAreaPlaneConvexSubsetTest.java
new file mode 100644
index 0000000..e40c52c
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedAreaPlaneConvexSubsetTest.java
@@ -0,0 +1,468 @@
+/*
+ * 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.euclidean.threed;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
+import org.apache.commons.geometry.euclidean.twod.ConvexArea;
+import org.apache.commons.geometry.euclidean.twod.Lines;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+import org.apache.commons.geometry.euclidean.twod.path.LinePath;
+import org.apache.commons.geometry.euclidean.twod.shape.Parallelogram;
+import org.apache.commons.numbers.angle.PlaneAngleRadians;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class EmbeddedAreaPlaneConvexSubsetTest {
+
+ private static final double TEST_EPS = 1e-10;
+
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(TEST_EPS);
+
+ private static final EmbeddingPlane XY_PLANE_Z1 = Planes.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1),
+ Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+
+ @Test
+ public void testSpaceConversion() {
+ // arrange
+ EmbeddingPlane plane = Planes.fromPointAndPlaneVectors(Vector3D.of(1, 0, 0),
+ Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+ EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(plane, ConvexArea.full());
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2), ps.toSubspace(Vector3D.of(-5, 1, 2)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, -2, 4), ps.toSpace(Vector2D.of(-2, 4)), TEST_EPS);
+ }
+
+ @Test
+ public void testProperties_infinite() {
+ // arrange
+ ConvexArea area = ConvexArea.full();
+
+ // act
+ EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1, area);
+
+ // assert
+ Assert.assertTrue(ps.isFull());
+ Assert.assertFalse(ps.isEmpty());
+ Assert.assertFalse(ps.isFinite());
+ Assert.assertTrue(ps.isInfinite());
+
+ GeometryTestUtils.assertPositiveInfinity(ps.getSize());
+
+ Assert.assertSame(XY_PLANE_Z1, ps.getPlane());
+ Assert.assertSame(area, ps.getSubspaceRegion());
+
+ Assert.assertEquals(0, ps.getVertices().size());
+ }
+
+ @Test
+ public void testProperties_finite() {
+ // arrange
+ ConvexArea area = ConvexArea.convexPolygonFromPath(LinePath.builder(TEST_PRECISION)
+ .appendVertices(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(0, 1))
+ .build(true));
+
+ // act
+ EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1, area);
+
+ // assert
+ Assert.assertFalse(ps.isFull());
+ Assert.assertFalse(ps.isEmpty());
+ Assert.assertTrue(ps.isFinite());
+ Assert.assertFalse(ps.isInfinite());
+
+ Assert.assertEquals(0.5, ps.getSize(), TEST_EPS);
+
+ Assert.assertSame(XY_PLANE_Z1, ps.getPlane());
+ Assert.assertSame(area, ps.getSubspaceRegion());
+
+ EuclideanTestUtils.assertVertexLoopSequence(
+ Arrays.asList(Vector3D.of(0, 0, 1), Vector3D.of(1, 0, 1), Vector3D.of(0, 1, 1)),
+ ps.getVertices(), TEST_PRECISION);
+ }
+
+ @Test
+ public void testGetVertices_twoParallelLines() {
+ // arrange
+ EmbeddingPlane plane = Planes.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION).getEmbedding();
+ PlaneConvexSubset sp = new EmbeddedAreaPlaneConvexSubset(plane, ConvexArea.fromBounds(
+ Lines.fromPointAndAngle(Vector2D.of(0, 1), PlaneAngleRadians.PI, TEST_PRECISION),
+ Lines.fromPointAndAngle(Vector2D.of(0, -1), 0.0, TEST_PRECISION)
+ ));
+
+ // act
+ List<Vector3D> vertices = sp.getVertices();
+
+ // assert
+ Assert.assertEquals(0, vertices.size());
+ }
+
+ @Test
+ public void testGetVertices_infiniteWithVertices() {
+ // arrange
+ EmbeddingPlane plane = Planes.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+ PlaneConvexSubset sp = new EmbeddedAreaPlaneConvexSubset(plane, ConvexArea.fromBounds(
+ Lines.fromPointAndAngle(Vector2D.of(0, 1), PlaneAngleRadians.PI, TEST_PRECISION),
+ Lines.fromPointAndAngle(Vector2D.of(0, -1), 0.0, TEST_PRECISION),
+ Lines.fromPointAndAngle(Vector2D.of(1, 0), PlaneAngleRadians.PI_OVER_TWO, TEST_PRECISION)
+ ));
+
+ // act
+ List<Vector3D> vertices = sp.getVertices();
+
+ // assert
+ Assert.assertEquals(2, vertices.size());
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, -1, 1), vertices.get(0), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 1), vertices.get(1), TEST_EPS);
+ }
+ @Test
+ public void testToTriangles_infinite() {
+ // arrange
+ Pattern pattern = Pattern.compile("^Cannot convert infinite plane subset to triangles: .*");
+
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1, ConvexArea.full()).toTriangles();
+ }, IllegalStateException.class, pattern);
+
+ GeometryTestUtils.assertThrows(() -> {
+ ConvexArea area = ConvexArea.fromBounds(Lines.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION));
+ EmbeddedAreaPlaneConvexSubset halfSpace = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1, area);
+
+ halfSpace.toTriangles();
+ }, IllegalStateException.class, pattern);
+
+ GeometryTestUtils.assertThrows(() -> {
+ ConvexArea area = ConvexArea.fromBounds(
+ Lines.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION),
+ Lines.fromPointAndAngle(Vector2D.ZERO, 0.5 * Math.PI, TEST_PRECISION));
+
+ EmbeddedAreaPlaneConvexSubset halfSpaceWithVertices = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1, area);
+
+ halfSpaceWithVertices.toTriangles();
+ }, IllegalStateException.class, pattern);
+ }
+
+ @Test
+ public void testToTriangles_finite() {
+ // arrange
+ Vector3D p1 = Vector3D.of(0, 0, 1);
+ Vector3D p2 = Vector3D.of(1, 0, 1);
+ Vector3D p3 = Vector3D.of(2, 1, 1);
+ Vector3D p4 = Vector3D.of(1.5, 1, 1);
+
+ List<Vector2D> subPts = XY_PLANE_Z1.toSubspace(Arrays.asList(p1, p2, p3, p4));
+
+ EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+ ConvexArea.convexPolygonFromVertices(subPts, TEST_PRECISION));
+
+ // act
+ List<Triangle3D> tris = ps.toTriangles();
+
+ // assert
+ Assert.assertEquals(2, tris.size());
+
+ EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p4, p1, p2),
+ tris.get(0).getVertices(), TEST_PRECISION);
+ EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p4, p2, p3),
+ tris.get(1).getVertices(), TEST_PRECISION);
+ }
+
+ @Test
+ public void testClassify() {
+ // arrange
+ EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+ Parallelogram.builder(TEST_PRECISION)
+ .setPosition(Vector2D.of(2, 3))
+ .setScale(2, 2)
+ .build());
+
+ // act/assert
+ checkPoints(ps, RegionLocation.INSIDE, Vector3D.of(2, 3, 1));
+ checkPoints(ps, RegionLocation.BOUNDARY,
+ Vector3D.of(1, 3, 1), Vector3D.of(3, 3, 1),
+ Vector3D.of(2, 2, 1), Vector3D.of(2, 4, 1));
+ checkPoints(ps, RegionLocation.OUTSIDE,
+ Vector3D.of(2, 3, 0), Vector3D.of(2, 3, 2),
+ Vector3D.of(0, 3, 1), Vector3D.of(4, 3, 1),
+ Vector3D.of(2, 1, 1), Vector3D.of(2, 5, 1));
+ }
+
+ @Test
+ public void testClosest() {
+ // arrange
+ EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+ Parallelogram.builder(TEST_PRECISION)
+ .setPosition(Vector2D.of(2, 3))
+ .setScale(2, 2)
+ .build());
+
+ // act/assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 3, 1), ps.closest(Vector3D.of(2, 3, 1)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 3, 1), ps.closest(Vector3D.of(2, 3, 100)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 1),
+ ps.closest(Vector3D.of(-100, -100, -100)), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 3.5, 1),
+ ps.closest(Vector3D.of(100, 3.5, 100)), TEST_EPS);
+ }
+
+ @Test
+ public void testGetBounds_noBounds() {
+ // arrange
+ EmbeddingPlane plane = Planes.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1),
+ Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X, TEST_PRECISION);
+
+ EmbeddedAreaPlaneConvexSubset full = new EmbeddedAreaPlaneConvexSubset(plane, ConvexArea.full());
+ EmbeddedAreaPlaneConvexSubset halfPlane = new EmbeddedAreaPlaneConvexSubset(plane,
+ ConvexArea.fromBounds(Lines.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION)));
+
+ // act/assert
+ Assert.assertNull(full.getBounds());
+ Assert.assertNull(halfPlane.getBounds());
+ }
+
+ @Test
+ public void testGetBounds_hasBounds() {
+ // arrange
+ EmbeddingPlane plane = Planes.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1),
+ Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X, TEST_PRECISION);
+
+ EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(plane,
+ ConvexArea.convexPolygonFromVertices(Arrays.asList(
+ Vector2D.of(1, 1), Vector2D.of(2, 1), Vector2D.of(1, 2)
+ ), TEST_PRECISION));
+
+ // act
+ Bounds3D bounds = ps.getBounds();
+
+ // assert
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-2, 1, 1), bounds.getMin(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 2, 1), bounds.getMax(), TEST_EPS);
+ }
+
+ @Test
+ public void testTransform() {
+ // arrange
+ AffineTransformMatrix3D t = AffineTransformMatrix3D.identity()
+ .rotate(QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, -PlaneAngleRadians.PI_OVER_TWO))
+ .scale(1, 1, 2)
+ .translate(Vector3D.of(1, 0, 0));
+
+ EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+ Parallelogram.builder(TEST_PRECISION)
+ .setPosition(Vector2D.of(2, 3))
+ .setScale(2, 2)
+ .build());
+
+ // act
+ EmbeddedAreaPlaneConvexSubset result = ps.transform(t);
+
+ // assert
+ Assert.assertFalse(result.isFull());
+ Assert.assertFalse(result.isEmpty());
+ Assert.assertTrue(result.isFinite());
+ Assert.assertFalse(result.isInfinite());
+
+ Assert.assertEquals(8, result.getSize(), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.MINUS_X, result.getPlane().getNormal(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Z, result.getPlane().getU(), TEST_EPS);
+
+ EuclideanTestUtils.assertVertexLoopSequence(
+ Arrays.asList(Vector3D.of(0, 2, 2), Vector3D.of(0, 2, 6), Vector3D.of(0, 4, 6), Vector3D.of(0, 4, 2)),
+ result.getVertices(), TEST_PRECISION);
+ }
+
+ @Test
+ public void testReverse() {
+ // arrange
+ EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+ Parallelogram.builder(TEST_PRECISION)
+ .setPosition(Vector2D.of(2, 3))
+ .setScale(2, 2)
+ .build());
+
+ // act
+ EmbeddedAreaPlaneConvexSubset result = ps.reverse();
+
+ // assert
+ Assert.assertFalse(result.isFull());
+ Assert.assertFalse(result.isEmpty());
+ Assert.assertTrue(result.isFinite());
+ Assert.assertFalse(result.isInfinite());
+
+ Assert.assertEquals(4, result.getSize(), TEST_EPS);
+
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.MINUS_Z, result.getPlane().getNormal(), TEST_EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Y, result.getPlane().getU(), TEST_EPS);
+
+ EuclideanTestUtils.assertVertexLoopSequence(
+ Arrays.asList(Vector3D.of(1, 4, 1), Vector3D.of(3, 4, 1), Vector3D.of(3, 2, 1), Vector3D.of(1, 2, 1)),
+ result.getVertices(), TEST_PRECISION);
+ }
+
+ @Test
+ public void testSplit_plus() {
+ // arrange
+ EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+ ConvexArea.convexPolygonFromVertices(Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(0, 1)),
+ TEST_PRECISION));
+
+ Plane splitter = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+ // act
+ Split<PlaneConvexSubset> split = ps.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertSame(ps, split.getPlus());
+ }
+
+ @Test
+ public void testSplit_minus() {
+ // arrange
+ EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+ ConvexArea.convexPolygonFromVertices(Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(0, 1)),
+ TEST_PRECISION));
+
+ Plane splitter = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.MINUS_Z, TEST_PRECISION);
+
+ // act
+ Split<PlaneConvexSubset> split = ps.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+ Assert.assertSame(ps, split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_both() {
+ // arrange
+ EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+ ConvexArea.convexPolygonFromVertices(Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(0, 1)),
+ TEST_PRECISION));
+
+ Plane splitter = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.of(-1, 1, 0), TEST_PRECISION);
+
+ // act
+ Split<PlaneConvexSubset> split = ps.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ PlaneConvexSubset minus = split.getMinus();
+ EuclideanTestUtils.assertVertexLoopSequence(
+ Arrays.asList(Vector3D.of(0, 0, 1), Vector3D.of(1, 0, 1), Vector3D.of(0.5, 0.5, 1)),
+ minus.getVertices(), TEST_PRECISION);
+
+ PlaneConvexSubset plus = split.getPlus();
+ EuclideanTestUtils.assertVertexLoopSequence(
+ Arrays.asList(Vector3D.of(0, 0, 1), Vector3D.of(0.5, 0.5, 1), Vector3D.of(0, 1, 1)),
+ plus.getVertices(), TEST_PRECISION);
+ }
+
+ @Test
+ public void testSplit_neither() {
+ // arrange
+ EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+ ConvexArea.convexPolygonFromVertices(Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(0, 1)),
+ TEST_PRECISION));
+
+ Plane splitter = Planes.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0, 1e-15, -1), TEST_PRECISION);
+
+ // act
+ Split<PlaneConvexSubset> split = ps.split(splitter);
+
+ // assert
+ Assert.assertEquals(SplitLocation.NEITHER, split.getLocation());
+
+ Assert.assertNull(split.getMinus());
+ Assert.assertNull(split.getPlus());
+ }
+
+ @Test
+ public void testSplit_usesVertexBasedSubsetsWhenPossible() {
+ // arrange
+ // create an infinite subset
+ EmbeddingPlane plane = Planes.fromPointAndPlaneVectors(Vector3D.ZERO,
+ Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+ EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(plane, ConvexArea.fromBounds(
+ Lines.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION),
+ Lines.fromPointAndAngle(Vector2D.of(1, 0), PlaneAngleRadians.PI_OVER_TWO, TEST_PRECISION),
+ Lines.fromPointAndAngle(Vector2D.of(0, 1), -PlaneAngleRadians.PI_OVER_TWO, TEST_PRECISION)
+ ));
+
+ Plane splitter = Planes.fromPointAndNormal(Vector3D.of(0.5, 0.5, 0), Vector3D.of(-1, 1, 0), TEST_PRECISION);
+
+ // act
+ Split<PlaneConvexSubset> split = ps.split(splitter);
+
+ // assert
+ Assert.assertTrue(ps.isInfinite());
+
+ Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+ PlaneConvexSubset plus = split.getPlus();
+ Assert.assertNotNull(plus);
+ Assert.assertTrue(plus.isInfinite());
+ Assert.assertTrue(plus instanceof EmbeddedAreaPlaneConvexSubset);
+
+ PlaneConvexSubset minus = split.getMinus();
+ Assert.assertNotNull(minus);
+ Assert.assertFalse(minus.isInfinite());
+ Assert.assertTrue(minus instanceof SimpleTriangle3D);
+ }
+
+ @Test
+ public void testToString() {
+ // arrange
+ EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+ ConvexArea.convexPolygonFromVertices(Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(0, 1)),
+ TEST_PRECISION));
+
+ // act
+ String str = ps.toString();
+
+ // assert
+ GeometryTestUtils.assertContains("EmbeddedAreaPlaneConvexSubset[plane= EmbeddingPlane[", str);
+ GeometryTestUtils.assertContains("subspaceRegion= ConvexArea[", str);
+ }
+
+ private static void checkPoints(EmbeddedAreaPlaneConvexSubset ps, RegionLocation loc, Vector3D... pts) {
+ for (Vector3D pt : pts) {
+ Assert.assertEquals("Unexpected location for point " + pt, loc, ps.classify(pt));
+ }
+ }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubsetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubsetTest.java
index 5860ac3..41dca76 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubsetTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubsetTest.java
@@ -18,6 +18,7 @@ package org.apache.commons.geometry.euclidean.threed;
import java.util.Arrays;
import java.util.List;
+import java.util.regex.Pattern;
... 6899 lines suppressed ...