package org.reprap.geometry.polyhedra; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.vecmath.Matrix4d; import org.reprap.CSGOp; import org.reprap.utilities.Debug; /** * This class reads in an OpenSCAD (http://openscad.org) CSG expression, parses it, and uses the result to * build a CSG3D model. * * @author ensab * */ public class CSGReader { private static final String group = "group(){"; private static final String difference = "difference(){"; private static final String union = "union(){"; private static final String intersection = "intersection(){"; private static final String multmatrix = "multmatrix("; private static final String cube = "cube("; private static final String cylinder = "cylinder("; private static final String sphere = "sphere("; private static final String[] starts = { group, difference, union, intersection, multmatrix, cube, cylinder, sphere }; private static final String[] cubeArgs = { "size=", "center=" }; private static final String[] cylinderArgs = { "$fn=", "$fa=", "$fs=", "h=", "r1=", "r2=", "center=" }; private static final String[] sphereArgs = { "$fn=", "$fa=", "$fs=", "r=" }; private String model=""; private String laggingModel=""; private CSG3D CSGModel = null; private boolean csgAvailable = false; /** * The constructor just checks the file and loads the CSG expression into a String. * @param fileName */ public CSGReader(String fileName) { csgAvailable = readModel(fileName); } /** * Check if the constructor found a model. * @return */ public boolean csgAvailable() { return csgAvailable; } /** * Return the model found by the constructor. Do lazy * evaluation as far as parsing is concerned. * @return */ public CSG3D csg() { ArrayList<CSG3D> c; if(CSGModel == null) { c = parseModel(); if(model.length() > 0) Debug.d("Unparsed: " + model); if(c.size() != 1) Debug.e("CSGReader.csg() - model contains " + c.size() + " separate elements. Did you mean to union them?"); CSGModel = c.get(0); } return CSGModel; } /** * For a given STL file, find if there's a CSG file for it * in the same directory that we can read. * @param STLfileName * @return */ public static String CSGFileExists(String STLfileName) { String fileName = new String(STLfileName); if(fileName.toLowerCase().endsWith(".stl")) fileName = fileName.substring(0, fileName.length()-4) + ".csg"; else return null; if(fileName.startsWith("file:")) fileName = fileName.substring(5, fileName.length()); try { BufferedReader inputStream = new BufferedReader(new FileReader(fileName)); return fileName; } catch (FileNotFoundException e) { return null; } } /** * Read a CSG model from OpenSCAD into a string. * Remove the line numbers ("n12:" etc), and all white space. * @param STLfileName * @return */ private boolean readModel(String STLfileName) { String fileName = CSGFileExists(STLfileName); if(fileName == null) return false; model = new String(); try { BufferedReader inputStream = new BufferedReader(new FileReader(fileName)); String line; while ((line = inputStream.readLine()) != null) { line = line.replaceAll("^\\s+", ""); // kill leading white space if(line.startsWith("n")) // kill line number { int cs = line.indexOf(":"); if(cs < 0) Debug.e("CSGReader.readModel() line number not ending in : ... " + line); else line = line.substring(cs+1); } line = line.replaceAll("^\\s+", ""); // kill more leading white space model += line; } } catch (FileNotFoundException e) { return false; } catch (IOException e) { return false; } model = model.replaceAll("\\s+", ""); // kill all remaining white space laggingModel = new String(model); Debug.d("CSGReader: read CSG model from: " + fileName); return true; } /** * The equivalent of the String.substring() function, but * it also maintains a lagging string with a few prior characters in * for error messages. * @param n */ private void subString(int n) { if(laggingModel.length() - model.length() > 10) laggingModel = laggingModel.substring(n); model = model.substring(n); } /** * String for a bit of the model around where we are parsing * @return */ private String printABitAbout() { return laggingModel.substring(0, Math.min(50, laggingModel.length())); } /** * String for a bit of the model exactly where we are parsing * @return */ private String printABit() { return model.substring(0, Math.min(50, model.length())); } /** * parse an integer terminated by a "," * @return */ private int parseIC() { int c = model.indexOf(","); if(c <= 0) { Debug.e("CSGReader.parseDCI() - expecting , ...got: " + printABit() + "..."); return 0; } String i = model.substring(0, c); subString(c+1); return Integer.valueOf(i); } /** * parse a double terminated by a "," * @return */ private double parseDC() { int c = model.indexOf(","); if(c <= 0) { Debug.e("CSGReader.parseDC() - expecting , ...got: " + printABit() + "..."); return 0; } String d = model.substring(0, c); subString(c+1); return Double.valueOf(d); } /** * parse a double terminated by a "]" * @return */ private double parseDB() { int c = model.indexOf("]"); if(c <= 0) { Debug.e("CSGReader.parseDB() - expecting ] ...got: " + printABit() + "..."); return 0; } String d = model.substring(0, c); subString(c+1); return Double.valueOf(d); } /** * parse a double terminated by a ")" * @return */ private double parseDb() { int c = model.indexOf(")"); if(c <= 0) { Debug.e("CSGReader.parseDb() - expecting ) ...got: " + printABit() + "..."); return 0; } String d = model.substring(0, c); subString(c+1); return Double.valueOf(d); } /** * Parse true/false * @return */ private boolean parseBoolean() { boolean result = model.startsWith("true"); if(result) subString(4); else { if(!model.startsWith("false")) Debug.e("CSGReader.parseBoolean() - expecting true or false ...got: " + printABit() + "..."); else subString(5); } return result; } /** * Parse a vector like "[1.2,-5.6,3.8]" * @param e length of vector * @return */ private double[] parseV(int e) { double[] r = new double[e]; if(!model.startsWith("[")) Debug.e("CSGReader.parseV() - expecting [ ...got: " + printABit() + "..."); subString(1); for(int i = 0; i < e-1 ; i++) r[i] = parseDC(); r[e-1] = parseDB(); return r; } /** * Parse a cube of the form: * "cube(size=[10,20,30],center=false);" * @return */ private CSG3D parseCube() { subString(cube.length()); if(!model.startsWith(cubeArgs[0])) Debug.e("CSGReader.parseCuber() - expecting: " + cubeArgs[0] + " ...got: " + printABit() + "..."); subString(cubeArgs[0].length()); double [] s = parseV(3); subString(1); // get rid of "," if(!model.startsWith(cubeArgs[1])) Debug.e("CSGReader.parseCube() - expecting: " + cubeArgs[1] + " ...got: " + printABit() + "..."); subString(cubeArgs[1].length()); boolean c = parseBoolean(); if(!model.startsWith(");")) Debug.e("CSGReader.parseCube() - expecting ); ...got: " + printABit() + "..."); subString(2); return Primitives.cube(s[0], s[1], s[2], c); } /** * Parse a cylinder of the form: * "cylinder($fn=20,$fa=12,$fs=1,h=3,r1=2,r2=2,center=false);" * * @return */ private CSG3D parseCylinder() { subString(cylinder.length()); if(!model.startsWith(cylinderArgs[0])) Debug.e("CSGReader.parseCylinder() - expecting: " + cylinderArgs[0] + " ...got: " + printABit() + "..."); subString(cylinderArgs[0].length()); int fn = parseIC(); if(!model.startsWith(cylinderArgs[1])) Debug.e("CSGReader.parseCylinder() - expecting: " + cylinderArgs[1] + " ...got: " + printABit() + "..."); subString(cylinderArgs[1].length()); double fa = parseDC(); if(!model.startsWith(cylinderArgs[2])) Debug.e("CSGReader.parseCylinder() - expecting: " + cylinderArgs[2] + " ...got: " + printABit() + "..."); subString(cylinderArgs[2].length()); double fs = parseDC(); if(!model.startsWith(cylinderArgs[3])) Debug.e("CSGReader.parseCylinder() - expecting: " + cylinderArgs[3] + " ...got: " + printABit() + "..."); subString(cylinderArgs[3].length()); double h = parseDC(); if(!model.startsWith(cylinderArgs[4])) Debug.e("CSGReader.parseCylinder() - expecting: " + cylinderArgs[4] + " ...got: " + printABit() + "..."); subString(cylinderArgs[4].length()); double r1 = parseDC(); if(!model.startsWith(cylinderArgs[5])) Debug.e("CSGReader.parseCylinder() - expecting: " + cylinderArgs[5] + " ...got: " + printABit() + "..."); subString(cylinderArgs[5].length()); double r2 = parseDC(); if(!model.startsWith(cylinderArgs[6])) Debug.e("CSGReader.parseCylinder() - expecting: " + cylinderArgs[6] + " ...got: " + printABit() + "..."); subString(cylinderArgs[6].length()); boolean c = parseBoolean(); if(!model.startsWith(");")) Debug.e("CSGReader.parseCylinder() - expecting ); ...got: " + printABit() + "..."); subString(2); return Primitives.cylinder(fn, fa, fs, h, r1, r2, c); } /** * Parse a sphere of the form: * sphere($fn=0,$fa=12,$fs=1,r=10); * */ private CSG3D parseSphere() { subString(sphere.length()); if(!model.startsWith(sphereArgs[0])) Debug.e("CSGReader.parseSphere() - expecting: " + sphereArgs[0] + " ...got: " + printABit() + "..."); subString(sphereArgs[0].length()); int fn = parseIC(); if(!model.startsWith(sphereArgs[1])) Debug.e("CSGReader.parseSphere() - expecting: " + sphereArgs[1] + " ...got: " + printABit() + "..."); subString(sphereArgs[1].length()); double fa = parseDC(); if(!model.startsWith(sphereArgs[2])) Debug.e("CSGReader.parseSphere() - expecting: " + sphereArgs[2] + " ...got: " + printABit() + "..."); subString(sphereArgs[2].length()); double fs = parseDC(); if(!model.startsWith(sphereArgs[3])) Debug.e("CSGReader.parseSphere() - expecting: " + sphereArgs[3] + " ...got: " + printABit() + "..."); subString(sphereArgs[3].length()); double r = parseDb(); if(!model.startsWith(";")) Debug.e("CSGReader.parseSphere() - expecting ; ...got: " + printABit() + "..."); subString(1); return Primitives.sphere(fn, fa, fs, r); } /** * Parse a 4x4 matrix of the form: * "[[6.12303e-17,0,1,0],[0,1,0,0],[-1,0,6.12303e-17,0],[0,0,0,1]])" * @return */ private Matrix4d parseMatrix() { if(!model.startsWith("[")) Debug.e("CSGReader.parseMatrix() - expecting [ ...got: " + printABit() + "..."); subString(1); Matrix4d m = new Matrix4d(); double[] v = parseV(4); subString(1); // Dump "," m.m00 = v[0]; m.m01 = v[1]; m.m02 = v[2]; m.m03 = v[3]; v = parseV(4); subString(1); // Dump "," m.m10 = v[0]; m.m11 = v[1]; m.m12 = v[2]; m.m13 = v[3]; v = parseV(4); subString(1); // Dump "," m.m20 = v[0]; m.m21 = v[1]; m.m22 = v[2]; m.m23 = v[3]; v = parseV(4); m.m30 = v[0]; m.m31 = v[1]; m.m32 = v[2]; m.m33 = v[3]; if(!model.startsWith("]")) Debug.e("CSGReader.parseMatrix() - expecting ] ...got: " + printABit() + "..."); subString(1); return m; } /** * Find out if the next thing is a valid starter for a sub-model * @return */ private boolean startNext() { for(int i = 0; i < starts.length; i++) { if(model.startsWith(starts[i])) return true; } return false; } /** * Transform a CSG object * @return */ private ArrayList<CSG3D> parseTransform() { subString(multmatrix.length()); Matrix4d transform; transform = parseMatrix(); if(!model.startsWith("){")) Debug.e("CSGReader.parseTransform() - expecting ){ ...got: " + printABit() + "..."); else subString(2); ArrayList<CSG3D> result = new ArrayList<CSG3D>(); if(model.startsWith("}")) // Nothing there? { subString(1); return result; } ArrayList<CSG3D> r1 = parseMultipleOperands(); //Should be parseModel(), but the user can be dumb sometimes; protect him. for(int i = 0; i < r1.size(); i++) result.add(r1.get(i).transform(transform)); if(!model.startsWith("}")) Debug.e("CSGReader.parseTransform() - expecting } ...got: " + printABit() + "..."); else subString(1); return result; } /** * Union, intersection, or difference * The first operand must be a single item. * The second operand is a list of items of length 0 or more. * @param operator * @return */ private CSG3D parseCSGOperation(CSGOp operator) { CSG3D leftOperand; ArrayList<CSG3D> c, rightOperand, temp; c = parseModel(); leftOperand = c.get(0); rightOperand = new ArrayList<CSG3D>(); if(c.size() != 1) { Debug.e("CSGReader.parseModel() " + operator + " - first operand is not a singleton ...got: " + printABitAbout() + "..."); for(int i = 1; i < c.size(); i++) rightOperand.add(c.get(i)); } temp = parseMultipleOperands(); for(int i = 0; i < temp.size(); i++) rightOperand.add(temp.get(i)); switch(operator) { case UNION: for(int i = 0; i < rightOperand.size(); i++) leftOperand = CSG3D.union(leftOperand, rightOperand.get(i)); break; case INTERSECTION: for(int i = 0; i < rightOperand.size(); i++) leftOperand = CSG3D.intersection(leftOperand, rightOperand.get(i)); break; case DIFFERENCE: for(int i = 0; i < rightOperand.size(); i++) leftOperand = CSG3D.difference(leftOperand, rightOperand.get(i)); break; default: Debug.e("CSGReader.parseCSGOperation() illegal operator: " + operator); } return leftOperand; } /** * get a whole list of things (including none) * @return */ private ArrayList<CSG3D> parseMultipleOperands() { ArrayList<CSG3D> result = new ArrayList<CSG3D>(); ArrayList<CSG3D> c; while(startNext()) { c = parseModel(); for(int i = 0; i < c.size(); i++) result.add(c.get(i)); } return result; } /** * The master parsing function that does a recursive descent through * the model, parsing it all and returning the final CSG object. * @return */ private ArrayList<CSG3D> parseModel() { //System.out.println("parsing: " + model.substring(0, Math.min(50, model.length()))); ArrayList<CSG3D> result = new ArrayList<CSG3D>(); if(model.startsWith("{")) { Debug.d("CSGReader.parseModel(): unattached {"); subString(1); result = parseMultipleOperands(); if(!model.startsWith("}")) Debug.e("CSGReader.parseModel() { - expecting } ...got: " + printABit() + "..."); else subString(1); return result; } else if(model.startsWith(group)) { subString(group.length()); result = parseMultipleOperands(); if(!model.startsWith("}")) Debug.e("CSGReader.parseModel() group(){ - expecting } ...got: " + printABit() + "..."); else subString(1); return result; } else if(model.startsWith("}")) { subString(1); Debug.e("CSGReader.parseModel() - unexpected } encountered: " + printABit() + "..."); return result; } else if(model.startsWith(difference)) { subString(difference.length()); result.add(parseCSGOperation(CSGOp.DIFFERENCE)); if(!model.startsWith("}")) Debug.e("CSGReader.parseModel() difference(){ - expecting } ...got: " + printABit() + "..."); else subString(1); return result; } else if(model.startsWith(union)) { subString(union.length()); result.add(parseCSGOperation(CSGOp.UNION)); if(!model.startsWith("}")) Debug.e("CSGReader.parseModel() union(){ - expecting } ...got: " + printABit() + "..."); else subString(1); return result; } else if(model.startsWith(intersection)) { subString(intersection.length()); result.add(parseCSGOperation(CSGOp.INTERSECTION)); if(!model.startsWith("}")) Debug.e("CSGReader.parseModel() intersection(){ - expecting } ...got: " + printABit() + "..."); else subString(1); return result; } else if(model.startsWith(multmatrix)) { return parseTransform(); } else if(model.startsWith(cube)) { result.add(parseCube()); return result; } else if(model.startsWith(cylinder)) { result.add(parseCylinder()); return result; } else if(model.startsWith(sphere)) { result.add(parseSphere()); return result; } Debug.e("CSGReader.parseModel() - unsupported item: " + printABit() + "..."); return result; } }