package rajawali.parser;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import rajawali.materials.TextureManager;
import rajawali.renderer.RajawaliRenderer;
import rajawali.util.LittleEndianDataInputStream;
import rajawali.util.RajLog;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
/**
* STL Parser written using the ASCII format as describe on Wikipedia.
*
* http://en.wikipedia.org/wiki/STL_(file_format)
*
* TODO More testing, Nexus 7 specifically has some issues with certain models. Nexus 4 works fine with same 'problem'
* models.
* TODO Add a feature for ASCII to Binary conversion.
*
* @author Ian Thomas - toxicbakery@gmail.com
*/
public class StlParser extends AMeshParser {
public enum StlType {
UNKNOWN,
ASCII,
BINARY
}
public StlParser(RajawaliRenderer renderer, String fileOnSDCard) {
super(renderer, fileOnSDCard);
}
public StlParser(Resources resources, TextureManager textureManager, int resourceId) {
super(resources, textureManager, resourceId);
}
@Override
public AMeshParser parse() throws ParsingException {
return parse(StlType.UNKNOWN);
}
public AMeshParser parse(StlType type) throws ParsingException {
super.parse();
try {
// Open the file
BufferedReader buffer = null;
LittleEndianDataInputStream dis = null;
switch (type) {
case UNKNOWN:
buffer = getBufferedReader();
// Determine if ASCII or Binary
boolean isASCII = isASCII(buffer);
// Determine ASCII or Binary format
if (isASCII) {
readASCII(buffer);
} else {
// Switch to a LittleEndianDataInputStream (all values in binary are stored in little endian format)
buffer.close();
dis = getLittleEndianInputStream();
readBinary(dis);
}
break;
case ASCII:
buffer = getBufferedReader();
readASCII(buffer);
break;
case BINARY:
dis = getLittleEndianInputStream();
readBinary(dis);
break;
}
// Cleanup
if (buffer != null)
buffer.close();
if (dis != null)
dis.close();
} catch (FileNotFoundException e) {
RajLog.e("[" + getClass().getCanonicalName() + "] Could not find file.");
throw new ParsingException("File not found.", e);
} catch (NumberFormatException e) {
RajLog.e(e.getMessage());
throw new ParsingException("Unexpected value.", e);
} catch (IOException e) {
RajLog.e(e.getMessage());
throw new ParsingException("File reading failed.", e);
} catch (Exception e) {
RajLog.e(e.getMessage());
throw new ParsingException("Unexpected exception occured.", e);
}
return this;
}
private BufferedReader getBufferedReader() throws FileNotFoundException {
BufferedReader buffer = null;
if (mFile == null) {
buffer = new BufferedReader(new InputStreamReader(mResources.openRawResource(mResourceId)));
} else {
buffer = new BufferedReader(new FileReader(mFile));
}
return buffer;
}
private LittleEndianDataInputStream getLittleEndianInputStream() throws FileNotFoundException {
LittleEndianDataInputStream dis = null;
if (mFile == null) {
dis = new LittleEndianDataInputStream(mResources.openRawResource(mResourceId));
} else {
dis = new LittleEndianDataInputStream(new FileInputStream(mFile));
}
return dis;
}
/**
* Read stream as ASCII. While this works well, ASCII is painfully slow on mobile devices due to tight memory
* constraints and lower processing power compared to desktops. It is advisable to use the binary parser whenever
* possible.
*
* @param buffer
* @throws NumberFormatException
* @throws IOException
*/
private void readASCII(final BufferedReader buffer) throws NumberFormatException, IOException {
RajLog.i("StlPaser: Reading ASCII");
final List<Float> vertices = new ArrayList<Float>();
final List<Float> normals = new ArrayList<Float>();
final float[] tempNorms = new float[3];
int nextOffset, prevOffset, i, insert;
String line;
// Skip the first line
line = buffer.readLine();
// Read the facet
while ((line = buffer.readLine()) != null) {
// Only read lines containing facet normal and vertex. No reason to read others
if (line.contains("facet normal ")) {
nextOffset = line.lastIndexOf(" ");
tempNorms[2] = Float.parseFloat(line.substring(nextOffset + 1));
prevOffset = nextOffset;
nextOffset = line.lastIndexOf(" ", prevOffset - 1);
tempNorms[1] = Float.parseFloat(line.substring(nextOffset + 1, prevOffset));
prevOffset = nextOffset;
nextOffset = line.lastIndexOf(" ", prevOffset - 1);
tempNorms[0] = Float.parseFloat(line.substring(nextOffset + 1, prevOffset));
// Need to duplicate the normal for each vertex of the triangle
for (i = 0; i < 3; i++) {
normals.add(tempNorms[0]);
normals.add(tempNorms[1]);
normals.add(tempNorms[2]);
}
} else if (line.contains("vertex ")) {
insert = vertices.size();
nextOffset = line.lastIndexOf(" ");
vertices.add(Float.parseFloat(line.substring(nextOffset + 1)));
prevOffset = nextOffset;
nextOffset = line.lastIndexOf(" ", prevOffset - 1);
vertices.add(insert, Float.parseFloat(line.substring(nextOffset + 1, prevOffset)));
prevOffset = nextOffset;
nextOffset = line.lastIndexOf(" ", prevOffset - 1);
vertices.add(insert, Float.parseFloat(line.substring(nextOffset + 1, prevOffset)));
}
}
float[] verticesArr = new float[vertices.size()];
float[] normalsArr = new float[normals.size()];
for (i = 0; i < verticesArr.length; i++) {
verticesArr[i] = vertices.get(i);
normalsArr[i] = normals.get(i);
}
// Cleanup
vertices.clear();
normals.clear();
int[] indicesArr = new int[verticesArr.length / 3];
for (i = 0; i < indicesArr.length; i++)
indicesArr[i] = i;
mRootObject.setData(verticesArr, normalsArr, null, null, indicesArr);
}
/**
* Read stream as binary STL. This is significantly faster than ASCII parsing. Additionally binary files are much
* more compressed allowing smaller file sizes for larger models compared to ASCII.
*
* @param dis
* @throws IOException
*/
private void readBinary(final LittleEndianDataInputStream dis) throws IOException {
RajLog.i("StlPaser: Reading Binary");
// Skip the header
dis.skip(80);
// Read the number of facets (have to convert the uint to a long
int facetCount = dis.readInt();
float[] verticesArr = new float[facetCount * 9];
float[] normalsArr = new float[facetCount * 9];
int[] indicesArr = new int[facetCount * 3];
float[] tempNorms = new float[3];
int vertPos = 0, normPos = 0;
for (int i = 0; i < indicesArr.length; i++)
indicesArr[i] = i;
// Read all the facets
while (dis.available() > 0) {
// Read normals
for (int j = 0; j < 3; j++) {
tempNorms[j] = dis.readFloat();
if (Float.isNaN(tempNorms[j]) || Float.isInfinite(tempNorms[j])) {
RajLog.w("STL contains bad normals of NaN or Infinite!");
tempNorms[0] = 0;
tempNorms[1] = 0;
tempNorms[2] = 0;
break;
}
}
for (int j = 0; j < 3; j++) {
normalsArr[normPos++] = tempNorms[0];
normalsArr[normPos++] = tempNorms[1];
normalsArr[normPos++] = tempNorms[2];
}
// Read vertices
for (int j = 0; j < 9; j++)
verticesArr[vertPos++] = dis.readFloat();
dis.skip(2);
}
mRootObject.setData(verticesArr, normalsArr, null, null, indicesArr);
}
/**
* Determine if a given file appears to be in ASCII format.
*
* @param file
* @return
* @throws IOException
* @throws StlParseException
*/
public static final boolean isASCII(File file) throws IOException, StlParseException {
if (file.exists())
throw new StlParseException("Passed file does not exist.");
if (!file.isFile())
throw new StlParseException("This is not a file.");
final BufferedReader buffer = new BufferedReader(new FileReader(file));
boolean isASCII = isASCII(buffer);
buffer.close();
return isASCII;
}
/**
* Determine if a given resource appears to be in ASCII format.
*
* @param res
* @param resId
* @return
* @throws IOException
*/
public static final boolean isASCII(Resources res, int resId) throws IOException, NotFoundException {
BufferedReader buffer = new BufferedReader(new InputStreamReader(res.openRawResource(resId)));
boolean isASCII = isASCII(buffer);
buffer.close();
return isASCII;
}
/**
* Determine if a given BufferedReader appears to be in ASCII format.
*
* @param buffer
* @return
* @throws IOException
*/
public static final boolean isASCII(BufferedReader buffer) throws IOException {
final char[] readAhead = new char[300];
buffer.mark(readAhead.length);
buffer.read(readAhead, 0, readAhead.length);
buffer.reset();
final String readAheadString = new String(readAhead);
// If the following text is present, then this is likely an ascii file
if (readAheadString.contains("facet normal") && readAheadString.contains("outer loop"))
return true;
// Likely a binary file
return false;
}
public static final class StlParseException extends ParsingException {
private static final long serialVersionUID = -5098120548044169618L;
public StlParseException(String msg) {
super(msg);
}
}
}