Просмотр исходного кода

Merging all shapes into a single mesh during X3D loading

Seva Alekseyev 8 лет назад
Родитель
Сommit
a27f82e64b
1 измененных файлов с 318 добавлено и 226 удалено
  1. 318 226
      plugins/X3DReader/X3DReader.py

+ 318 - 226
plugins/X3DReader/X3DReader.py

@@ -11,6 +11,7 @@ from UM.Job import Job
 from math import pi, sin, cos, sqrt
 import numpy
 
+
 EPSILON = 0.000001 # So very crude. :(
 
 try:
@@ -18,9 +19,19 @@ try:
 except ImportError:
     import xml.etree.ElementTree as ET
     
+# TODO: preserve the structure of scenes that contain several objects
+# Use CADPart, for example, to distinguish between separate objects    
     
-DEFAULT_SUBDIV = 16 # Default subdivision factor for spheres, cones, and cylinders    
+DEFAULT_SUBDIV = 16 # Default subdivision factor for spheres, cones, and cylinders
 
+class Shape:
+    def __init__(self, v, f, ib, n):
+        self.verts = v
+        self.faces = f
+        # Those are here for debugging purposes only
+        self.index_base = ib 
+        self.name = n
+        
 class X3DReader(MeshReader):
     def __init__(self):
         super().__init__()
@@ -32,102 +43,97 @@ class X3DReader(MeshReader):
     def read(self, file_name):
         try:
             self.defs = {}
-            self.sceneNodes = []
-            self.fileName = file_name
+            self.shapes = []
             
             tree = ET.parse(file_name)
-            root = tree.getroot()
+            xml_root = tree.getroot()
             
-            if root.tag != "X3D":
+            if xml_root.tag != "X3D":
                 return None
 
             scale = 1000 # Default X3D unit it one meter, while Cura's is one millimeters            
-            if root[0].tag == "head":
-                for headNode in root[0]:
-                    if headNode.tag == "unit" and headNode.attrib.get("category") == "length":
-                        scale *= float(headNode.attrib["conversionFactor"])
+            if xml_root[0].tag == "head":
+                for head_node in xml_root[0]:
+                    if head_node.tag == "unit" and head_node.attrib.get("category") == "length":
+                        scale *= float(head_node.attrib["conversionFactor"])
                         break 
-                scene = root[1]
+                xml_scene = xml_root[1]
             else:
-                scene = root[0]
+                xml_scene = xml_root[0]
                 
-            if scene.tag != "Scene":
+            if xml_scene.tag != "Scene":
                 return None 
             
             self.transform = Matrix()
             self.transform.setByScaleFactor(scale)
+            self.index_base = 0
             
-            # Traverse the scene tree, populate the sceneNodes array
-            self.processChildNodes(scene)
+            # Traverse the scene tree, populate the shapes list
+            self.processChildNodes(xml_scene)
             
-            if len(self.sceneNodes) > 1:
-                theScene = SceneNode()
-                group_decorator = GroupDecorator()
-                theScene.addDecorator(group_decorator)
-                for node in self.sceneNodes:
-                    theScene.addChild(node)
-                theScene.setSelectable(True)
-            elif len(self.sceneNodes) == 1:
-                theScene = self.sceneNodes[0]
-            else: # No shapes read :(
+            if self.shapes:
+                bui = MeshBuilder()
+                bui.setVertices(numpy.concatenate([shape.verts for shape in self.shapes]))
+                bui.setIndices(numpy.concatenate([shape.faces for shape in self.shapes]))
+                bui.calculateNormals()
+                bui.setFileName(file_name)
+                    
+                scene = SceneNode()
+                scene.setMeshData(bui.build().getTransformed(Matrix()))
+                scene.setSelectable(True)
+                scene.setName(file_name)
+            else:
                 return None
-            theScene.setName(file_name)
+            
         except Exception as e:
             Logger.log("e", "exception occured in x3d reader: %s", e)
 
         try:
-            boundingBox = theScene.getBoundingBox()
+            boundingBox = scene.getBoundingBox()
             boundingBox.isValid()
         except:
             return None
 
-        return theScene
+        return scene
     
     # ------------------------- XML tree traversal
   
-    def processNode(self, xmlNode):
-        xmlNode =  self.resolveDefUse(xmlNode)
-        if xmlNode is None:
+    def processNode(self, xml_node):
+        xml_node =  self.resolveDefUse(xml_node)
+        if xml_node is None:
             return
         
