blob: 4346306ce558aa2c4bbf8b13b4ca180c2100cfc5 [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.io.File;
import java.io.IOException;
import java.io.Writer;
import java.nio.file.Files;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
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.precision.DoublePrecisionContext;
import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
/** This class creates simple OBJ files from {@link PolyhedronsSet} instances.
* The output files can be opened in a 3D viewer for visual debugging of 3D
* regions. This class is only intended for use in testing.
*
* @see https://en.wikipedia.org/wiki/Wavefront_.obj_file
*/
public class OBJWriter {
/** Writes an OBJ file representing the given {@link PolyhedronsSet}. Only
* finite boundaries are written. Infinite boundaries are ignored.
* @param file The path of the file to write
* @param poly The input PolyhedronsSet
* @throws IOException
*/
public static void write(String file, PolyhedronsSet poly) throws IOException {
write(new File(file), poly);
}
/** Writes an OBJ file representing the given {@link PolyhedronsSet}. Only
* finite boundaries are written. Infinite boundaries are ignored.
* @param file The file to write
* @param poly The input PolyhedronsSet
* @throws IOException
*/
public static void write(File file, PolyhedronsSet poly) throws IOException {
// get the vertices and faces
MeshBuilder meshBuilder = new MeshBuilder(poly.getPrecision());
poly.getTree(true).visit(meshBuilder);
// write them to the file
try (Writer writer = Files.newBufferedWriter(file.toPath())) {
writer.write("# Generated by " + OBJWriter.class.getName() + " on " + new Date() + "\n");
writeVertices(writer, meshBuilder.getVertices());
writeFaces(writer, meshBuilder.getFaces());
}
}
/** Writes the given list of vertices to the file in the OBJ format.
* @param writer
* @param vertices
* @throws IOException
*/
private static void writeVertices(Writer writer, List<Vector3D> vertices) throws IOException {
DecimalFormat df = new DecimalFormat("0.######");
for (Vector3D v : vertices) {
writer.write("v ");
writer.write(df.format(v.getX()));
writer.write(" ");
writer.write(df.format(v.getY()));
writer.write(" ");
writer.write(df.format(v.getZ()));
writer.write("\n");
}
}
/** Writes the given list of face vertex indices to the file in the OBJ format. The indices
* are expected to be 0-based and are converted to the 1-based format used by OBJ.
* @param writer
* @param faces
* @throws IOException
*/
private static void writeFaces(Writer writer, List<int[]> faces) throws IOException {
for (int[] face : faces) {
writer.write("f ");
for (int idx : face) {
writer.write(String.valueOf(idx + 1)); // obj indices are 1-based
writer.write(" ");
}
writer.write("\n");
}
}
/** Class used to impose a strict sorting on 3D vertices.
* If all of the components of two vertices are within tolerance of each
* other, then the vertices are considered equal. This helps to avoid
* writing duplicate vertices in the OBJ output.
*/
private static class VertexComparator implements Comparator<Vector3D> {
/** Precision context to deteremine floating-point equality */
private final DoublePrecisionContext precision;
/** Creates a new instance with the given tolerance value.
* @param tolerance
*/
public VertexComparator(final DoublePrecisionContext precision) {
this.precision = precision;
}
/** {@inheritDoc} */
@Override
public int compare(Vector3D a, Vector3D b) {
int result = precision.compare(a.getX(), b.getX());
if (result == 0) {
result = precision.compare(a.getY(), b.getY());
if (result == 0) {
result = precision.compare(a.getZ(), b.getZ());
}
}
return result;
}
}
/** Class for converting a 3D BSPTree into a list of vertices
* and face vertex indices.
*/
private static class MeshBuilder implements BSPTreeVisitor<Vector3D> {
/** Precision context to deteremine floating-point equality */
private final DoublePrecisionContext precision;
/** Map of vertices to their index in the vertices list */
private Map<Vector3D, Integer> vertexIndexMap;
/** List of unique vertices in the BSPTree boundary */
private List<Vector3D> vertices;
/**
* List of face vertex indices. Each face will have 3 indices. Indices
* are 0-based.
* */
private List<int[]> faces;
/** Creates a new instance with the given tolerance.
* @param tolerance
*/
public MeshBuilder(final DoublePrecisionContext precision) {
this.precision = precision;
this.vertexIndexMap = new TreeMap<>(new VertexComparator(precision));
this.vertices = new ArrayList<>();
this.faces = new ArrayList<>();
}
/** Returns the list of unique vertices found in the BSPTree.
* @return
*/
public List<Vector3D> getVertices() {
return vertices;
}
/** Returns the list of 0-based face vertex indices for the BSPTree. Each face is
* a triangle with 3 indices.
* @return
*/
public List<int[]> getFaces() {
return faces;
}
/** {@inheritDoc} */
@Override
public Order visitOrder(BSPTree<Vector3D> node) {
return Order.SUB_MINUS_PLUS;
}
/** {@inheritDoc} */
@SuppressWarnings("unchecked")
@Override
public void visitInternalNode(BSPTree<Vector3D> node) {
BoundaryAttribute<Vector3D> attr = (BoundaryAttribute<Vector3D>) node.getAttribute();
if (attr.getPlusOutside() != null) {
addBoundary((SubPlane) attr.getPlusOutside());
}
else if (attr.getPlusInside() != null) {
addBoundary((SubPlane) attr.getPlusInside());
}
}
/** {@inheritDoc} */
@Override
public void visitLeafNode(BSPTree<Vector3D> node) {
// do nothing
}
/** Adds the region boundary defined by the given {@link SubPlane}
* to the mesh.
* @param subplane
*/
private void addBoundary(SubPlane subplane) {
Plane plane = (Plane) subplane.getHyperplane();
PolygonsSet poly = (PolygonsSet) subplane.getRemainingRegion();
TriangleExtractor triExtractor = new TriangleExtractor(precision);
poly.getTree(true).visit(triExtractor);
Vector3D v1, v2, v3;
for (Vector2D[] tri : triExtractor.getTriangles()) {
v1 = plane.toSpace(tri[0]);
v2 = plane.toSpace(tri[1]);
v3 = plane.toSpace(tri[2]);
faces.add(new int[] {
getVertexIndex(v1),
getVertexIndex(v2),
getVertexIndex(v3)
});
}
}
/** Returns the 0-based index of the given vertex in the <code>vertices</code>
* list. If the vertex has not been encountered before, it is added
* to the list.
* @param vertex
* @return
*/
private int getVertexIndex(Vector3D vertex) {
Integer idx = vertexIndexMap.get(vertex);
if (idx == null) {
idx = vertices.size();
vertices.add(vertex);
vertexIndexMap.put(vertex, idx);
}
return idx.intValue();
}
}
/** Visitor for extracting a collection of triangles from a 2D BSPTree.
*/
private static class TriangleExtractor implements BSPTreeVisitor<Vector2D> {
/** Precision context to deteremine floating-point equality */
private final DoublePrecisionContext precision;
/** List of extracted triangles */
private List<Vector2D[]> triangles = new ArrayList<>();
/** Creates a new instance with the given geometric tolerance.
* @param tolerance
*/
public TriangleExtractor(final DoublePrecisionContext precision) {
this.precision = precision;
}
/** Returns the list of extracted triangles.
* @return
*/
public List<Vector2D[]> getTriangles() {
return triangles;
}
/** {@inheritDoc} */
@Override
public Order visitOrder(BSPTree<Vector2D> node) {
return Order.SUB_MINUS_PLUS;
}
/** {@inheritDoc} */
@Override
public void visitInternalNode(BSPTree<Vector2D> node) {
// do nothing
}
/** {@inheritDoc} */
@Override
public void visitLeafNode(BSPTree<Vector2D> node) {
if ((Boolean) node.getAttribute()) {
PolygonsSet convexPoly = new PolygonsSet(node.pruneAroundConvexCell(Boolean.TRUE,
Boolean.FALSE, null), precision);
for (Vector2D[] loop : convexPoly.getVertices()) {
if (loop.length > 0 && loop[0] != null) { // skip unclosed loops
addTriangles(loop);
}
}
}
}
/** Splits the 2D convex area defined by the given vertices into
* triangles and adds them to the internal list.
* @param vertices
*/
private void addTriangles(Vector2D[] vertices) {
// use a triangle fan to add the convex region
for (int i=2; i<vertices.length; ++i) {
triangles.add(new Vector2D[] { vertices[0], vertices[i-1], vertices[i] });
}
}
}
}