diff --git a/plugins/X3DReader/X3DReader.py b/plugins/X3DReader/X3DReader.py index 72fba5d1fe..8a5822d9ed 100644 --- a/plugins/X3DReader/X3DReader.py +++ b/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))) - - - \ No newline at end of file