-        tag = xmlNode.tag
-        if tag in ("Group", "StaticGroup", "CADAssembly", "CADFace", "CADLayer", "CADPart", "Collision"):
-            self.processChildNodes(xmlNode)
+        tag = xml_node.tag
+        if tag in ("Group", "StaticGroup", "CADAssembly", "CADFace", "CADLayer", "Collision"):
+            self.processChildNodes(xml_node)
+        if tag == "CADPart":
+            self.processTransform(xml_node) # TODO: split the parts
         elif tag == "LOD":
-            self.processNode(xmlNode[0])
+            self.processNode(xml_node[0])
         elif tag == "Transform":
-            self.processTransform(xmlNode)
+            self.processTransform(xml_node)
         elif tag == "Shape":
-            self.processShape(xmlNode)
+            self.processShape(xml_node)
             
             
-    def processShape(self, xmlNode):
+    def processShape(self, xml_node):
         # Find the geometry and the appearance inside the Shape
         geometry = appearance = None
-        for subNode in xmlNode:
+        for subNode in xml_node:
             if subNode.tag == "Appearance" and not appearance:
                 appearance = self.resolveDefUse(subNode)
-            elif subNode.tag in self.geometryImporters and not geometry:
+            elif subNode.tag in self.geometry_importers and not geometry:
                 geometry = self.resolveDefUse(subNode)
         
         # TODO: appearance is completely ignored. At least apply the material color...        
         if not geometry is None:
             try:
-                bui = MeshBuilder()
-                self.geometryImporters[geometry.tag](self, geometry, bui)
-                
-                bui.calculateNormals()
-                bui.setFileName(self.fileName)
-                
-                sceneNode = SceneNode()
-                if "DEF" in geometry.attrib:
-                    sceneNode.setName(geometry.tag + "#" + geometry.attrib["DEF"])
-                else:
-                    sceneNode.setName(geometry.tag)
-                     
-                sceneNode.setMeshData(bui.build().getTransformed(self.transform))
-                sceneNode.setSelectable(True)
-                self.sceneNodes.append(sceneNode)
+                self.verts = self.faces = [] # Safeguard 
+                self.geometry_importers[geometry.tag](self, geometry)
+                m = self.transform.getData()
+                verts = numpy.array([m.dot(vert)[:3] for vert in self.verts])
+                self.shapes.append(Shape(verts, self.faces, self.index_base, geometry.tag))
+                self.index_base += len(verts)
                 
             except Exception as e:
                 Logger.log("e", "exception occured in x3d reader while reading %s: %s", geometry.tag, e)
@@ -198,12 +204,33 @@ class X3DReader(MeshReader):
     
     # Primitives
 
-    def geomBox(self, node, bui):
-        size = readFloatArray(node, "size", [2, 2, 2])
-        bui.addCube(size[0], size[1], size[2])
+    def geomBox(self, node):
+        (dx, dy, dz) = readFloatArray(node, "size", [2, 2, 2])
+        dx /= 2
+        dy /= 2
+        dz /= 2
+        self.reserveFaceAndVertexCount(12, 8)
+
+        # xz plane at +y, ccw
+        self.addVertex(dx, dy, dz)
+        self.addVertex(-dx, dy, dz)
+        self.addVertex(-dx, dy, -dz)
+        self.addVertex(dx, dy, -dz)
+        # xz plane at -y
+        self.addVertex(dx, -dy, dz)
+        self.addVertex(-dx, -dy, dz)
+        self.addVertex(-dx, -dy, -dz)
+        self.addVertex(dx, -dy, -dz)
+        
+        self.addQuad(0, 1, 2, 3)   # +y
+        self.addQuad(4, 0, 3, 7)   # +x
+        self.addQuad(7, 3, 2, 6)   # -z
+        self.addQuad(6, 2, 1, 5)   # -x
+        self.addQuad(5, 1, 0, 4)   # +z
+        self.addQuad(7, 6, 5, 4)  # -y
     
     # The sphere is subdivided into nr rings and ns segments
-    def geomSphere(self, node, bui):
+    def geomSphere(self, node):
         r = readFloat(node, "radius", 0.5)
         subdiv = readIntArray(node, 'subdivision', None)
         if subdiv:
@@ -217,17 +244,17 @@ class X3DReader(MeshReader):
         lau = pi / nr  # Unit angle of latitude (rings) for the given tesselation
         lou = 2 * pi / ns  # Unit angle of longitude (segments)
         
-        bui.reserveFaceAndVertexCount(ns*(nr*2 - 2), 2 + (nr + 1)*ns)
+        self.reserveFaceAndVertexCount(ns*(nr*2 - 2), 2 + (nr - 1)*ns)
         
         # +y and -y poles
-        bui.addVertex(0, r, 0)
-        bui.addVertex(0, -r, 0)
+        self.addVertex(0, r, 0)
+        self.addVertex(0, -r, 0)
         
         # The non-polar vertices go from x=0, negative z plane counterclockwise -
         # to -x, to +z, to +x, back to -z
         for ring in range(1, nr):
             for seg in range(ns):
