1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.geometry.io.euclidean.threed.stl;
18
19 import java.io.ByteArrayInputStream;
20 import java.io.ByteArrayOutputStream;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.UncheckedIOException;
24 import java.nio.charset.Charset;
25 import java.nio.charset.StandardCharsets;
26 import java.util.Arrays;
27
28 import org.apache.commons.geometry.core.GeometryTestUtils;
29 import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
30 import org.apache.commons.geometry.euclidean.threed.Vector3D;
31 import org.junit.jupiter.api.Assertions;
32 import org.junit.jupiter.api.Test;
33
34 class BinaryStlFacetDefinitionReaderTest {
35
36 private static final double TEST_EPS = 1e-10;
37
38 private static final String LONG_STRING =
39 "A long string that will most definitely exceed the 80 byte length of the binary STL file format header.";
40
41 private final ByteArrayOutputStream out = new ByteArrayOutputStream();
42
43 @Test
44 void testHeader_zeros() throws IOException {
45
46 final byte[] bytes = new byte[StlConstants.BINARY_HEADER_BYTES + 4];
47 out.write(bytes);
48
49 final byte[] expectedHeader = new byte[StlConstants.BINARY_HEADER_BYTES];
50 System.arraycopy(bytes, 0, expectedHeader, 0, expectedHeader.length);
51
52 try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
53
54 Assertions.assertArrayEquals(expectedHeader, reader.getHeader().array());
55 Assertions.assertEquals(0L, reader.getNumTriangles());
56
57 Assertions.assertNull(reader.readFacet());
58 }
59 }
60
61 @Test
62 void testHeader_ones() throws IOException {
63
64 final byte[] bytes = new byte[StlConstants.BINARY_HEADER_BYTES + 4];
65 Arrays.fill(bytes, (byte) -1);
66 out.write(bytes);
67
68 final byte[] expectedHeader = new byte[StlConstants.BINARY_HEADER_BYTES];
69 System.arraycopy(bytes, 0, expectedHeader, 0, expectedHeader.length);
70
71 try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
72
73 Assertions.assertArrayEquals(expectedHeader, reader.getHeader().array());
74 Assertions.assertEquals(0xffffffffL, reader.getNumTriangles());
75 }
76 }
77
78 @Test
79 void testHeader_shortString() throws IOException {
80
81 out.write(createHeader("Hello!", StandardCharsets.UTF_8, 1));
82
83 try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
84
85 Assertions.assertEquals("Hello!", reader.getHeaderAsString());
86 Assertions.assertEquals(1L, reader.getNumTriangles());
87 }
88 }
89
90 @Test
91 void testHeader_longString() throws IOException {
92
93 out.write(createHeader(LONG_STRING, StandardCharsets.UTF_8, 8736720));
94
95 try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
96
97 Assertions.assertEquals(LONG_STRING.substring(0, StlConstants.BINARY_HEADER_BYTES),
98 reader.getHeaderAsString());
99 Assertions.assertEquals(8736720L, reader.getNumTriangles());
100 }
101 }
102
103 @Test
104 void testHeader_longString_givenCharset() throws IOException {
105
106 out.write(createHeader(LONG_STRING, StandardCharsets.UTF_16, 256));
107
108 try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
109
110 Assertions.assertEquals("A long string that will most definitely",
111 reader.getHeaderAsString(StandardCharsets.UTF_16));
112 Assertions.assertEquals(256L, reader.getNumTriangles());
113 }
114 }
115
116 @Test
117 void testGetHeader_noData() throws IOException {
118
119 out.write(new byte[32]);
120
121 try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
122
123 GeometryTestUtils.assertThrowsWithMessage(
124 () -> reader.getHeader(),
125 IllegalStateException.class, "Failed to read STL header: data not available");
126 }
127 }
128
129 @Test
130 void testGetHeader_noTriangleCount() throws IOException {
131
132 out.write(new byte[StlConstants.BINARY_HEADER_BYTES]);
133
134 try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
135
136 GeometryTestUtils.assertThrowsWithMessage(
137 () -> reader.getHeader(),
138 IllegalStateException.class, "Failed to read STL triangle count: data not available");
139 }
140 }
141
142 @Test
143 void testGetHeader_ioException() throws IOException {
144
145 final InputStream failIn = new InputStream() {
146 @Override
147 public int read() throws IOException {
148 throw new IOException("read");
149 }
150 };
151
152 try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(failIn)) {
153
154 GeometryTestUtils.assertThrowsWithMessage(
155 () -> reader.getHeader(),
156 UncheckedIOException.class, "IOException: read");
157 }
158 }
159
160 @Test
161 void testReadFacet_noData() throws IOException {
162
163 out.write(createHeader(1));
164
165
166 try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
167
168 GeometryTestUtils.assertThrowsWithMessage(
169 () -> reader.readFacet(),
170 IllegalStateException.class, "Failed to read STL triangle at index 0: data not available");
171 }
172 }
173
174 @Test
175 void testReadFacet() throws IOException {
176
177 out.write(createHeader(2));
178
179 out.write(getBytes(Vector3D.of(1, 2, 3)));
180 out.write(getBytes(Vector3D.of(4, 5, 6)));
181 out.write(getBytes(Vector3D.of(7, 8, 9)));
182 out.write(getBytes(Vector3D.of(10, 11, 12)));
183 out.write(getBytes((short) 1));
184
185 out.write(getBytes(Vector3D.of(-1, -2, -3)));
186 out.write(getBytes(Vector3D.of(-4, -5, -6)));
187 out.write(getBytes(Vector3D.of(-7, -8, -9)));
188 out.write(getBytes(Vector3D.of(-10, -11, -12)));
189 out.write(getBytes((short) 65535));
190
191 try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
192 Assertions.assertEquals(2, reader.getNumTriangles());
193
194 final BinaryStlFacetDefinition facet1 = reader.readFacet();
195
196 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 3), facet1.getNormal(), TEST_EPS);
197 Assertions.assertEquals(3, facet1.getVertices().size());
198 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(4, 5, 6), facet1.getVertices().get(0), TEST_EPS);
199 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(7, 8, 9), facet1.getVertices().get(1), TEST_EPS);
200 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(10, 11, 12), facet1.getVertices().get(2), TEST_EPS);
201
202 Assertions.assertEquals(1, facet1.getAttributeValue());
203
204 final BinaryStlFacetDefinition facet2 = reader.readFacet();
205
206 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, -2, -3), facet2.getNormal(), TEST_EPS);
207 Assertions.assertEquals(3, facet2.getVertices().size());
208 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-4, -5, -6), facet2.getVertices().get(0), TEST_EPS);
209 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-7, -8, -9), facet2.getVertices().get(1), TEST_EPS);
210 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-10, -11, -12), facet2.getVertices().get(2), TEST_EPS);
211
212 Assertions.assertEquals(65535, facet2.getAttributeValue());
213
214 Assertions.assertNull(reader.readFacet());
215 }
216 }
217
218 @Test
219 void testReadFacet_stopsWhenTriangleCountReached() throws IOException {
220
221 out.write(createHeader(1));
222
223 out.write(getBytes(Vector3D.of(1, 2, 3)));
224 out.write(getBytes(Vector3D.of(4, 5, 6)));
225 out.write(getBytes(Vector3D.of(7, 8, 9)));
226 out.write(getBytes(Vector3D.of(10, 11, 12)));
227 out.write(getBytes((short) 1));
228
229 out.write(getBytes(Vector3D.of(-1, -2, -3)));
230 out.write(getBytes(Vector3D.of(-4, -5, -6)));
231 out.write(getBytes(Vector3D.of(-7, -8, -9)));
232 out.write(getBytes(Vector3D.of(-10, -11, -12)));
233 out.write(getBytes((short) 65535));
234
235 try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
236 Assertions.assertEquals(1, reader.getNumTriangles());
237
238 final BinaryStlFacetDefinition facet = reader.readFacet();
239
240 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 3), facet.getNormal(), TEST_EPS);
241 Assertions.assertEquals(3, facet.getVertices().size());
242 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(4, 5, 6), facet.getVertices().get(0), TEST_EPS);
243 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(7, 8, 9), facet.getVertices().get(1), TEST_EPS);
244 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(10, 11, 12), facet.getVertices().get(2), TEST_EPS);
245
246 Assertions.assertEquals(1, facet.getAttributeValue());
247
248 Assertions.assertNull(reader.readFacet());
249 }
250 }
251
252 private ByteArrayInputStream getInput() {
253 return new ByteArrayInputStream(out.toByteArray());
254 }
255
256 private static byte[] createHeader(final int count) {
257 return createHeader("", StandardCharsets.UTF_8, count);
258 }
259
260 private static byte[] createHeader(final String str, final Charset charset, final int count) {
261 final byte[] result = new byte[StlConstants.BINARY_HEADER_BYTES + 4];
262
263 final byte[] strBytes = str.getBytes(charset);
264 System.arraycopy(strBytes, 0, result, 0, Math.min(StlConstants.BINARY_HEADER_BYTES, strBytes.length));
265
266 final byte[] countBytes = getBytes(count);
267 System.arraycopy(countBytes, 0, result, StlConstants.BINARY_HEADER_BYTES, countBytes.length);
268
269 return result;
270 }
271
272 private static byte[] getBytes(final Vector3D vec) {
273 final byte[] result = new byte[Float.BYTES * 3];
274 int offset = 0;
275
276 System.arraycopy(getBytes((float) vec.getX()), 0, result, offset, Float.BYTES);
277 offset += Float.BYTES;
278
279 System.arraycopy(getBytes((float) vec.getY()), 0, result, offset, Float.BYTES);
280 offset += Float.BYTES;
281
282 System.arraycopy(getBytes((float) vec.getZ()), 0, result, offset, Float.BYTES);
283
284 return result;
285 }
286
287 private static byte[] getBytes(final float value) {
288 return getBytes(Float.floatToIntBits(value));
289 }
290
291 private static byte[] getBytes(final int value) {
292 final byte[] bytes = new byte[4];
293 bytes[0] = (byte) (value & 0x000000ff);
294 bytes[1] = (byte) ((value & 0x0000ff00) >> 8);
295 bytes[2] = (byte) ((value & 0x00ff0000) >> 16);
296 bytes[3] = (byte) ((value & 0xff000000) >> 24);
297
298 return bytes;
299 }
300
301 private static byte[] getBytes(final short value) {
302 final byte[] bytes = new byte[2];
303 bytes[0] = (byte) (value & 0x00ff);
304 bytes[1] = (byte) ((value & 0xff00) >> 8);
305
306 return bytes;
307 }
308 }