1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.geometry.euclidean.threed;
18
19 import java.util.Arrays;
20 import java.util.Collections;
21 import java.util.List;
22 import java.util.stream.Stream;
23
24 import org.apache.commons.geometry.core.Transform;
25 import org.apache.commons.geometry.core.partitioning.AbstractConvexHyperplaneBoundedRegion;
26 import org.apache.commons.geometry.core.partitioning.Hyperplane;
27 import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
28 import org.apache.commons.geometry.core.partitioning.Split;
29
30 /** Class representing a finite or infinite convex volume in Euclidean 3D space.
31 * The boundaries of this area, if any, are composed of plane convex subsets.
32 */
33 public class ConvexVolume extends AbstractConvexHyperplaneBoundedRegion<Vector3D, PlaneConvexSubset>
34 implements BoundarySource3D {
35
36 /** Instance representing the full 3D volume. */
37 private static final ConvexVolume FULL = new ConvexVolume(Collections.emptyList());
38
39 /** Simple constructor. Callers are responsible for ensuring that the given path
40 * represents the boundary of a convex area. No validation is performed.
41 * @param boundaries the boundaries of the convex area
42 */
43 protected ConvexVolume(final List<PlaneConvexSubset> boundaries) {
44 super(boundaries);
45 }
46
47 /** {@inheritDoc} */
48 @Override
49 public Stream<PlaneConvexSubset> boundaryStream() {
50 return getBoundaries().stream();
51 }
52
53 /** {@inheritDoc} */
54 @Override
55 public double getSize() {
56 if (isFull()) {
57 return Double.POSITIVE_INFINITY;
58 }
59
60 double volumeSum = 0.0;
61
62 for (final PlaneConvexSubset boundary : getBoundaries()) {
63 if (boundary.isInfinite()) {
64 return Double.POSITIVE_INFINITY;
65 }
66
67 final Plane boundaryPlane = boundary.getPlane();
68 final double boundaryArea = boundary.getSize();
69 final Vector3D boundaryCentroid = boundary.getCentroid();
70
71 volumeSum += boundaryArea * boundaryCentroid.dot(boundaryPlane.getNormal());
72 }
73
74 return volumeSum / 3.0;
75 }
76
77 /** {@inheritDoc} */
78 @Override
79 public Vector3D getCentroid() {
80 double volumeSum = 0.0;
81
82 double sumX = 0.0;
83 double sumY = 0.0;
84 double sumZ = 0.0;
85
86 for (final PlaneConvexSubset boundary : getBoundaries()) {
87 if (boundary.isInfinite()) {
88 return null;
89 }
90
91 final Plane boundaryPlane = boundary.getPlane();
92 final double boundaryArea = boundary.getSize();
93 final Vector3D boundaryCentroid = boundary.getCentroid();
94
95 final double scaledVolume = boundaryArea * boundaryCentroid.dot(boundaryPlane.getNormal());
96
97 volumeSum += scaledVolume;
98
99 sumX += scaledVolume * boundaryCentroid.getX();
100 sumY += scaledVolume * boundaryCentroid.getY();
101 sumZ += scaledVolume * boundaryCentroid.getZ();
102 }
103
104 if (volumeSum > 0) {
105 final double size = volumeSum / 3.0;
106
107 // Since the volume we used when adding together the boundary contributions
108 // was 3x the actual pyramid size, we'll multiply by 1/4 here instead
109 // of 3/4 to adjust for the actual centroid position in each pyramid.
110 final double centroidScale = 1.0 / (4 * size);
111 return Vector3D.of(
112 sumX * centroidScale,
113 sumY * centroidScale,
114 sumZ * centroidScale);
115 }
116
117 return null;
118 }
119
120 /** {@inheritDoc} */
121 @Override
122 public Split<ConvexVolume> split(final Hyperplane<Vector3D> splitter) {
123 return splitInternal(splitter, this, PlaneConvexSubset.class, ConvexVolume::new);
124 }
125
126 /** {@inheritDoc} */
127 @Override
128 public RegionBSPTree3D toTree() {
129 return RegionBSPTree3D.from(getBoundaries(), true);
130 }
131
132 /** {@inheritDoc} */
133 @Override
134 public PlaneConvexSubset trim(final HyperplaneConvexSubset<Vector3D> convexSubset) {
135 return (PlaneConvexSubset) super.trim(convexSubset);
136 }
137
138 /** Return a new instance transformed by the argument.
139 * @param transform transform to apply
140 * @return a new instance transformed by the argument
141 */
142 public ConvexVolume transform(final Transform<Vector3D> transform) {
143 return transformInternal(transform, this, PlaneConvexSubset.class, ConvexVolume::new);
144 }
145
146 /** Return an instance representing the full 3D volume.
147 * @return an instance representing the full 3D volume.
148 */
149 public static ConvexVolume full() {
150 return FULL;
151 }
152
153 /** Create a convex volume formed by the intersection of the negative half-spaces of the
154 * given bounding planes. The returned instance represents the volume that is on the
155 * minus side of all of the given plane. Note that this method does not support volumes
156 * of zero size (ie, infinitely thin volumes or points.)
157 * @param planes planes used to define the convex area
158 * @return a new convex volume instance representing the volume on the minus side of all
159 * of the bounding plane or an instance representing the full space if the collection
160 * is empty
161 * @throws IllegalArgumentException if the given set of bounding planes do not form a convex volume,
162 * meaning that there is no region that is on the minus side of all of the bounding planes.
163 */
164 public static ConvexVolume fromBounds(final Plane... planes) {
165 return fromBounds(Arrays.asList(planes));
166 }
167
168 /** Create a convex volume formed by the intersection of the negative half-spaces of the
169 * given bounding planes. The returned instance represents the volume that is on the
170 * minus side of all of the given plane. Note that this method does not support volumes
171 * of zero size (ie, infinitely thin volumes or points.)
172 * @param boundingPlanes planes used to define the convex area
173 * @return a new convex volume instance representing the volume on the minus side of all
174 * of the bounding plane or an instance representing the full space if the collection
175 * is empty
176 * @throws IllegalArgumentException if the given set of bounding planes do not form a convex volume,
177 * meaning that there is no region that is on the minus side of all of the bounding planes.
178 */
179 public static ConvexVolume fromBounds(final Iterable<? extends Plane> boundingPlanes) {
180 final List<PlaneConvexSubset> facets = new ConvexRegionBoundaryBuilder<>(PlaneConvexSubset.class)
181 .build(boundingPlanes);
182 return facets.isEmpty() ? full() : new ConvexVolume(facets);
183 }
184 }