-                bui.addVertex(-r*sin(lou * seg) * sin(lau * ring),
+                self.addVertex(-r*sin(lou * seg) * sin(lau * ring),
                           r*cos(lau * ring),
                           -r*cos(lou * seg) * sin(lau * ring))
                 
@@ -241,8 +268,8 @@ class X3DReader(MeshReader):
         # (starting from +y pole)
         # Bottom cap goes: up left down (starting from -y pole)
         for seg in range(ns):
-            addTri(bui, 0, seg + 2, (seg + 1) % ns + 2)
-            addTri(bui, 1, vb + (seg + 1) % ns, vb + seg)
+            self.addTri(0, seg + 2, (seg + 1) % ns + 2)
+            self.addTri(1, vb + (seg + 1) % ns, vb + seg)
     
         # Sides
         # Side face vertices go in order:  down right upleft, downright up left
@@ -253,9 +280,9 @@ class X3DReader(MeshReader):
             # First vertex index for the bottom edge of the ring
             for seg in range(ns):
                 nseg = (seg + 1) % ns
-                addQuad(bui, tvb + seg, bvb + seg, bvb + nseg, tvb + nseg)
+                self.addQuad(tvb + seg, bvb + seg, bvb + nseg, tvb + nseg)
         
-    def geomCone(self, node, bui):
+    def geomCone(self, node):
         r = readFloat(node, "bottomRadius", 1)
         height = readFloat(node, "height", 2)
         bottom = readBoolean(node, "bottom", True)
@@ -265,21 +292,22 @@ class X3DReader(MeshReader):
         d = height / 2
         angle = 2 * pi / n
     
-        bui.reserveFaceAndVertexCount((n if side else 0) + (n-1 if bottom else 0), n+1)
+        self.reserveFaceAndVertexCount((n if side else 0) + (n-2 if bottom else 0), n+1)
     
-        bui.addVertex(0, d, 0)
+        # Vertex 0 is the apex, vertices 1..n are the bottom
+        self.addVertex(0, d, 0)
         for i in range(n):
-            bui.addVertex(-r * sin(angle * i), -d, -r * cos(angle * i))
+            self.addVertex(-r * sin(angle * i), -d, -r * cos(angle * i))
                     
         # Side face vertices go: up down right
         if side:
             for i in range(n):
-                addTri(bui, 1 + (i + 1) % n, 0, 1 + i)
+                self.addTri(1 + (i + 1) % n, 0, 1 + i)
         if bottom:
             for i in range(2, n):
-                addTri(bui, 1, i, i+1)
+                self.addTri(1, i, i+1)
     
-    def geomCylinder(self, node, bui):
+    def geomCylinder(self, node):
         r = readFloat(node, "radius", 1)
         height = readFloat(node, "height", 2)
         bottom = readBoolean(node, "bottom", True)
@@ -291,30 +319,30 @@ class X3DReader(MeshReader):
         angle = 2 * pi / n
         hh = height/2
         
-        bui.reserveFaceAndVertexCount((nn if side else 0) + (n - 2 if top else 0) + (n - 2 if bottom else 0), nn)
+        self.reserveFaceAndVertexCount((nn if side else 0) + (n - 2 if top else 0) + (n - 2 if bottom else 0), nn)
         
         # The seam is at x=0, z=-r, vertices go ccw -
         # to pos x, to neg z, to neg x, back to neg z
         for i in range(n):
             rs = -r * sin(angle * i)
             rc = -r * cos(angle * i)
-            bui.addVertex(rs, hh, rc)
-            bui.addVertex(rs, -hh, rc)
+            self.addVertex(rs, hh, rc)
+            self.addVertex(rs, -hh, rc)
         
         if side:
             for i in range(n):
                 ni = (i + 1) % n
-                addQuad(bui, ni * 2 + 1, ni * 2, i * 2, i * 2 + 1)
+                self.addQuad(ni * 2 + 1, ni * 2, i * 2, i * 2 + 1)
             
         for i in range(2, nn-3, 2):
             if top:
-                addTri(bui, 0, i, i+2)
+                self.addTri(0, i, i+2)
             if bottom:
-                addTri(bui, 1, i+1, i+3)
+                self.addTri(1, i+1, i+3)
     
-# Semi-primitives
+    # Semi-primitives
 
-    def geomElevationGrid(self, node, bui):
+    def geomElevationGrid(self, node):
         dx = readFloat(node, "xSpacing", 1)
         dz = readFloat(node, "zSpacing", 1)
         nx = readInt(node, "xDimension", 0)
@@ -325,18 +353,18 @@ class X3DReader(MeshReader):
         if nx <= 0 or nz <= 0 or len(height) < nx*nz:
             return # That's weird, the wording of the standard suggests grids with zero quads are somehow valid
         
-        bui.reserveFaceAndVertexCount(2*(nx-1)*(nz-1), nx*nz)
+        self.reserveFaceAndVertexCount(2*(nx-1)*(nz-1), nx*nz)
         
         for z in range(nz):
             for x in range(nx):
-                bui.addVertex(x * dx, height[z*nx + x], z * dz)
+                self.addVertex(x * dx, height[z*nx + x], z * dz)
                 
         for z in range(1, nz):
             for x in range(1, nx):
-                addTriFlip(bui, (z - 1)*nx + x - 1, z*nx + x, (z - 1)*nx + x, ccw)
-                addTriFlip(bui, (z - 1)*nx + x - 1, z*nx + x - 1, z*nx + x, ccw)
+                self.addTriFlip((z - 1)*nx + x - 1, z*nx + x, (z - 1)*nx + x, ccw)
+                self.addTriFlip((z - 1)*nx + x - 1, z*nx + x - 1, z*nx + x, ccw)
     
-    def geomExtrusion(self, node, bui):
+    def geomExtrusion(self, node):
         ccw = readBoolean(node, "ccw", True)
         beginCap = readBoolean(node, "beginCap", True)
         endCap = readBoolean(node, "endCap", True)
@@ -403,7 +431,7 @@ class X3DReader(MeshReader):
                 orig_z = Vector(*m.dot(orig_z.getData()))
             return orig_z
     
-        bui.reserveFaceAndVertexCount(2*nsf*ncf + (nc - 2 if beginCap else 0) + (nc - 2 if endCap else 0), ns*nc)
+        self.reserveFaceAndVertexCount(2*nsf*ncf + (nc - 2 if beginCap else 0) + (nc - 2 if endCap else 0), ns*nc)
 
         z = None
         for i, spt in enumerate(spine):
@@ -456,10 +484,10 @@ class X3DReader(MeshReader):
             sptv3 = numpy.array(spt.getData()[:3])
             for cpt in cross:
                 v = sptv3 + m.dot(cpt)
-                bui.addVertex(*v)
+                self.addVertex(*v)
     
         if beginCap:
-            addFace(bui, [x for x in range(nc - 1, -1, -1)], ccw)
+            self.addFace([x for x in range(nc - 1, -1, -1)], ccw)
     
         # Order of edges in the face: forward along cross, forward along spine,
         # backward along cross, backward along spine, flipped if now ccw.
@@ -468,117 +496,167 @@ class X3DReader(MeshReader):
     
         for s in range(ns - 1):
             for c in range(ncf):
-                addQuadFlip(bui, s * nc + c, s * nc + (c + 1) % nc,
+                self.addQuadFlip(s * nc + c, s * nc + (c + 1) % nc,
                     (s + 1) * nc + (c + 1) % nc, (s + 1) * nc + c, ccw)
     
         if spineClosed:
             # The faces between the last and the first spine points
             b = (ns - 1) * nc
             for c in range(ncf):
-                addQuadFlip(bui, b + c, b + (c + 1) % nc,
+                self.addQuadFlip(b + c, b + (c + 1) % nc,
                     (c + 1) % nc, c, ccw)
                         
         if endCap:
-            addFace(bui, [(ns - 1) * nc + x for x in range(0, nc)], ccw)
+            self.addFace([(ns - 1) * nc + x for x in range(0, nc)], ccw)
     
 # Triangle meshes
 
     # Helper for numerous nodes with a Coordinate subnode holding vertices
     # That all triangle meshes and IndexedFaceSet
-    # nFaces can be a function, in case the face count is a function of coord 
-    def startCoordMesh(self, node, bui, nFaces):
+    # num_faces can be a function, in case the face count is a function of coord 
+    def startCoordMesh(self, node, num_faces):
         ccw = readBoolean(node, "ccw", True)
         coord = self.readVertices(node)
-        if hasattr(nFaces, '__call__'):
-            nFaces = nFaces(coord)
-        bui.reserveFaceAndVertexCount(nFaces, len(coord))
+        if hasattr(num_faces, '__call__'):
+            num_faces = num_faces(coord)
+        self.reserveFaceAndVertexCount(num_faces, len(coord))
         for pt in coord:
-            bui.addVertex(*pt)
+            self.addVertex(*pt)
             
         return ccw
         
 
-    def geomIndexedTriangleSet(self, node, bui):
+    def geomIndexedTriangleSet(self, node):
         index = readIntArray(node, "index", [])
-        nFaces = len(index) // 3
-        ccw = self.startCoordMesh(node, bui, nFaces)
+        num_faces = len(index) // 3
+        ccw = self.startCoordMesh(node, num_faces)
         
-        for i in range(0, nFaces*3, 3):
-            addTriFlip(bui, index[i], index[i+1], index[i+2], ccw)
+        for i in range(0, num_faces*3, 3):
+            self.addTriFlip(index[i], index[i+1], index[i+2], ccw)
     
-    def geomIndexedTriangleStripSet(self, node, bui):
+    def geomIndexedTriangleStripSet(self, node):
         strips = readIndex(node, "index")
-        ccw = self.startCoordMesh(node, bui, sum([len(strip) - 2 for strip in strips]))
+        ccw = self.startCoordMesh(node, sum([len(strip) - 2 for strip in strips]))
             
         for strip in strips:
             sccw = ccw # Running CCW value, reset for each strip
             for i in range(len(strip) - 2):
-                addTriFlip(bui, strip[i], strip[i+1], strip[i+2], sccw)
+                self.addTriFlip(strip[i], strip[i+1], strip[i+2], sccw)
                 sccw = not sccw
     
-    def geomIndexedTriangleFanSet(self, node, bui):
+    def geomIndexedTriangleFanSet(self, node):
         fans = readIndex(node, "index")
-        ccw = self.startCoordMesh(node, bui, sum([len(fan) - 2 for fan in fans]))
+        ccw = self.startCoordMesh(node, sum([len(fan) - 2 for fan in fans]))
         
         for fan in fans:
             for i in range(1, len(fan) - 1):
-                addTriFlip(bui, fan[0], fan[i], fan[i+1], ccw)
+                self.addTriFlip(fan[0], fan[i], fan[i+1], ccw)
    
-    def geomTriangleSet(self, node, bui):
-        ccw = self.startCoordMesh(node, bui, lambda coord: len(coord) // 3)
-        for i in range(0, len(bui.getVertices()), 3):
-            addTriFlip(bui, i, i+1, i+2, ccw)
+    def geomTriangleSet(self, node):
+        ccw = self.startCoordMesh(node, lambda coord: len(coord) // 3)
+        for i in range(0, len(self.verts), 3):
+            self.addTriFlip(i, i+1, i+2, ccw)
     
-    def geomTriangleStripSet(self, node, bui):
+    def geomTriangleStripSet(self, node):
         strips = readIntArray(node, "stripCount", [])
-        ccw = self.startCoordMesh(node, bui, sum([n-2 for n in strips]))
+        ccw = self.startCoordMesh(node, sum([n-2 for n in strips]))
             
         vb = 0
         for n in strips:
             sccw = ccw
             for i in range(n-2): 
-                addTriFlip(bui, vb+i, vb+i+1, vb+i+2, sccw)
+                self.addTriFlip(vb+i, vb+i+1, vb+i+2, sccw)
                 sccw = not sccw
             vb += n
     
-    def geomTriangleFanSet(self, node, bui):
+    def geomTriangleFanSet(self, node):
         fans = readIntArray(node, "fanCount", [])
-        ccw = self.startCoordMesh(node, bui, sum([n-2 for n in fans]))
+        ccw = self.startCoordMesh(node, sum([n-2 for n in fans]))
         
         vb = 0
         for n in fans:
             for i in range(1, n-1): 
-                addTriFlip(bui, vb, vb+i, vb+i+1, ccw)
+                self.addTriFlip(vb, vb+i, vb+i+1, ccw)
             vb += n
             
     # Quad geometries from the CAD module, might be relevant for printing
     
-    def geomQuadSet(self, node, bui):
-        ccw = self.startCoordMesh(node, bui, lambda coord: 2*(len(coord) // 4))
-        for i in range(0, len(bui.getVertices()), 4):
-            addQuadFlip(bui, i, i+1, i+2, i+3, ccw)
+    def geomQuadSet(self, node):
+        ccw = self.startCoordMesh(node, lambda coord: 2*(len(coord) // 4))
+        for i in range(0, len(self.verts), 4):
+            self.addQuadFlip(i, i+1, i+2, i+3, ccw)
             
-    def geomIndexedQuadSet(self, node, bui):
+    def geomIndexedQuadSet(self, node):
         index = readIntArray(node, "index", [])
         nQuads = len(index) // 4
-        ccw = self.startCoordMesh(node, bui, nQuads*2)
+        ccw = self.startCoordMesh(node, nQuads*2)
         
         for i in range(0, nQuads*4, 4):
-            addQuadFlip(bui, index[i], index[i+1], index[i+2], index[i+3], ccw)
+            self.addQuadFlip(index[i], index[i+1], index[i+2], index[i+3], ccw)
+            
+    # 2D polygon geometries
+    # Won't work for now, since Cura expects every mesh to have a nontrivial convex hull
+    # The only way around that is merging meshes.
+    
+    def geomDisk2D(self, node):
+        innerRadius = readFloat(node, "innerRadius", 0)
+        outerRadius = readFloat(node, "outerRadius", 1)
+        n = readInt(node, "subdivision", DEFAULT_SUBDIV)
+        
+        angle = 2 * pi / n
+        
+        if innerRadius:
+            self.reserveFaceAndVertexCount(n*4 if innerRadius else n-2, n*2 if innerRadius else n)
+            
+        for i in range(n):
+            s = sin(angle * i)
+            c = cos(angle * i)
+            self.addVertex(outerRadius*c, outerRadius*s, 0)
+            if innerRadius:
+                self.addVertex(innerRadius*c, innerRadius*s, 0)
+                ni = (i+1) % n
+                self.addQuad(2*i, 2*ni, 2*ni+1, 2*i+1)
+                
+        if not innerRadius:
+            for i in range(2, n):
+                self.addTri(0, i-1, i)
+                
+    def geomRectangle2D(self, node):
+        (x, y) = readFloatArray(node, "size", (2, 2))
+        self.reserveFaceAndVertexCount(2, 4)
+        self.addVertex(-x/2, -y/2, 0)
+        self.addVertex(x/2, -y/2, 0)
+        self.addVertex(x/2, y/2, 0)
+        self.addVertex(-x/2, y/2, 0)
+        self.addQuad(0, 1, 2, 3)
+        
+    def geomTriangleSet2D(self, node):
+        verts = readFloatArray(node, "vertices", ())
+        num_faces = len(verts) // 6;
+        verts = [(verts[i], verts[i+1], 0) for i in range(0, 6 * num_faces, 2)]
+        self.reserveFaceAndVertexCount(num_faces, num_faces * 3)
+        for vert in verts:
+            self.addVertex(*vert)
+        
+        # The front face is on the +Z side, so CCW is a variable
+        for i in range(0, num_faces*3, 3):
+            a = Vector(*verts[i+2]) - Vector(*verts[i])
+            b = Vector(*verts[i+1]) - Vector(*verts[i])
+            self.addTriFlip(i, i+1, i+2, a.x*b.y > a.y*b.x)
     
     # General purpose polygon mesh
 
-    def geomIndexedFaceSet(self, node, bui):
+    def geomIndexedFaceSet(self, node):
         faces = readIndex(node, "coordIndex")
-        ccw = self.startCoordMesh(node, bui, sum([len(face) - 2 for face in faces]))
+        ccw = self.startCoordMesh(node, sum([len(face) - 2 for face in faces]))
             
         for face in faces:
             if len(face) == 3:
-                addTriFlip(bui, face[0], face[1], face[2], ccw)
+                self.addTriFlip(face[0], face[1], face[2], ccw)
             elif len(face) > 3:
-                addFace(bui, face, ccw)
+                self.addFace(face, ccw)
                 
-    geometryImporters = {
+    geometry_importers = {
         'IndexedFaceSet': geomIndexedFaceSet,
         'IndexedTriangleSet': geomIndexedTriangleSet,
         'IndexedTriangleStripSet': geomIndexedTriangleStripSet,
@@ -588,6 +666,9 @@ class X3DReader(MeshReader):
         'TriangleFanSet': geomTriangleFanSet,
         'QuadSet': geomQuadSet,
         'IndexedQuadSet': geomIndexedQuadSet,
+        'TriangleSet2D': geomTriangleSet2D,
+        'Rectangle2D': geomRectangle2D,
+        'Disk2D': geomDisk2D,
         'ElevationGrid': geomElevationGrid,
         'Extrusion': geomExtrusion,
         'Sphere': geomSphere,
@@ -609,6 +690,103 @@ class X3DReader(MeshReader):
                         return [(co[i], co[i+1], co[i+2]) for i in range(0, (len(co) // 3)*3, 3)]
         return []
     
+    # Mesh builder helpers
+    
+    def reserveFaceAndVertexCount(self, num_faces, num_verts):
+        # Unlike the Cura MeshBuilder, we use 4-vectors here for easier transform
+        self.verts = numpy.array([(0,0,0,1) for i in range(num_verts)], dtype=numpy.float32)
+        self.faces = numpy.zeros((num_faces, 3), dtype=numpy.int32)
+        self.num_faces = 0
+        self.num_verts = 0
+        
+    def addVertex(self, x, y, z):
+        self.verts[self.num_verts, 0] = x
+        self.verts[self.num_verts, 1] = y
+        self.verts[self.num_verts, 2] = z
+        self.num_verts += 1
+    
+    # Indices are 0-based for this shape, but they won't be zero-based in the merged mesh
+    def addTri(self, a, b, c):
+        self.faces[self.num_faces, 0] = self.index_base + a
+        self.faces[self.num_faces, 1] = self.index_base + b
+        self.faces[self.num_faces, 2] = self.index_base + c
+        self.num_faces += 1
+        
+    def addTriFlip(self, a, b, c, ccw):
+        if ccw:
+            self.addTri(a, b, c)
+        else:
+            self.addTri(b, a, c)
+        
+    # Needs to be convex, but not necessaily planar
+    # Assumed ccw, cut along the ac diagonal
+    def addQuad(self, a, b, c, d):
+        self.addTri(a, b, c)
+        self.addTri(c, d, a)
+        
+    def addQuadFlip(self, a, b, c, d, ccw):
+        if ccw:
+            self.addTri(a, b, c)
+            self.addTri(c, d, a)
+        else:
+            self.addTri(a, c, b)
+            self.addTri(c, a, d)    
+    
+    
+    # Arbitrary polygon triangulation.
+    # Doesn't assume convexity and doesn't check the "convex" flag in the file.
+    # Works by the "cutting of ears" algorithm:
+    # - Find an outer vertex with the smallest angle and no vertices inside its adjacent triangle
+    # - Remove the triangle at that vertex
+    # - Repeat until done
+    # Vertex coordinates are supposed to be already set
+    def addFace(self, indices, ccw):
+        # Resolve indices to coordinates for faster math
+        n = len(indices)
+        verts = self.verts
+        face = [Vector(verts[i, 0], verts[i, 1], verts[i, 2]) for i in indices]
+        
+        # Need a normal to the plane so that we can know which vertices form inner angles
+        normal = findOuterNormal(face)
+            
+        if not normal: # Couldn't find an outer edge, non-planar polygon maybe?
+            return
+        
+        # Find the vertex with the smallest inner angle and no points inside, cut off. Repeat until done
+        m = len(face)
+        vi = [i for i in range(m)] # We'll be using this to kick vertices from the face
+        while m > 3:
+            max_cos = EPSILON # We don't want to check anything on Pi angles
+            i_min = 0 # max cos corresponds to min angle
+            for i in range(m):
+                inext = (i + 1) % m
+                iprev = (i + m - 1) % m
+                v = face[vi[i]]
+                next = face[vi[inext]] - v
+                prev = face[vi[iprev]] - v
+                nextXprev = next.cross(prev)
+                if nextXprev.dot(normal) > EPSILON: # If it's an inner angle
+                    cos = next.dot(prev) / (next.length() * prev.length())
+                    if cos > max_cos:
+                        # Check if there are vertices inside the triangle
+                        no_points_inside = True
+                        for j in range(m):
+                            if j != i and j != iprev and j != inext:
+                                vx = face[vi[j]] - v
+                                if pointInsideTriangle(vx, next, prev, nextXprev):
+                                    no_points_inside = False
+                                    break
+                                
+                        if no_points_inside:
+                            max_cos = cos
+                            i_min = i
+                            
+            self.addTriFlip(indices[vi[(i_min + m - 1) % m]], indices[vi[i_min]], indices[vi[(i_min + 1) % m]], ccw)
+            vi.pop(i_min)
+            m -= 1
+        self.addTriFlip(indices[vi[0]], indices[vi[1]], indices[vi[2]], ccw)
+
+    
 # ------------------------------------------------------------
 # X3D field parsers
 # ------------------------------------------------------------
@@ -665,89 +843,6 @@ def readIndex(node, attr):
     if chunk:
         chunks.append(chunk)
     return chunks  
-    
-# Mesh builder helpers
-
-def addTri(bui, a, b, c):
-    bui._indices[bui._face_count, 0] = a
-    bui._indices[bui._face_count, 1] = b
-    bui._indices[bui._face_count, 2] = c
-    bui._face_count += 1
-    
-def addTriFlip(bui, a, b, c, ccw):
-    if ccw:
-        addTri(bui, a, b, c)
-    else:
-        addTri(bui, b, a, c)
-    
-# Needs to be convex, but not necessaily planar
-# Assumed ccw, cut along the ac diagonal
-def addQuad(bui, a, b, c, d):
-    addTri(bui, a, b, c)
-    addTri(bui, c, d, a)
-    
-def addQuadFlip(bui, a, b, c, d, ccw):
-    if ccw:
-        addTri(bui, a, b, c)
-        addTri(bui, c, d, a)
-    else:
-        addTri(bui, a, c, b)
-        addTri(bui, c, a, d)
-    
-    
-# Arbitrary polygon triangulation.
-# Doesn't assume convexity and doesn't check the "convex" flag in the file.
-# Works by the "cutting of ears" algorithm:
-# - Find an outer vertex with the smallest angle and no vertices inside its adjacent triangle
-# - Remove the triangle at that vertex
-# - Repeat until done
-# Vertex coordinates are supposed to be already in the mesh builder object
-def addFace(bui, indices, ccw):
-    # Resolve indices to coordinates for faster math
-    n = len(indices)
-    verts = bui.getVertices()
-    face = [Vector(verts[i, 0], verts[i, 1], verts[i, 2]) for i in indices]
-    
-    # Need a normal to the plane so that we can know which vertices form inner angles
-    normal = findOuterNormal(face)
-        
-    if not normal: # Couldn't find an outer edge, non-planar polygon maybe?
-        return
-    
-    # Find the vertex with the smallest inner angle and no points inside, cut off. Repeat until done
-    m = len(face)
-    vi = [i for i in range(m)] # We'll be using this to kick vertices from the face
-    while m > 3:
-        maxCos = EPSILON # We don't want to check anything on Pi angles
-        iMin = 0 # max cos corresponds to min angle
-        for i in range(m):
-            inext = (i + 1) % m
-            iprev = (i + m - 1) % m
-            v = face[vi[i]]
-            next = face[vi[inext]] - v
-            prev = face[vi[iprev]] - v
-            nextXprev = next.cross(prev)
-            if nextXprev.dot(normal) > EPSILON: # If it's an inner angle
-                cos = next.dot(prev) / (next.length() * prev.length())
-                if cos > maxCos:
-                    # Check if there are vertices inside the triangle
-                    noPointsInside = True
-                    for j in range(m):
-                        if j != i and j != iprev and j != inext:
-                            vx = face[vi[j]] - v
-                            if pointInsideTriangle(vx, next, prev, nextXprev):
-                                noPointsInside = False
-                                break
-                            
-                    if noPointsInside:
-                        maxCos = cos
-                        iMin = i
-                        
-        addTriFlip(bui, indices[vi[(iMin + m - 1) % m]], indices[vi[iMin]], indices[vi[(iMin + 1) % m]], ccw)
-        vi.pop(iMin)
-        m -= 1
-    addTriFlip(bui, indices[vi[0]], indices[vi[1]], indices[vi[2]], ccw)
-  
   
 # Given a face as a sequence of vectors, returns a normal to the polygon place that forms a right triple
 # with a vector along the polygon sequence and a vector backwards
@@ -758,21 +853,21 @@ def findOuterNormal(face):
             edge = face[j] - face[i]
             if edge.length() > EPSILON:
                 edge = edge.normalized()
-                prevRejection = Vector()
-                isOuter = True
+                prev_rejection = Vector()
+                is_outer = True
                 for k in range(n):
                     if k != i and k != j:
                         pt = face[k] - face[i]
                         pte = pt.dot(edge)
                         rejection = pt - edge*pte
-                        if rejection.dot(prevRejection) < -EPSILON: # points on both sides of the edge - not an outer one
-                            isOuter = False
+                        if rejection.dot(prev_rejection) < -EPSILON: # points on both sides of the edge - not an outer one
+                            is_outer = False
                             break
-                        elif rejection.length() > prevRejection.length(): # Pick a greater rejection for numeric stability 
-                            prevRejection = rejection
+                        elif rejection.length() > prev_rejection.length(): # Pick a greater rejection for numeric stability 
+                            prev_rejection = rejection
                         
-                if isOuter: # Found an outer edge, prevRejection is the rejection inside the face. Generate a normal.
-                    return edge.cross(prevRejection)
+                if is_outer: # Found an outer edge, prev_rejection is the rejection inside the face. Generate a normal.
+                    return edge.cross(prev_rejection)
 
     return False
     
@@ -780,9 +875,9 @@ def findOuterNormal(face):
 # No error handling.
 # For stability, taking the ration between the biggest coordinates would be better; none of that, either.   
 def ratio(a, b):
-    if b.x > EPSILON:
+    if b.x > EPSILON or b.x < -EPSILON:
         return a.x / b.x
-    elif b.y > EPSILON:
+    elif b.y > EPSILON or b.y < -EPSILON:
         return a.y / b.y
     else:
         return a.z / b.z    
@@ -806,6 +901,3 @@ def toNumpyRotation(rot):
         (x * x * t + c,  x * y * t - z*s, x * z * t + y * s),
         (x * y * t + z*s, y * y * t + c, y * z * t - x * s),
         (x * z * t - y * s, y * z * t + x * s, z * z * t + c)))    
-
-    
-