001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.io.euclidean.threed.stl;
018
019import java.io.Closeable;
020import java.io.OutputStream;
021import java.nio.ByteBuffer;
022
023import org.apache.commons.geometry.euclidean.threed.Vector3D;
024import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
025
026/** Low-level class for writing binary STL content.
027 */
028public class BinaryStlWriter implements Closeable {
029
030    /** Output stream to write to. */
031    private final OutputStream out;
032
033    /** Buffer used to construct triangle definitions. */
034    private final ByteBuffer triangleBuffer = StlUtils.byteBuffer(StlConstants.BINARY_TRIANGLE_BYTES);
035
036    /** Construct a new instance for writing to the given output.
037     * @param out output stream to write to
038     */
039    public BinaryStlWriter(final OutputStream out) {
040        this.out = out;
041    }
042
043    /** Write binary STL header content. If {@code headerContent} is null, the written header
044     * will consist entirely of zeros. Otherwise, up to 80 bytes from {@code headerContent}
045     * are written to the header, with any remaining bytes of the header filled with zeros.
046     * @param headerContent bytes to include in the header; may be null
047     * @param triangleCount number of triangles to be included in the content
048     * @throws java.io.UncheckedIOException if an I/O error occurs
049     */
050    public void writeHeader(final byte[] headerContent, final int triangleCount) {
051        writeHeader(headerContent, triangleCount, out);
052    }
053
054    /** Write a triangle to the output using a default attribute value of 0.
055     * Callers are responsible for ensuring that the number of triangles written
056     * matches the number given in the header.
057     *
058     * <p>If a normal is given, the vertices are ordered using the right-hand rule,
059     * meaning that they will be in a counter-clockwise orientation when looking down
060     * the normal. Thus, the given point ordering may not be the ordering used in
061     * the written content.</p>
062     * @param p1 first point
063     * @param p2 second point
064     * @param p3 third point
065     * @param normal triangle normal; may be null
066     * @throws java.io.UncheckedIOException if an I/O error occurs
067     */
068    public void writeTriangle(final Vector3D p1, final Vector3D p2, final Vector3D p3,
069            final Vector3D normal) {
070        writeTriangle(p1, p2, p3, normal, 0);
071    }
072
073    /** Write a triangle to the output. Callers are responsible for ensuring
074     * that the number of triangles written matches the number given in the header.
075     *
076     * <p>If a non-zero normal is given, the vertices are ordered using the right-hand rule,
077     * meaning that they will be in a counter-clockwise orientation when looking down
078     * the normal. If no normal is given, or the given value cannot be normalized, a normal
079     * is computed from the triangle vertices, also using the right-hand rule. If this also
080     * fails (for example, if the triangle vertices do not define a plane), then the
081     * zero vector is used.</p>
082     * @param p1 first point
083     * @param p2 second point
084     * @param p3 third point
085     * @param normal triangle normal; may be null
086     * @param attributeValue 2-byte STL triangle attribute value
087     * @throws java.io.UncheckedIOException if an I/O error occurs
088     */
089    public void writeTriangle(final Vector3D p1, final Vector3D p2, final Vector3D p3,
090            final Vector3D normal, final int attributeValue) {
091        triangleBuffer.rewind();
092
093        putVector(StlUtils.determineNormal(p1, p2, p3, normal));
094        putVector(p1);
095
096        if (StlUtils.pointsAreCounterClockwise(p1, p2, p3, normal)) {
097            putVector(p2);
098            putVector(p3);
099        } else {
100            putVector(p3);
101            putVector(p2);
102        }
103
104        triangleBuffer.putShort((short) attributeValue);
105
106        GeometryIOUtils.acceptUnchecked(out::write, triangleBuffer.array());
107    }
108
109    /** {@inheritDoc} */
110    @Override
111    public void close() {
112        GeometryIOUtils.closeUnchecked(out);
113    }
114
115    /** Put all double components of {@code vec} into the internal buffer.
116     * @param vec vector to place into the buffer
117     */
118    private void putVector(final Vector3D vec) {
119        triangleBuffer.putFloat((float) vec.getX());
120        triangleBuffer.putFloat((float) vec.getY());
121        triangleBuffer.putFloat((float) vec.getZ());
122    }
123
124    /** Write binary STL header content to the given output stream. If {@code headerContent}
125     * is null, the written header will consist entirely of zeros. Otherwise, up to 80 bytes
126     * from {@code headerContent} are written to the header, with any remaining bytes of the
127     * header filled with zeros.
128     * @param headerContent
129     * @param triangleCount
130     * @param out
131     * @throws java.io.UncheckedIOException if an I/O error occurs
132     */
133    static void writeHeader(final byte[] headerContent, final int triangleCount, final OutputStream out) {
134        // write the header
135        final byte[] bytes = new byte[StlConstants.BINARY_HEADER_BYTES];
136        if (headerContent != null) {
137            System.arraycopy(
138                    headerContent, 0,
139                    bytes, 0,
140                    Math.min(headerContent.length, StlConstants.BINARY_HEADER_BYTES));
141        }
142
143        GeometryIOUtils.acceptUnchecked(out::write, bytes);
144
145        // write the triangle count number
146        ByteBuffer countBuffer = StlUtils.byteBuffer(Integer.BYTES);
147        countBuffer.putInt(triangleCount);
148        countBuffer.flip();
149
150        GeometryIOUtils.acceptUnchecked(out::write, countBuffer.array());
151    }
152}