blob: 48c81811729d5d64e88fef9f95333036f69c5b18 [file] [log] [blame]
/*
* 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 org.apache.commons.geometry.core.partitioning.AbstractSubHyperplane;
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.RegionFactory;
import org.apache.commons.geometry.core.partitioning.SubHyperplane;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
/** Extractor for {@link PolygonsSet polyhedrons sets} outlines.
* <p>This class extracts the 2D outlines from {{@link PolygonsSet
* polyhedrons sets} in a specified projection plane.</p>
*/
public class OutlineExtractor {
/** Abscissa axis of the projection plane. */
private final Vector3D u;
/** Ordinate axis of the projection plane. */
private final Vector3D v;
/** Normal of the projection plane (viewing direction). */
private final Vector3D w;
/** Build an extractor for a specific projection plane.
* @param u abscissa axis of the projection point
* @param v ordinate axis of the projection point
*/
public OutlineExtractor(final Vector3D u, final Vector3D v) {
this.u = u;
this.v = v;
this.w = u.cross(v);
}
/** Extract the outline of a polyhedrons set.
* @param polyhedronsSet polyhedrons set whose outline must be extracted
* @return an outline, as an array of loops.
*/
public Vector2D[][] getOutline(final PolyhedronsSet polyhedronsSet) {
// project all boundary facets into one polygons set
final BoundaryProjector projector = new BoundaryProjector(polyhedronsSet.getPrecision());
polyhedronsSet.getTree(true).visit(projector);
final PolygonsSet projected = projector.getProjected();
// Remove the spurious intermediate vertices from the outline
final Vector2D[][] outline = projected.getVertices();
for (int i = 0; i < outline.length; ++i) {
final Vector2D[] rawLoop = outline[i];
int end = rawLoop.length;
int j = 0;
while (j < end) {
if (pointIsBetween(rawLoop, end, j)) {
// the point should be removed
for (int k = j; k < (end - 1); ++k) {
rawLoop[k] = rawLoop[k + 1];
}
--end;
} else {
// the point remains in the loop
++j;
}
}
if (end != rawLoop.length) {
// resize the array
outline[i] = new Vector2D[end];
System.arraycopy(rawLoop, 0, outline[i], 0, end);
}
}
return outline;
}
/** Check if a point is geometrically between its neighbor in an array.
* <p>The neighbors are computed considering the array is a loop
* (i.e. point at index (n-1) is before point at index 0)</p>
* @param loop points array
* @param n number of points to consider in the array
* @param i index of the point to check (must be between 0 and n-1)
* @return true if the point is exactly between its neighbors
*/
private boolean pointIsBetween(final Vector2D[] loop, final int n, final int i) {
final Vector2D previous = loop[(i + n - 1) % n];
final Vector2D current = loop[i];
final Vector2D next = loop[(i + 1) % n];
final double dx1 = current.getX() - previous.getX();
final double dy1 = current.getY() - previous.getY();
final double dx2 = next.getX() - current.getX();
final double dy2 = next.getY() - current.getY();
final double cross = dx1 * dy2 - dx2 * dy1;
final double dot = dx1 * dx2 + dy1 * dy2;
final double d1d2 = Math.sqrt((dx1 * dx1 + dy1 * dy1) * (dx2 * dx2 + dy2 * dy2));
return (Math.abs(cross) <= (1.0e-6 * d1d2)) && (dot >= 0.0);
}
/** Visitor projecting the boundary facets on a plane. */
private class BoundaryProjector implements BSPTreeVisitor<Vector3D> {
/** Projection of the polyhedrons set on the plane. */
private PolygonsSet projected;
/** Precision context used to compare floating point numbers. */
private final DoublePrecisionContext precision;
/** Simple constructor.
* @param precision precision context used to compare floating point values
*/
BoundaryProjector(final DoublePrecisionContext precision) {
this.projected = new PolygonsSet(new BSPTree<Vector2D>(Boolean.FALSE), precision);
this.precision = precision;
}
/** {@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 he 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) {
// extract the vertices of the facet
@SuppressWarnings("unchecked")
final AbstractSubHyperplane<Vector3D, Vector2D> absFacet =
(AbstractSubHyperplane<Vector3D, Vector2D>) facet;
final Plane plane = (Plane) facet.getHyperplane();
final double scal = plane.getNormal().dot(w);
if (Math.abs(scal) > 1.0e-3) {
Vector2D[][] vertices =
((PolygonsSet) absFacet.getRemainingRegion()).getVertices();
if ((scal < 0) ^ reversed) {
// the facet is seen from the inside,
// we need to invert its boundary orientation
final Vector2D[][] newVertices = new Vector2D[vertices.length][];
for (int i = 0; i < vertices.length; ++i) {
final Vector2D[] loop = vertices[i];
final Vector2D[] newLoop = new Vector2D[loop.length];
if (loop[0] == null) {
newLoop[0] = null;
for (int j = 1; j < loop.length; ++j) {
newLoop[j] = loop[loop.length - j];
}
} else {
for (int j = 0; j < loop.length; ++j) {
newLoop[j] = loop[loop.length - (j + 1)];
}
}
newVertices[i] = newLoop;
}
// use the reverted vertices
vertices = newVertices;
}
// compute the projection of the facet in the outline plane
final ArrayList<SubHyperplane<Vector2D>> edges = new ArrayList<>();
for (Vector2D[] loop : vertices) {
final boolean closed = loop[0] != null;
int previous = closed ? (loop.length - 1) : 1;
Vector3D previous3D = plane.toSpace(loop[previous]);
int current = (previous + 1) % loop.length;
Vector2D pPoint = Vector2D.of(previous3D.dot(u),
previous3D.dot(v));
while (current < loop.length) {
final Vector3D current3D = plane.toSpace(loop[current]);
final Vector2D cPoint = Vector2D.of(current3D.dot(u),
current3D.dot(v));
final org.apache.commons.geometry.euclidean.twod.Line line =
org.apache.commons.geometry.euclidean.twod.Line.fromPoints(pPoint, cPoint, precision);
SubHyperplane<Vector2D> edge = line.wholeHyperplane();
if (closed || (previous != 1)) {
// the previous point is a real vertex
// it defines one bounding point of the edge
final double angle = line.getAngle() + 0.5 * Math.PI;
final org.apache.commons.geometry.euclidean.twod.Line l =
org.apache.commons.geometry.euclidean.twod.Line.fromPointAndAngle(pPoint, angle, precision);
edge = edge.split(l).getPlus();
}
if (closed || (current != (loop.length - 1))) {
// the current point is a real vertex
// it defines one bounding point of the edge
final double angle = line.getAngle() + 0.5 * Math.PI;
final org.apache.commons.geometry.euclidean.twod.Line l =
org.apache.commons.geometry.euclidean.twod.Line.fromPointAndAngle(cPoint, angle, precision);
edge = edge.split(l).getMinus();
}
edges.add(edge);
previous = current++;
previous3D = current3D;
pPoint = cPoint;
}
}
final PolygonsSet projectedFacet = new PolygonsSet(edges, precision);
// add the contribution of the facet to the global outline
projected = (PolygonsSet) new RegionFactory<Vector2D>().union(projected, projectedFacet);
}
}
/** Get the projection of the polyhedrons set on the plane.
* @return projection of the polyhedrons set on the plane
*/
public PolygonsSet getProjected() {
return projected;
}
}
}