| /* |
| * 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.text.MessageFormat; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.List; |
| |
| import org.apache.commons.geometry.core.Point; |
| import org.apache.commons.geometry.core.partitioning.AbstractRegion; |
| import org.apache.commons.geometry.core.partitioning.BSPTree; |
| import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor; |
| import org.apache.commons.geometry.core.partitioning.BoundaryAttribute; |
| import org.apache.commons.geometry.core.partitioning.Hyperplane; |
| import org.apache.commons.geometry.core.partitioning.Region; |
| import org.apache.commons.geometry.core.partitioning.RegionFactory; |
| import org.apache.commons.geometry.core.partitioning.SubHyperplane; |
| import org.apache.commons.geometry.core.partitioning.Transform; |
| import org.apache.commons.geometry.core.precision.DoublePrecisionContext; |
| import org.apache.commons.geometry.euclidean.oned.Vector1D; |
| import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation; |
| import org.apache.commons.geometry.euclidean.twod.PolygonsSet; |
| import org.apache.commons.geometry.euclidean.twod.SubLine; |
| import org.apache.commons.geometry.euclidean.twod.Vector2D; |
| |
| /** This class represents a 3D region: a set of polyhedrons. |
| */ |
| public class PolyhedronsSet extends AbstractRegion<Vector3D, Vector2D> { |
| |
| /** Build a polyhedrons set representing the whole real line. |
| * @param precision precision context used to compare floating point values |
| */ |
| public PolyhedronsSet(final DoublePrecisionContext precision) { |
| super(precision); |
| } |
| |
| /** Build a polyhedrons set from a BSP tree. |
| * <p>The leaf nodes of the BSP tree <em>must</em> have a |
| * {@code Boolean} attribute representing the inside status of |
| * the corresponding cell (true for inside cells, false for outside |
| * cells). In order to avoid building too many small objects, it is |
| * recommended to use the predefined constants |
| * {@code Boolean.TRUE} and {@code Boolean.FALSE}</p> |
| * <p> |
| * This constructor is aimed at expert use, as building the tree may |
| * be a difficult task. It is not intended for general use and for |
| * performances reasons does not check thoroughly its input, as this would |
| * require walking the full tree each time. Failing to provide a tree with |
| * the proper attributes, <em>will</em> therefore generate problems like |
| * {@link NullPointerException} or {@link ClassCastException} only later on. |
| * This limitation is known and explains why this constructor is for expert |
| * use only. The caller does have the responsibility to provided correct arguments. |
| * </p> |
| * @param tree inside/outside BSP tree representing the region |
| * @param precision precision context used to compare floating point values |
| */ |
| public PolyhedronsSet(final BSPTree<Vector3D> tree, final DoublePrecisionContext precision) { |
| super(tree, precision); |
| } |
| |
| /** Build a polyhedrons set from a Boundary REPresentation (B-rep) specified by sub-hyperplanes. |
| * <p>The boundary is provided as a collection of {@link |
| * SubHyperplane sub-hyperplanes}. Each sub-hyperplane has the |
| * interior part of the region on its minus side and the exterior on |
| * its plus side.</p> |
| * <p>The boundary elements can be in any order, and can form |
| * several non-connected sets (like for example polyhedrons with holes |
| * or a set of disjoint polyhedrons considered as a whole). In |
| * fact, the elements do not even need to be connected together |
| * (their topological connections are not used here). However, if the |
| * boundary does not really separate an inside open from an outside |
| * open (open having here its topological meaning), then subsequent |
| * calls to the {@link Region#checkPoint(Point) checkPoint} method will |
| * not be meaningful anymore.</p> |
| * <p>If the boundary is empty, the region will represent the whole |
| * space.</p> |
| * @param boundary collection of boundary elements, as a |
| * collection of {@link SubHyperplane SubHyperplane} objects |
| * @param precision precision context used to compare floating point values |
| */ |
| public PolyhedronsSet(final Collection<SubHyperplane<Vector3D>> boundary, |
| final DoublePrecisionContext precision) { |
| super(boundary, precision); |
| } |
| |
| /** Build a polyhedrons set from a Boundary REPresentation (B-rep) specified by connected vertices. |
| * <p> |
| * The boundary is provided as a list of vertices and a list of facets. |
| * Each facet is specified as an integer array containing the arrays vertices |
| * indices in the vertices list. Each facet normal is oriented by right hand |
| * rule to the facet vertices list. |
| * </p> |
| * <p> |
| * Some basic sanity checks are performed but not everything is thoroughly |
| * assessed, so it remains under caller responsibility to ensure the vertices |
| * and facets are consistent and properly define a polyhedrons set. |
| * </p> |
| * @param vertices list of polyhedrons set vertices |
| * @param facets list of facets, as vertices indices in the vertices list |
| * @param precision precision context used to compare floating point values |
| * @exception IllegalArgumentException if some basic sanity checks fail |
| */ |
| public PolyhedronsSet(final List<Vector3D> vertices, final List<int[]> facets, |
| final DoublePrecisionContext precision) { |
| super(buildBoundary(vertices, facets, precision), precision); |
| } |
| |
| /** Build a parallellepipedic box. |
| * @param xMin low bound along the x direction |
| * @param xMax high bound along the x direction |
| * @param yMin low bound along the y direction |
| * @param yMax high bound along the y direction |
| * @param zMin low bound along the z direction |
| * @param zMax high bound along the z direction |
| * @param precision precision context used to compare floating point values |
| */ |
| public PolyhedronsSet(final double xMin, final double xMax, |
| final double yMin, final double yMax, |
| final double zMin, final double zMax, |
| final DoublePrecisionContext precision) { |
| super(buildBoundary(xMin, xMax, yMin, yMax, zMin, zMax, precision), precision); |
| } |
| |
| /** Build a parallellepipedic box boundary. |
| * @param xMin low bound along the x direction |
| * @param xMax high bound along the x direction |
| * @param yMin low bound along the y direction |
| * @param yMax high bound along the y direction |
| * @param zMin low bound along the z direction |
| * @param zMax high bound along the z direction |
| * @param precision precision context used to compare floating point values |
| * @return boundary tree |
| */ |
| private static BSPTree<Vector3D> buildBoundary(final double xMin, final double xMax, |
| final double yMin, final double yMax, |
| final double zMin, final double zMax, |
| final DoublePrecisionContext precision) { |
| if (precision.eq(xMin, xMax) || |
| precision.eq(yMin, yMax) || |
| precision.eq(zMin, zMax)) { |
| // too thin box, build an empty polygons set |
| return new BSPTree<>(Boolean.FALSE); |
| } |
| final Plane pxMin = Plane.fromPointAndNormal(Vector3D.of(xMin, 0, 0), Vector3D.Unit.MINUS_X, precision); |
| final Plane pxMax = Plane.fromPointAndNormal(Vector3D.of(xMax, 0, 0), Vector3D.Unit.PLUS_X, precision); |
| final Plane pyMin = Plane.fromPointAndNormal(Vector3D.of(0, yMin, 0), Vector3D.Unit.MINUS_Y, precision); |
| final Plane pyMax = Plane.fromPointAndNormal(Vector3D.of(0, yMax, 0), Vector3D.Unit.PLUS_Y, precision); |
| final Plane pzMin = Plane.fromPointAndNormal(Vector3D.of(0, 0, zMin), Vector3D.Unit.MINUS_Z, precision); |
| final Plane pzMax = Plane.fromPointAndNormal(Vector3D.of(0, 0, zMax), Vector3D.Unit.PLUS_Z, precision); |
| final Region<Vector3D> boundary = |
| new RegionFactory<Vector3D>().buildConvex(pxMin, pxMax, pyMin, pyMax, pzMin, pzMax); |
| return boundary.getTree(false); |
| } |
| |
| /** Build boundary from vertices and facets. |
| * @param vertices list of polyhedrons set vertices |
| * @param facets list of facets, as vertices indices in the vertices list |
| * @param precision precision context used to compare floating point values |
| * @return boundary as a list of sub-hyperplanes |
| * @exception IllegalArgumentException if some basic sanity checks fail |
| */ |
| private static List<SubHyperplane<Vector3D>> buildBoundary(final List<Vector3D> vertices, |
| final List<int[]> facets, |
| final DoublePrecisionContext precision) { |
| |
| // check vertices distances |
| for (int i = 0; i < vertices.size() - 1; ++i) { |
| final Vector3D vi = vertices.get(i); |
| for (int j = i + 1; j < vertices.size(); ++j) { |
| if (precision.eqZero(vi.distance(vertices.get(j)))) { |
| throw new IllegalArgumentException("Vertices are too close near point " + vi); |
| } |
| } |
| } |
| |
| // find how vertices are referenced by facets |
| final int[][] references = findReferences(vertices, facets); |
| |
| // find how vertices are linked together by edges along the facets they belong to |
| final int[][] successors = successors(vertices, facets, references); |
| |
| // check edges orientations |
| for (int vA = 0; vA < vertices.size(); ++vA) { |
| for (final int vB : successors[vA]) { |
| |
| if (vB >= 0) { |
| // when facets are properly oriented, if vB is the successor of vA on facet f1, |
| // then there must be an adjacent facet f2 where vA is the successor of vB |
| boolean found = false; |
| for (final int v : successors[vB]) { |
| found = found || (v == vA); |
| } |
| if (!found) { |
| final Vector3D start = vertices.get(vA); |
| final Vector3D end = vertices.get(vB); |
| throw new IllegalArgumentException(MessageFormat.format("Edge joining points {0} and {1} is connected to one facet only", start, end)); |
| } |
| } |
| } |
| } |
| |
| final List<SubHyperplane<Vector3D>> boundary = new ArrayList<>(); |
| final List<Vector3D> facetVertices = new ArrayList<>(); |
| |
| for (final int[] facet : facets) { |
| |
| for (int i = 0; i < facet.length; ++i) { |
| facetVertices.add(vertices.get(facet[i])); |
| } |
| |
| Plane plane = Plane.fromPoints(facetVertices, precision); |
| |
| // convert to subspace points |
| final Vector2D[] two2Points = new Vector2D[facet.length]; |
| for (int i = 0 ; i < facet.length; ++i) { |
| two2Points[i] = plane.toSubSpace(facetVertices.get(i)); |
| } |
| |
| // create the polygonal facet |
| boundary.add(new SubPlane(plane, new PolygonsSet(precision, two2Points))); |
| |
| facetVertices.clear(); |
| } |
| |
| return boundary; |
| } |
| |
| /** Find the facets that reference each edges. |
| * @param vertices list of polyhedrons set vertices |
| * @param facets list of facets, as vertices indices in the vertices list |
| * @return references array such that r[v][k] = f for some k if facet f contains vertex v |
| * @exception IllegalArgumentException if some facets have fewer than 3 vertices |
| */ |
| private static int[][] findReferences(final List<Vector3D> vertices, final List<int[]> facets) { |
| |
| // find the maximum number of facets a vertex belongs to |
| final int[] nbFacets = new int[vertices.size()]; |
| int maxFacets = 0; |
| for (final int[] facet : facets) { |
| if (facet.length < 3) { |
| throw new IllegalArgumentException("3 points are required, got only " + facet.length); |
| } |
| for (final int index : facet) { |
| maxFacets = Math.max(maxFacets, ++nbFacets[index]); |
| } |
| } |
| |
| // set up the references array |
| final int[][] references = new int[vertices.size()][maxFacets]; |
| for (int[] r : references) { |
| Arrays.fill(r, -1); |
| } |
| for (int f = 0; f < facets.size(); ++f) { |
| for (final int v : facets.get(f)) { |
| // vertex v is referenced by facet f |
| int k = 0; |
| while (k < maxFacets && |
| references[v][k] >= 0) { |
| ++k; |
| } |
| references[v][k] = f; |
| } |
| } |
| |
| return references; |
| } |
| |
| /** Find the successors of all vertices among all facets they belong to. |
| * @param vertices list of polyhedrons set vertices |
| * @param facets list of facets, as vertices indices in the vertices list |
| * @param references facets references array |
| * @return indices of vertices that follow vertex v in some facet (the array |
| * may contain extra entries at the end, set to negative indices) |
| * @exception IllegalArgumentException if the same vertex appears more than |
| * once in the successors list (which means one facet orientation is wrong) |
| |
| */ |
| private static int[][] successors(final List<Vector3D> vertices, final List<int[]> facets, |
| final int[][] references) { |
| |
| // create an array large enough |
| final int[][] successors = new int[vertices.size()][references[0].length]; |
| for (final int[] s : successors) { |
| Arrays.fill(s, -1); |
| } |
| |
| for (int v = 0; v < vertices.size(); ++v) { |
| for (int k = 0; k < successors[v].length && references[v][k] >= 0; ++k) { |
| |
| // look for vertex v |
| final int[] facet = facets.get(references[v][k]); |
| int i = 0; |
| while (i < facet.length && |
| facet[i] != v) { |
| ++i; |
| } |
| |
| // we have found vertex v, we deduce its successor on current facet |
| successors[v][k] = facet[(i + 1) % facet.length]; |
| for (int l = 0; l < k; ++l) { |
| if (successors[v][l] == successors[v][k]) { |
| final Vector3D start = vertices.get(v); |
| final Vector3D end = vertices.get(successors[v][k]); |
| throw new IllegalArgumentException(MessageFormat.format("Facet orientation mismatch around edge joining points {0} and {1}", start, end)); |
| } |
| } |
| } |
| } |
| |
| return successors; |
| |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public PolyhedronsSet buildNew(final BSPTree<Vector3D> tree) { |
| return new PolyhedronsSet(tree, getPrecision()); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| protected void computeGeometricalProperties() { |
| // check simple cases first |
| if (isEmpty()) { |
| setSize(0); |
| setBarycenter(Vector3D.NaN); |
| } |
| else if (isFull()) { |
| setSize(Double.POSITIVE_INFINITY); |
| setBarycenter(Vector3D.NaN); |
| } |
| else { |
| // not empty or full; compute the contribution of all boundary facets |
| final FacetsContributionVisitor contributionVisitor = new FacetsContributionVisitor(); |
| getTree(true).visit(contributionVisitor); |
| |
| final double size = contributionVisitor.getSize(); |
| final Vector3D barycenter = contributionVisitor.getBarycenter(); |
| |
| if (size < 0) { |
| // the polyhedrons set is a finite outside surrounded by an infinite inside |
| setSize(Double.POSITIVE_INFINITY); |
| setBarycenter(Vector3D.NaN); |
| } else { |
| // the polyhedrons set is finite |
| setSize(size); |
| setBarycenter(barycenter); |
| } |
| } |
| } |
| |
| /** Visitor computing polyhedron geometrical properties. |
| * The volume of the polyhedron is computed using the equation |
| * <code>V = (1/3)*Σ<sub>F</sub>[(C<sub>F</sub>⋅N<sub>F</sub>)*area(F)]</code>, |
| * where <code>F</code> represents each face in the polyhedron, <code>C<sub>F</sub></code> |
| * represents the barycenter of the face, and <code>N<sub>F</sub></code> represents the |
| * normal of the face. (More details can be found in the article |
| * <a href="https://en.wikipedia.org/wiki/Polyhedron#Volume">here</a>.) |
| * This essentially splits up the polyhedron into pyramids with a polyhedron |
| * face forming the base of each pyramid. |
| * The barycenter is computed in a similar way. The barycenter of each pyramid |
| * is calculated using the fact that it is located 3/4 of the way along the |
| * line from the apex to the base. The polyhedron barycenter then becomes |
| * the volume-weighted average of these pyramid centers. |
| */ |
| private static class FacetsContributionVisitor implements BSPTreeVisitor<Vector3D> { |
| |
| /** Accumulator for facet volume contributions. */ |
| private double volumeSum; |
| |
| /** Accumulator for barycenter contributions. */ |
| private Vector3D barycenterSum = Vector3D.ZERO; |
| |
| /** Returns the total computed size (ie, volume) of the polyhedron. |
| * This value will be negative if the polyhedron is "inside-out", meaning |
| * that it has a finite outside surrounded by an infinite inside. |
| * @return the volume. |
| */ |
| public double getSize() { |
| // apply the 1/3 pyramid volume scaling factor |
| return volumeSum / 3; |
| } |
| |
| /** Returns the computed barycenter. This is the volume-weighted average |
| * of contributions from all facets. All coordinates will be NaN if the |
| * region is infinite. |
| * @return the barycenter. |
| */ |
| public Vector3D getBarycenter() { |
| // Since the volume we used when adding together the facet contributions |
| // was 3x the actual pyramid size, we'll multiply by 1/4 here instead |
| // of 3/4 to adjust for the actual barycenter position in each pyramid. |
| return Vector3D.linearCombination(1.0 / (4 * getSize()), barycenterSum); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Order visitOrder(final BSPTree<Vector3D> node) { |
| return Order.MINUS_SUB_PLUS; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void visitInternalNode(final BSPTree<Vector3D> node) { |
| @SuppressWarnings("unchecked") |
| final BoundaryAttribute<Vector3D> attribute = |
| (BoundaryAttribute<Vector3D>) node.getAttribute(); |
| if (attribute.getPlusOutside() != null) { |
| addContribution(attribute.getPlusOutside(), false); |
| } |
| if (attribute.getPlusInside() != null) { |
| addContribution(attribute.getPlusInside(), true); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void visitLeafNode(final BSPTree<Vector3D> node) {} |
| |
| /** Add the contribution of a boundary facet. |
| * @param facet boundary facet |
| * @param reversed if true, the facet has the inside on its plus side |
| */ |
| private void addContribution(final SubHyperplane<Vector3D> facet, final boolean reversed) { |
| |
| final Region<Vector2D> polygon = ((SubPlane) facet).getRemainingRegion(); |
| final double area = polygon.getSize(); |
| |
| if (Double.isInfinite(area)) { |
| volumeSum = Double.POSITIVE_INFINITY; |
| barycenterSum = Vector3D.NaN; |
| } else { |
| final Plane plane = (Plane) facet.getHyperplane(); |
| final Vector3D facetBarycenter = plane.toSpace(polygon.getBarycenter()); |
| |
| // 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()); |
| if (reversed) { |
| scaledVolume = -scaledVolume; |
| } |
| |
| volumeSum += scaledVolume; |
| barycenterSum = Vector3D.linearCombination(1.0, barycenterSum, scaledVolume, facetBarycenter); |
| } |
| } |
| } |
| |
| /** Get the first sub-hyperplane crossed by a semi-infinite line. |
| * @param point start point of the part of the line considered |
| * @param line line to consider (contains point) |
| * @return the first sub-hyperplane crossed by the line after the |
| * given point, or null if the line does not intersect any |
| * sub-hyperplane |
| */ |
| public SubHyperplane<Vector3D> firstIntersection(final Vector3D point, final Line line) { |
| return recurseFirstIntersection(getTree(true), point, line); |
| } |
| |
| /** Get the first sub-hyperplane crossed by a semi-infinite line. |
| * @param node current node |
| * @param point start point of the part of the line considered |
| * @param line line to consider (contains point) |
| * @return the first sub-hyperplane crossed by the line after the |
| * given point, or null if the line does not intersect any |
| * sub-hyperplane |
| */ |
| private SubHyperplane<Vector3D> recurseFirstIntersection(final BSPTree<Vector3D> node, |
| final Vector3D point, |
| final Line line) { |
| |
| final SubHyperplane<Vector3D> cut = node.getCut(); |
| if (cut == null) { |
| return null; |
| } |
| final BSPTree<Vector3D> minus = node.getMinus(); |
| final BSPTree<Vector3D> plus = node.getPlus(); |
| final Plane plane = (Plane) cut.getHyperplane(); |
| |
| // establish search order |
| final double offset = plane.getOffset(point); |
| final boolean in = getPrecision().eqZero(Math.abs(offset)); |
| final BSPTree<Vector3D> near; |
| final BSPTree<Vector3D> far; |
| if (offset < 0) { |
| near = minus; |
| far = plus; |
| } else { |
| near = plus; |
| far = minus; |
| } |
| |
| if (in) { |
| // search in the cut hyperplane |
| final SubHyperplane<Vector3D> facet = boundaryFacet(point, node); |
| |
| // only return the facet here if it exists and intersects the plane |
| // (ie, is not parallel it) |
| if (facet != null && |
| plane.intersection(line) != null) { |
| return facet; |
| } |
| } |
| |
| // search in the near branch |
| final SubHyperplane<Vector3D> crossed = recurseFirstIntersection(near, point, line); |
| if (crossed != null) { |
| return crossed; |
| } |
| |
| if (!in) { |
| // search in the cut hyperplane |
| final Vector3D hit3D = plane.intersection(line); |
| if (hit3D != null && |
| line.getAbscissa(hit3D) > line.getAbscissa(point)) { |
| final SubHyperplane<Vector3D> facet = boundaryFacet(hit3D, node); |
| if (facet != null) { |
| return facet; |
| } |
| } |
| } |
| |
| // search in the far branch |
| return recurseFirstIntersection(far, point, line); |
| |
| } |
| |
| /** Check if a point belongs to the boundary part of a node. |
| * @param point point to check |
| * @param node node containing the boundary facet to check |
| * @return the boundary facet this points belongs to (or null if it |
| * does not belong to any boundary facet) |
| */ |
| private SubHyperplane<Vector3D> boundaryFacet(final Vector3D point, |
| final BSPTree<Vector3D> node) { |
| final Vector2D Vector2D = ((Plane) node.getCut().getHyperplane()).toSubSpace(point); |
| @SuppressWarnings("unchecked") |
| final BoundaryAttribute<Vector3D> attribute = |
| (BoundaryAttribute<Vector3D>) node.getAttribute(); |
| if ((attribute.getPlusOutside() != null) && |
| (((SubPlane) attribute.getPlusOutside()).getRemainingRegion().checkPoint(Vector2D) != Location.OUTSIDE)) { |
| return attribute.getPlusOutside(); |
| } |
| if ((attribute.getPlusInside() != null) && |
| (((SubPlane) attribute.getPlusInside()).getRemainingRegion().checkPoint(Vector2D) != Location.OUTSIDE)) { |
| return attribute.getPlusInside(); |
| } |
| return null; |
| } |
| |
| /** Rotate the region 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 instance representing the rotated region |
| */ |
| public PolyhedronsSet rotate(final Vector3D center, final QuaternionRotation rotation) { |
| return (PolyhedronsSet) applyTransform(new RotationTransform(center, rotation)); |
| } |
| |
| /** 3D rotation as a Transform. */ |
| private static class RotationTransform implements Transform<Vector3D, Vector2D> { |
| |
| /** Center point of the rotation. */ |
| private final Vector3D center; |
| |
| /** Quaternion rotation. */ |
| private final QuaternionRotation rotation; |
| |
| /** Cached original hyperplane. */ |
| private Plane cachedOriginal; |
| |
| /** Cached 2D transform valid inside the cached original hyperplane. */ |
| private Transform<Vector2D, Vector1D> cachedTransform; |
| |
| /** Build a rotation transform. |
| * @param center center point of the rotation |
| * @param rotation vectorial rotation |
| */ |
| RotationTransform(final Vector3D center, final QuaternionRotation rotation) { |
| this.center = center; |
| this.rotation = rotation; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Vector3D apply(final Vector3D point) { |
| final Vector3D delta = point.subtract(center); |
| return Vector3D.linearCombination(1.0, center, 1.0, rotation.apply(delta)); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Plane apply(final Hyperplane<Vector3D> hyperplane) { |
| return ((Plane) hyperplane).rotate(center, rotation); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public SubHyperplane<Vector2D> apply(final SubHyperplane<Vector2D> sub, |
| final Hyperplane<Vector3D> original, |
| final Hyperplane<Vector3D> transformed) { |
| if (original != cachedOriginal) { |
| // we have changed hyperplane, reset the in-hyperplane transform |
| |
| final Plane oPlane = (Plane) original; |
| final Plane tPlane = (Plane) transformed; |
| final Vector3D p00 = oPlane.getOrigin(); |
| final Vector3D p10 = oPlane.toSpace(Vector2D.of(1.0, 0.0)); |
| final Vector3D p01 = oPlane.toSpace(Vector2D.of(0.0, 1.0)); |
| final Vector2D tP00 = tPlane.toSubSpace(apply(p00)); |
| final Vector2D tP10 = tPlane.toSubSpace(apply(p10)); |
| final Vector2D tP01 = tPlane.toSubSpace(apply(p01)); |
| |
| cachedOriginal = (Plane) original; |
| cachedTransform = |
| org.apache.commons.geometry.euclidean.twod.Line.getTransform(tP00.vectorTo(tP10), |
| tP00.vectorTo(tP01), |
| tP00); |
| } |
| return ((SubLine) sub).applyTransform(cachedTransform); |
| } |
| |
| } |
| |
| /** Translate the region by the specified amount. |
| * <p>The instance is not modified, a new instance is created.</p> |
| * @param translation translation to apply |
| * @return a new instance representing the translated region |
| */ |
| public PolyhedronsSet translate(final Vector3D translation) { |
| return (PolyhedronsSet) applyTransform(new TranslationTransform(translation)); |
| } |
| |
| /** 3D translation as a transform. */ |
| private static class TranslationTransform implements Transform<Vector3D, Vector2D> { |
| |
| /** Translation vector. */ |
| private final Vector3D translation; |
| |
| /** Cached original hyperplane. */ |
| private Plane cachedOriginal; |
| |
| /** Cached 2D transform valid inside the cached original hyperplane. */ |
| private Transform<Vector2D, Vector1D> cachedTransform; |
| |
| /** Build a translation transform. |
| * @param translation translation vector |
| */ |
| TranslationTransform(final Vector3D translation) { |
| this.translation = translation; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Vector3D apply(final Vector3D point) { |
| return Vector3D.linearCombination(1.0, point, 1.0, translation); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Plane apply(final Hyperplane<Vector3D> hyperplane) { |
| return ((Plane) hyperplane).translate(translation); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public SubHyperplane<Vector2D> apply(final SubHyperplane<Vector2D> sub, |
| final Hyperplane<Vector3D> original, |
| final Hyperplane<Vector3D> transformed) { |
| if (original != cachedOriginal) { |
| // we have changed hyperplane, reset the in-hyperplane transform |
| |
| final Plane oPlane = (Plane) original; |
| final Plane tPlane = (Plane) transformed; |
| final Vector2D shift = tPlane.toSubSpace(apply(oPlane.getOrigin())); |
| |
| cachedOriginal = (Plane) original; |
| cachedTransform = |
| org.apache.commons.geometry.euclidean.twod.Line.getTransform(Vector2D.Unit.PLUS_X, |
| Vector2D.Unit.PLUS_Y, |
| shift); |
| } |
| |
| return ((SubLine) sub).applyTransform(cachedTransform); |
| } |
| } |
| } |