TextStlWriterTest.java
/*
* 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
*
* https://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.io.euclidean.threed.stl;
import java.io.StringWriter;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import org.apache.commons.geometry.core.GeometryTestUtils;
import org.apache.commons.geometry.euclidean.threed.ConvexPolygon3D;
import org.apache.commons.geometry.euclidean.threed.Planes;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
import org.apache.commons.geometry.io.core.test.CloseCountWriter;
import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;
import org.apache.commons.geometry.io.euclidean.threed.SimpleFacetDefinition;
import org.apache.commons.numbers.core.Precision;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class TextStlWriterTest {
private static final double TEST_EPS = 1e-10;
private static final Precision.DoubleEquivalence TEST_PRECISION =
Precision.doubleEquivalenceOfEpsilon(TEST_EPS);
private final StringWriter out = new StringWriter();
@Test
void testDefaultProperties() {
// act/assert
try (TextStlWriter writer = new TextStlWriter(out)) {
Assertions.assertNotNull(writer.getDoubleFormat());
Assertions.assertEquals("\n", writer.getLineSeparator());
}
}
@Test
void testNoContent() {
// arrange
final CloseCountWriter countWriter = new CloseCountWriter(out);
// act
try (TextStlWriter writer = new TextStlWriter(countWriter)) {
Assertions.assertEquals(0, countWriter.getCloseCount());
}
// assert
Assertions.assertEquals(1, countWriter.getCloseCount());
Assertions.assertEquals("", out.toString());
}
@Test
void testStartSolid_alreadyStarted() {
// arrange
try (TextStlWriter writer = new TextStlWriter(out)) {
writer.startSolid();
// act/assert
GeometryTestUtils.assertThrowsWithMessage(
writer::startSolid,
IllegalStateException.class, "Cannot start solid definition: a solid is already being written");
}
}
@Test
void testEndSolid_notStarted() {
// arrange
try (TextStlWriter writer = new TextStlWriter(out)) {
// act/assert
GeometryTestUtils.assertThrowsWithMessage(
writer::endSolid,
IllegalStateException.class, "Cannot end solid definition: no solid has been started");
}
}
@Test
void testEmpty_noName() {
// arrange
final CloseCountWriter countWriter = new CloseCountWriter(out);
// act
try (TextStlWriter writer = new TextStlWriter(countWriter)) {
writer.startSolid();
writer.endSolid();
Assertions.assertEquals(0, countWriter.getCloseCount());
}
// assert
Assertions.assertEquals(1, countWriter.getCloseCount());
Assertions.assertEquals(
"solid \n" +
"endsolid \n", out.toString());
}
@Test
void testEmpty_withName() {
// arrange
final CloseCountWriter countWriter = new CloseCountWriter(out);
// act
try (TextStlWriter writer = new TextStlWriter(countWriter)) {
writer.startSolid("Name of the solid");
writer.endSolid();
Assertions.assertEquals(0, countWriter.getCloseCount());
}
// assert
Assertions.assertEquals(1, countWriter.getCloseCount());
Assertions.assertEquals(
"solid Name of the solid\n" +
"endsolid Name of the solid\n", out.toString());
}
@Test
void testClose_endsSolid() {
// arrange
final CloseCountWriter countWriter = new CloseCountWriter(out);
// act
try (TextStlWriter writer = new TextStlWriter(countWriter)) {
writer.startSolid("name");
Assertions.assertEquals(0, countWriter.getCloseCount());
}
// assert
Assertions.assertEquals(1, countWriter.getCloseCount());
Assertions.assertEquals(
"solid name\n" +
"endsolid name\n", out.toString());
}
@Test
void testStartSolid_containsNewLine() {
// arrange
try (TextStlWriter writer = new TextStlWriter(out)) {
final String err = "Solid name cannot contain new line characters";
// act/assert
GeometryTestUtils.assertThrowsWithMessage(
() -> writer.startSolid("Hi\nthere"),
IllegalArgumentException.class, err);
GeometryTestUtils.assertThrowsWithMessage(
() -> writer.startSolid("Hi\r\nthere"),
IllegalArgumentException.class, err);
GeometryTestUtils.assertThrowsWithMessage(
() -> writer.startSolid("Hi\rthere"),
IllegalArgumentException.class, err);
}
}
@Test
void testWriteTriangle_noNormal_computesNormal() {
// arrange
final Vector3D p1 = Vector3D.of(0, 4, 0);
final Vector3D p2 = Vector3D.of(1.0 / 3.0, 0, 0);
final Vector3D p3 = Vector3D.of(0, 0.5, 10);
// act
try (TextStlWriter writer = new TextStlWriter(out)) {
writer.startSolid();
writer.writeTriangle(p1, p2, p3, null);
}
// assert
Assertions.assertEquals(
"solid \n" +
"facet -0.9961250701090868 -0.08301042250909056 -0.029053647878181696\n" +
"outer loop\n" +
"vertex 0.0 4.0 0.0\n" +
"vertex 0.3333333333333333 0.0 0.0\n" +
"vertex 0.0 0.5 10.0\n" +
"endloop\n" +
"endfacet\n" +
"endsolid \n", out.toString());
}
@Test
void testWriteTriangle_zeroNormal_computesNormal() {
// arrange
final Vector3D p1 = Vector3D.of(0, 4, 0);
final Vector3D p2 = Vector3D.of(1.0 / 3.0, 0, 0);
final Vector3D p3 = Vector3D.of(0, 0.5, 10);
// act
try (TextStlWriter writer = new TextStlWriter(out)) {
writer.startSolid();
writer.writeTriangle(p1, p2, p3, Vector3D.ZERO);
}
// assert
Assertions.assertEquals(
"solid \n" +
"facet -0.9961250701090868 -0.08301042250909056 -0.029053647878181696\n" +
"outer loop\n" +
"vertex 0.0 4.0 0.0\n" +
"vertex 0.3333333333333333 0.0 0.0\n" +
"vertex 0.0 0.5 10.0\n" +
"endloop\n" +
"endfacet\n" +
"endsolid \n", out.toString());
}
@Test
void testWriteTriangle_noNormal_cannotComputeNormal() {
// arrange
final Vector3D p1 = Vector3D.ZERO;
final Vector3D p2 = Vector3D.of(1.0 / 3.0, 0, 0);
final Vector3D p3 = Vector3D.ZERO;
// act
try (TextStlWriter writer = new TextStlWriter(out)) {
writer.startSolid();
writer.writeTriangle(p1, p2, p3, null);
}
// assert
Assertions.assertEquals(
"solid \n" +
"facet 0.0 0.0 0.0\n" +
"outer loop\n" +
"vertex 0.0 0.0 0.0\n" +
"vertex 0.3333333333333333 0.0 0.0\n" +
"vertex 0.0 0.0 0.0\n" +
"endloop\n" +
"endfacet\n" +
"endsolid \n", out.toString());
}
@Test
void testWriteTriangle_withNormal_correctOrientation() {
// arrange
final Vector3D p1 = Vector3D.of(0, 4, 0);
final Vector3D p2 = Vector3D.of(1.0 / 3.0, 0, 0);
final Vector3D p3 = Vector3D.of(0, 0.5, 10);
final Vector3D normal = p1.vectorTo(p2).cross(p1.vectorTo(p3)).normalize();
// act
try (TextStlWriter writer = new TextStlWriter(out)) {
writer.startSolid();
writer.writeTriangle(p1, p2, p3, normal);
}
// assert
Assertions.assertEquals(
"solid \n" +
"facet -0.9961250701090868 -0.08301042250909056 -0.029053647878181696\n" +
"outer loop\n" +
"vertex 0.0 4.0 0.0\n" +
"vertex 0.3333333333333333 0.0 0.0\n" +
"vertex 0.0 0.5 10.0\n" +
"endloop\n" +
"endfacet\n" +
"endsolid \n", out.toString());
}
@Test
void testWriteTriangle_withNormal_reversedOrientation() {
// arrange
final Vector3D p1 = Vector3D.of(0, 4, 0);
final Vector3D p2 = Vector3D.of(1.0 / 3.0, 0, 0);
final Vector3D p3 = Vector3D.of(0, 0.5, 10);
final Vector3D normal = p1.vectorTo(p2).cross(p1.vectorTo(p3)).normalize();
// act
try (TextStlWriter writer = new TextStlWriter(out)) {
writer.startSolid();
writer.writeTriangle(p1, p2, p3, normal.negate());
}
// assert
Assertions.assertEquals(
"solid \n" +
"facet 0.9961250701090868 0.08301042250909056 0.029053647878181696\n" +
"outer loop\n" +
"vertex 0.0 4.0 0.0\n" +
"vertex 0.0 0.5 10.0\n" +
"vertex 0.3333333333333333 0.0 0.0\n" +
"endloop\n" +
"endfacet\n" +
"endsolid \n", out.toString());
}
@Test
void testWrite_verticesAndNormal() {
// arrange
final List<Vector3D> vertices = Arrays.asList(
Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0));
final Vector3D n1 = Vector3D.of(0, 0, 100);
final Vector3D n2 = Vector3D.Unit.MINUS_Z;
// act
try (TextStlWriter writer = new TextStlWriter(out)) {
writer.startSolid();
writer.writeTriangles(vertices, n1);
writer.writeTriangles(vertices, n2);
writer.writeTriangles(vertices, null);
}
// assert
Assertions.assertEquals(
"solid \n" +
"facet 0.0 0.0 1.0\n" +
"outer loop\n" +
"vertex 0.0 0.0 0.0\n" +
"vertex 1.0 0.0 0.0\n" +
"vertex 0.0 1.0 0.0\n" +
"endloop\n" +
"endfacet\n" +
"facet 0.0 0.0 -1.0\n" +
"outer loop\n" +
"vertex 0.0 0.0 0.0\n" +
"vertex 0.0 1.0 0.0\n" +
"vertex 1.0 0.0 0.0\n" +
"endloop\n" +
"endfacet\n" +
"facet 0.0 0.0 1.0\n" +
"outer loop\n" +
"vertex 0.0 0.0 0.0\n" +
"vertex 1.0 0.0 0.0\n" +
"vertex 0.0 1.0 0.0\n" +
"endloop\n" +
"endfacet\n" +
"endsolid \n", out.toString());
}
@Test
void testWrite_verticesAndNormal_moreThanThreeVertices() {
// arrange
final List<Vector3D> vertices = Arrays.asList(
Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0), Vector3D.of(0, 1, 0));
final Vector3D normal = Vector3D.Unit.PLUS_Z;
// act
try (TextStlWriter writer = new TextStlWriter(out)) {
writer.startSolid();
writer.writeTriangles(vertices, normal);
}
// assert
Assertions.assertEquals(
"solid \n" +
"facet 0.0 0.0 1.0\n" +
"outer loop\n" +
"vertex 0.0 0.0 0.0\n" +
"vertex 1.0 0.0 0.0\n" +
"vertex 1.0 1.0 0.0\n" +
"endloop\n" +
"endfacet\n" +
"facet 0.0 0.0 1.0\n" +
"outer loop\n" +
"vertex 0.0 0.0 0.0\n" +
"vertex 1.0 1.0 0.0\n" +
"vertex 0.0 1.0 0.0\n" +
"endloop\n" +
"endfacet\n" +
"endsolid \n", out.toString());
}
@Test
void testWrite_verticesAndNormal_fewerThanThreeVertices() {
// arrange
try (TextStlWriter writer = new TextStlWriter(out)) {
writer.startSolid();
final List<Vector3D> noElements = Collections.emptyList();
final List<Vector3D> singleElement = Collections.singletonList(Vector3D.ZERO);
final List<Vector3D> twoElements = Arrays.asList(Vector3D.ZERO, Vector3D.of(1, 1, 1));
// act/assert
Assertions.assertThrows(IllegalArgumentException.class,
() -> writer.writeTriangles(noElements, null));
Assertions.assertThrows(IllegalArgumentException.class,
() -> writer.writeTriangles(singleElement, null));
Assertions.assertThrows(IllegalArgumentException.class,
() -> writer.writeTriangles(twoElements, null));
}
}
@Test
void testWrite_boundary() {
// arrange
final ConvexPolygon3D boundary = Planes.convexPolygonFromVertices(
Arrays.asList(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 0, 1), Vector3D.of(0, 0, 1)),
TEST_PRECISION);
// act
try (TextStlWriter writer = new TextStlWriter(out)) {
writer.startSolid();
writer.writeTriangles(boundary);
}
// assert
Assertions.assertEquals(
"solid \n" +
"facet 0.0 -1.0 0.0\n" +
"outer loop\n" +
"vertex 0.0 0.0 0.0\n" +
"vertex 1.0 0.0 0.0\n" +
"vertex 1.0 0.0 1.0\n" +
"endloop\n" +
"endfacet\n" +
"facet 0.0 -1.0 0.0\n" +
"outer loop\n" +
"vertex 0.0 0.0 0.0\n" +
"vertex 1.0 0.0 1.0\n" +
"vertex 0.0 0.0 1.0\n" +
"endloop\n" +
"endfacet\n" +
"endsolid \n", out.toString());
}
@Test
void testWrite_facetDefinition_noNormal() {
// arrange
final FacetDefinition facet = new SimpleFacetDefinition(Arrays.asList(
Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0), Vector3D.of(0, 1, 0)));
// act
try (TextStlWriter writer = new TextStlWriter(out)) {
writer.startSolid();
writer.writeTriangles(facet);
}
// assert
Assertions.assertEquals(
"solid \n" +
"facet 0.0 0.0 1.0\n" +
"outer loop\n" +
"vertex 0.0 0.0 0.0\n" +
"vertex 1.0 0.0 0.0\n" +
"vertex 1.0 1.0 0.0\n" +
"endloop\n" +
"endfacet\n" +
"facet 0.0 0.0 1.0\n" +
"outer loop\n" +
"vertex 0.0 0.0 0.0\n" +
"vertex 1.0 1.0 0.0\n" +
"vertex 0.0 1.0 0.0\n" +
"endloop\n" +
"endfacet\n" +
"endsolid \n", out.toString());
}
@Test
void testWrite_facetDefinition_withNormal() {
// arrange
final Vector3D normal = Vector3D.Unit.PLUS_Z;
final FacetDefinition facet = new SimpleFacetDefinition(Arrays.asList(
Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0), Vector3D.of(0, 1, 0)),
normal);
// act
try (TextStlWriter writer = new TextStlWriter(out)) {
writer.startSolid();
writer.writeTriangles(facet);
}
// assert
Assertions.assertEquals(
"solid \n" +
"facet 0.0 0.0 1.0\n" +
"outer loop\n" +
"vertex 0.0 0.0 0.0\n" +
"vertex 1.0 0.0 0.0\n" +
"vertex 1.0 1.0 0.0\n" +
"endloop\n" +
"endfacet\n" +
"facet 0.0 0.0 1.0\n" +
"outer loop\n" +
"vertex 0.0 0.0 0.0\n" +
"vertex 1.0 1.0 0.0\n" +
"vertex 0.0 1.0 0.0\n" +
"endloop\n" +
"endfacet\n" +
"endsolid \n", out.toString());
}
@Test
void testWrite_noSolidStarted() {
// arrange
final List<Vector3D> vertices = Arrays.asList(
Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0));
final Vector3D normal = Vector3D.Unit.PLUS_Z;
final String msg = "Cannot write triangle: no solid has been started";
try (TextStlWriter writer = new TextStlWriter(out)) {
// act/assert
GeometryTestUtils.assertThrowsWithMessage(
() -> writer.writeTriangle(vertices.get(0), vertices.get(1), vertices.get(2), normal),
IllegalStateException.class, msg);
GeometryTestUtils.assertThrowsWithMessage(
() -> writer.writeTriangles(vertices, normal),
IllegalStateException.class, msg);
GeometryTestUtils.assertThrowsWithMessage(
() -> writer.writeTriangles(new SimpleFacetDefinition(vertices, normal)),
IllegalStateException.class, msg);
GeometryTestUtils.assertThrowsWithMessage(
() -> writer.writeTriangles(Planes.convexPolygonFromVertices(vertices, TEST_PRECISION)),
IllegalStateException.class, msg);
}
}
@Test
void testWrite_customFormat() {
// arrange
final List<Vector3D> vertices = Arrays.asList(
Vector3D.ZERO, Vector3D.of(1.0 / 3.0, 0, 0), Vector3D.of(0, 1.0 / 3.0, 0));
final Vector3D normal = Vector3D.Unit.PLUS_Z;
final DecimalFormat fmt =
new DecimalFormat("0.0##", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
try (TextStlWriter writer = new TextStlWriter(out)) {
writer.setDoubleFormat(fmt::format);
writer.setLineSeparator("\r\n");
// act
writer.startSolid();
writer.writeTriangles(vertices, normal);
}
// assert
Assertions.assertEquals(
"solid \r\n" +
"facet 0.0 0.0 1.0\r\n" +
"outer loop\r\n" +
"vertex 0.0 0.0 0.0\r\n" +
"vertex 0.333 0.0 0.0\r\n" +
"vertex 0.0 0.333 0.0\r\n" +
"endloop\r\n" +
"endfacet\r\n" +
"endsolid \r\n", out.toString());
}
@Test
void testWrite_badFacet_withNormal() {
// arrange
final List<Vector3D> vertices = Arrays.asList(
Vector3D.ZERO, Vector3D.ZERO, Vector3D.ZERO);
final Vector3D normal = Vector3D.Unit.PLUS_Z;
try (TextStlWriter writer = new TextStlWriter(out)) {
// act
writer.startSolid();
writer.writeTriangles(vertices, normal);
}
// assert
Assertions.assertEquals(
"solid \n" +
"facet 0.0 0.0 1.0\n" +
"outer loop\n" +
"vertex 0.0 0.0 0.0\n" +
"vertex 0.0 0.0 0.0\n" +
"vertex 0.0 0.0 0.0\n" +
"endloop\n" +
"endfacet\n" +
"endsolid \n", out.toString());
}
@Test
void testWrite_badFacet_noNormal() {
// arrange
final List<Vector3D> vertices = Arrays.asList(
Vector3D.ZERO, Vector3D.ZERO, Vector3D.ZERO);
try (TextStlWriter writer = new TextStlWriter(out)) {
// act
writer.startSolid();
writer.writeTriangles(vertices, null);
}
// assert
Assertions.assertEquals(
"solid \n" +
"facet 0.0 0.0 0.0\n" +
"outer loop\n" +
"vertex 0.0 0.0 0.0\n" +
"vertex 0.0 0.0 0.0\n" +
"vertex 0.0 0.0 0.0\n" +
"endloop\n" +
"endfacet\n" +
"endsolid \n", out.toString());
}
}