From ab60d2baf612c945cacb6e724223f79356d3ccd2 Mon Sep 17 00:00:00 2001 From: Julian Kunze <41923255+JulianKu@users.noreply.github.com> Date: Wed, 29 Jan 2020 14:19:26 +0100 Subject: [PATCH] fix compability issues with Python3 --- ObjReader.py | 763 ++++++++++++++++++++++++------------------------ navMesh.py | 349 ++++++++++++---------- objToNavMesh.py | 427 +++++++++++++++------------ primitives.py | 478 +++++++++++++++--------------- 4 files changed, 1073 insertions(+), 944 deletions(-) diff --git a/ObjReader.py b/ObjReader.py index 3902827..79a06b1 100644 --- a/ObjReader.py +++ b/ObjReader.py @@ -1,3 +1,5 @@ +#! /usr/bin/env python3 + # aspects of file format not supported # doesn't read material libraries # doesn't do smoothing groups (s) @@ -12,124 +14,133 @@ import re -from primitives import * from datetime import datetime -from copy import deepcopy -from struct import pack -#from Horde3D import Horde3DMesh -VERSION = '0.9' +from .primitives import * + +# from copy import deepcopy + +# from Horde3D import Horde3DMesh + +VERSION = "0.9" -FLOAT_EXP = '[-\+]?[0-9]+\.?[0-9]*e[-\+]?[0-9]+|[-\+]?[0-9]+\.?[0-9]*' -UINT_EXP = '[0-9]+' +FLOAT_EXP = r"[-\+]?[0-9]+\.?[0-9]*e[-\+]?[0-9]+|[-\+]?[0-9]+\.?[0-9]*" +UINT_EXP = "[0-9]+" # (?:_){0,2} - match 0 to 2 instances of previous expression -VERT_PAT = re.compile('\s*v\s+(%s)\s+(%s)\s(%s)' % (FLOAT_EXP, FLOAT_EXP, FLOAT_EXP) ) -UV_PAT = re.compile('\s*vt\s+(%s)\s+(%s)' % (FLOAT_EXP, FLOAT_EXP) ) -NORM_PAT = re.compile('\s*vn\s+(%s)\s+(%s)\s(%s)' % (FLOAT_EXP, FLOAT_EXP, FLOAT_EXP) ) -FACE_PAT = re.compile('\s*f\s+(%s(?:\\%s){0,2})' % (UINT_EXP, UINT_EXP) ) -MTLLIB_PAT = re.compile('\s*mtllib\s+([a-zA-Z_0-9\./]+\.[a-zA-Z0-9]{3})') -GRP_PAT = re.compile('\s*g\s+([a-zA-Z][a-zA-Z_0-9]*)') -USEMTL_PAT = re.compile('\s*usemtl\s+([a-zA-Z][a-zA-Z_0-9\(\)]*)') - -##mtllib wooddoll.mtl -##g Figure_4 -##g Figure_4 -##usemtl skin +VERT_PAT = re.compile(r"\s*v\s+(%s)\s+(%s)\s(%s)" % (FLOAT_EXP, FLOAT_EXP, FLOAT_EXP)) +UV_PAT = re.compile(r"\s*vt\s+(%s)\s+(%s)" % (FLOAT_EXP, FLOAT_EXP)) +NORM_PAT = re.compile(r"\s*vn\s+(%s)\s+(%s)\s(%s)" % (FLOAT_EXP, FLOAT_EXP, FLOAT_EXP)) +FACE_PAT = re.compile(r"\s*f\s+(%s(?:\\%s){0,2})" % (UINT_EXP, UINT_EXP)) +MTLLIB_PAT = re.compile(r"\s*mtllib\s+([a-zA-Z_0-9./]+\.[a-zA-Z0-9]{3})") +GRP_PAT = re.compile(r"\s*g\s+([a-zA-Z][a-zA-Z_0-9]*)") +USEMTL_PAT = re.compile(r"\s*usemtl\s+([a-zA-Z][a-zA-Z_0-9()]*)") + + +# mtllib wooddoll.mtl +# g Figure_4 +# g Figure_4 +# usemtl skin # face format is f vert/uv/norm -def getFaceData( data ): +def getFaceData(data): """Extracts vertex, normal and uv indices for a face definition. data consists of strings: i, i/i, i//i, or i/i/i. All strings in the data should be of the same format. Format determines what indices are defined""" dataLists = [[], [], []] for s in data: - indices = s.split('/') + indices = s.split("/") for i in range(len(indices)): try: - index = int( indices[i] ) - if ( i == 0 and index in dataLists[i] ): + index = int(indices[i]) + if i == 0 and index in dataLists[i]: raise IOError - dataLists[i].append( int( indices[i] ) ) + dataLists[i].append(int(indices[i])) except ValueError: pass # validate the data -- i.e. lengths should equal all be equal, unless length = 0 - if ( ( len(dataLists[0]) != len(dataLists[1] ) and len(dataLists[1]) != 0 ) or - ( len(dataLists[0] ) != len( dataLists[2] ) and len(dataLists[2]) != 0 ) ): + if ((len(dataLists[0]) != len(dataLists[1]) and len(dataLists[1]) != 0) or + (len(dataLists[0]) != len(dataLists[2]) and len(dataLists[2]) != 0)): raise IOError return dataLists + class Material: """Material class""" - def __init__( self, name = 'default' ): + + def __init__(self, name="default"): self.isAssigned = False self.name = name - self.diffuse = [0, 60, 128] # ranges from 0-255 + self.diffuse = [0, 60, 128] # ranges from 0-255 - def setDiffuse( self, r, g, b ): - self.diffuse = [r, g, b] + def setDiffuse(self, r, g, b): + self.diffuse = [r, g, b] - def OBJFormat( self ): + def OBJFormat(self): """Writes material definition to obj text format""" - s = 'newmtl %s\n' % (self.name) + s = "newmtl %s\n" % self.name r, g, b = self.diffuse - s += 'Kd %d %d %d' % (r, g, b) + s += "Kd %d %d %d" % (r, g, b) return s - - def PLYBinaryFormat( self ): + + def PLYBinaryFormat(self): """Writes material definition in binary PLY format""" - # for now it simply outputs the diffuse r, g, b vlaues + # for now it simply outputs the diffuse r, g, b values r, g, b = self.diffuse - return pack('>uuu', r, g, b ) + return pack(">uuu", r, g, b) + + # ILLUM_RGB_PAT = re.compile("\s*[(?:K[asd])(?:Tf)]") + -#ILLUM_RGB_PAT = re.compile('\s*[(?:K[asd])(?:Tf)]') class MtlLib: """Class to handle material library""" - def __init__( self, filename = None ): - if ( filename != None ): - self.materials = {'default':Material()} - self.readFile( filename ) - def readFile( self, filename ): - file = open( filename, 'r' ) + def __init__(self, filename=None): + if filename is not None: + self.materials = {"default": Material()} + self.readFile(filename) + + def readFile(self, filename): + file = open(filename, "r") lineNum = 0 currMat = None - for line in file.xreadlines(): - lineNum +=1 + for line in file: + lineNum += 1 line = line.strip() - if ( not line ): + if not line: continue - elif ( line.startswith( '#' ) ): + elif line.startswith("#"): continue - elif ( line.startswith( 'newmtl' ) ): # new material + elif line.startswith("newmtl"): # new material try: name = line.split()[-1] - currMat = Material( name ) - if ( self.materials.has_key( name ) ): - raise IOError, "Error on newmtl definition - library already has material with that name. Line: %d: %s" % (lineNum, line) + currMat = Material(name) + if name in self.materials: + raise IOError("Error on newmtl definition - library already has material with that name. " + "Line: %d: %s" % (lineNum, line)) else: self.materials[name] = currMat except: - raise IOError, "Error on newmtl definition. Line: %d: %s" % (lineNum, line) - elif ( currMat == None ): - raise IOError, "Reading line without having an active material. Line: %d: %s" % ( lineNum, line ) + raise IOError("Error on newmtl definition. Line: %d: %s" % (lineNum, line)) + elif currMat is None: + raise IOError("Reading line without having an active material. Line: %d: %s" % (lineNum, line)) # following settings can use the format: # 1 Ka r g b # 2 Ka spectral name.rfl # 3 Ka xyz x y z} # Only #1 is supported - elif ( line.startswith( 'Ka' ) ): # ambient color - pass # only supports Ka rgb - elif ( line.startswith( 'Kd' ) ): # diffuse color + elif line.startswith("Ka"): # ambient color + pass # only supports Ka rgb + elif line.startswith("Kd"): # diffuse color try: r, g, b = line.split()[-3:] - currMat.setDiffuse( r, g, b ) + currMat.setDiffuse(r, g, b) except: - raise IOError, "Error on Kd definition. Line: %d: %s" % (lineNum, line) - elif ( line.startswith( 'Ks' ) ): # specular color + raise IOError("Error on Kd definition. Line: %d: %s" % (lineNum, line)) + elif line.startswith("Ks"): # specular color pass - elif ( line.startswith( 'Tf' ) ): # Transmission filter - determines which colors are permitted through + elif line.startswith("Tf"): # Transmission filter - determines which colors are permitted through pass # illumination model is illum # # 0 color on, ambient off @@ -143,15 +154,15 @@ def readFile( self, filename ): # 8 Reflection on and Ray trace off # 9 Transparency: Glass on Reflection: Ray trace off # 10 Casts shadows onto invisible surfaces - elif ( line.startswith( 'illum' ) ): # illumination model + elif line.startswith("illum"): # illumination model pass - elif ( line.startswith( 'd' ) ): # dissolve (aka opacity) - optional -halo argument + elif line.startswith("d"): # dissolve (aka opacity) - optional -halo argument pass - elif ( line.startswith( 'Ns' ) ): # Specular exponent + elif line.startswith("Ns"): # Specular exponent pass - elif ( line.startswith( 'sharpness' ) ): # sharpness of reflections + elif line.startswith("sharpness"): # sharpness of reflections pass - elif ( line.startswith( 'Ni' ) ): # index of refraction + elif line.startswith("Ni"): # index of refraction pass # map parameters have the format map_ -options args filename # possible options @@ -166,59 +177,59 @@ def readFile( self, filename ): # -s u v w scales image # -t u v w turbulence # -texres value resolution of texture created - elif ( line.startswith( 'map_Ka' ) ): # + elif line.startswith("map_Ka"): # pass - elif ( line.startswith( 'map_Kd' ) ): # ??? + elif line.startswith("map_Kd"): # ??? pass - elif ( line.startswith( 'map_Ks' ) ): # ??? + elif line.startswith("map_Ks"): # ??? pass - elif ( line.startswith( 'map_Ns' ) ): # ??? + elif line.startswith("map_Ns"): # ??? pass - elif ( line.startswith( 'map_d' ) ): # ??? + elif line.startswith("map_d"): # ??? pass - elif ( line.startswith( 'map_aat' ) ): # determiens if anti-aliasing is on for this one material + elif line.startswith("map_aat"): # determiens if anti-aliasing is on for this one material pass - elif ( line.startswith( 'disp' ) ): # ??? + elif line.startswith("disp"): # ??? pass - elif ( line.startswith( 'decal' ) ): # ??? + elif line.startswith("decal"): # ??? pass - elif ( line.startswith( 'bump' ) ): # ??? + elif line.startswith("bump"): # ??? pass # material reflection map -- (environment reflection map) # refl -type sphere -options -args filename # -type: sphere, cube_side, cube_top, cube_bottom, cube_front, cube_back, cube_left, cube_right - elif ( line.startswith( 'refl' ) ): # ??? + elif line.startswith("refl"): # ??? pass - elif ( line.startswith( 'map_Bump' ) ): # funny U of U version of bump map???? + elif line.startswith("map_Bump"): # funny U of U version of bump map???? pass - def OBJFormat( self ): + def OBJFormat(self): """Writes material library to obj text format""" keys = self.materials.keys() - keys.sort() - s = '' - for key in keys: - mat = self.materials[ key ] - if ( mat.isAssigned ): + s = "" + for key in sorted(keys): + mat = self.materials[key] + if mat.isAssigned: s += mat.OBJFormat() return s - - + + class Group: """Group of faces in obj file""" + def __init__(self, name): self.name = name -## self.currMatName = matName + # self.currMatName = matName # simply a list of faces -## self.currMatGrp = [] + # self.currMatGrp = [] # a mapping from material name to list of faces - self.materials = {}#{self.currMatName:[]} + self.materials = {} # {self.currMatName:[]} - def __repr__( self ): - return 'Group( %s ) with %d faces and %d materials' % ( self.name, len( self ), len( self.materials ) ) + def __repr__(self): + return "Group( %s ) with %d faces and %d materials" % (self.name, len(self), len(self.materials)) - def __str__( self ): - return 'Group( %s ) with %d faces' % ( self.name, len( self ) ) + def __str__(self): + return "Group( %s ) with %d faces" % (self.name, len(self)) def __len__(self): count = 0 @@ -226,380 +237,384 @@ def __len__(self): count += len(matGrp) return count -## def addMaterial( self, mat ): -## self.currMatName = mat -## if ( self.materials.has_key( mat ) ): -## self.currMatGrp = self.materials[mat] -## else: -## self.currMatGrp = [] -## self.materials[ mat ] = self.currMatGrp - - def addFace( self, f, matName ): - if ( self.materials.has_key( matName ) ): - self.materials[ matName ].append( f ) + # def addMaterial( self, mat ): + # self.currMatName = mat + # if ( self.materials.has_key( mat ) ): + # self.currMatGrp = self.materials[mat] + # else: + # self.currMatGrp = [] + # self.materials[ mat ] = self.currMatGrp + + def addFace(self, f, matName): + if matName in self.materials: + self.materials[matName].append(f) else: - self.materials[ matName ] = [ f ] -## self.currMatGrp.append( f ) + self.materials[matName] = [f] - def OBJFormat( self ): + # self.currMatGrp.append( f ) + + def OBJFormat(self): """Outputs the group information into obj specific format""" - #s = 'g %s' % self.name - s = '' + # s = "g %s" % self.name + s = "" for mat, matGrp in self.materials.items(): - if ( matGrp ): - s += '\nusemtl %s' % mat + if matGrp: + s += "\nusemtl %s" % mat for f in matGrp: - s += '\n%s' % f.OBJFormat() - if ( s ): - return 'g %s' % self.name + s + s += "\n%s" % f.OBJFormat() + if s: + return "g %s" % self.name + s else: - return '' + return "" - def PLYAsciiFormat( self, useNorms, useUvs ): + def PLYAsciiFormat(self, useNorms, useUvs): """Outputs the group information into ply ascii specific format""" - #s = 'g %s' % self.name - s = '' + # s = "g %s" % self.name + s = "" for mat, matGrp in self.materials.items(): - if ( matGrp ): + if matGrp: for f in matGrp: - s += '\n%s' % f.PLYAsciiFormat( useNorms, useUvs ) + s += "\n%s" % f.PLYAsciiFormat(useNorms, useUvs) return s - def PLYBinaryFormat( self, useNorms, useUvs ): + def PLYBinaryFormat(self, useNorms, useUvs): """Outputs the group information into ply ascii specific format""" - #s = 'g %s' % self.name - s = '' + # s = "g %s" % self.name + s = "" for mat, matGrp in self.materials.items(): - if ( matGrp ): + if matGrp: for f in matGrp: - s += '%s' % f.PLYBinaryFormat( useNorms, useUvs ) - return s + s += "%s" % f.PLYBinaryFormat(useNorms, useUvs) + return s - def triangulate( self ): + def triangulate(self): """Returns a triangulated version of the group""" hasFaces = False - newGrp = Group( self.name ) + newGrp = Group(self.name) for mat, matGrp in self.materials.items(): - if ( matGrp ): + if matGrp: hasFaces = True newMatGrp = [] - newGrp.materials[ mat ] = newMatGrp + newGrp.materials[mat] = newMatGrp for f in matGrp: newMatGrp += f.triangulate() - if (hasFaces): + if hasFaces: return newGrp else: return None - + + class ObjFile: class FaceIterator: - """An iterator through the objfile's faces + """An iterator through the objfile"s faces It will iterate across all faces, returning the face data *and* the group to which the face belongs. """ - def __init__( self, objfile ): + + def __init__(self, objfile): self.mesh = objfile - self.groups = objfile.groups.values() + self.groups = list(objfile.groups.values()) self.grpIndex = 0 - self.matGrps = self.groups[0].materials.values() + self.matGrps = list(self.groups[0].materials.values()) self.matGrpIndex = 0 self.faces = self.matGrps[0] self.faceIndex = -1 - def __iter__( self ): + def __iter__(self): return self - def next( self ): + def __next__(self): self.faceIndex += 1 - if (self.faceIndex >= len( self.faces ) ): + if self.faceIndex >= len(self.faces): self.faceIndex = 0 - # TODO: Replace this test with an object clean up - # that removes empty materials from a group - tryAgain = True # use this to skip empty groups - while ( tryAgain ): + # TODO: Replace this test with an object clean up + # that removes empty materials from a group + tryAgain = True # use this to skip empty groups + while tryAgain: self.matGrpIndex += 1 - if ( self.matGrpIndex >= len( self.matGrps ) ): + if self.matGrpIndex >= len(self.matGrps): self.matGrpIndex = 0 self.grpIndex += 1 - if ( self.grpIndex >= len( self.groups) ): - raise StopIteration, "End of faces" + if self.grpIndex >= len(self.groups): + raise StopIteration("End of faces") else: - self.matGrps = self.groups[ self.grpIndex ].materials.values() - if ( self.matGrps ): - self.faces = self.matGrps[ self.matGrpIndex ] - if ( self.faces ): + self.matGrps = list(self.groups[self.grpIndex].materials.values()) + if self.matGrps: + self.faces = self.matGrps[self.matGrpIndex] + if self.faces: tryAgain = False try: - return self.faces[ self.faceIndex ], self.groups[ self.grpIndex ].name + return self.faces[self.faceIndex], self.groups[self.grpIndex].name except IndexError: - print "Error getting face at index %d" % (self.faceIndex) + print("Error getting face at index %d" % self.faceIndex) raise StopIteration - - - def __init__( self, filename = None ): + + def __init__(self, filename=None): self.vertSet = [] self.normSet = [] self.uvSet = [] - self.groups = {}#{'default':Group('default')} - self.currGroup = None #self.groups['default'] + self.groups = {} # {"default":Group("default")} + self.currGroup = None # self.groups["default"] self.mtllib = None - self.currMatName = 'default' + self.currMatName = "default" # A map from each vertex, normal, uv and face read and which line it was found. self.object_line_numbers = {} - if ( filename != None ): - self.readFile( filename ) + if filename is not None: + self.readFile(filename) - def summary( self ): - '''Creates a summary string of the obj file''' + def summary(self): + """Creates a summary string of the obj file""" s = "OBJ file" - s += '\n\t%d vertices' % ( len ( self.vertSet ) ) - s += '\n\t%d normals' % ( len ( self.normSet ) ) - s += '\n\t%d uvs' % ( len ( self.uvSet ) ) + s += "\n\t%d vertices" % (len(self.vertSet)) + s += "\n\t%d normals" % (len(self.normSet)) + s += "\n\t%d uvs" % (len(self.uvSet)) grpCount, faceCount = self.faceStats() - s += '\n\t%d faces in %d groups' % ( faceCount, grpCount ) - s += '\n\t%d materials' % ( self.materialCount() ) + s += "\n\t%d faces in %d groups" % (faceCount, grpCount) + s += "\n\t%d materials" % (self.materialCount()) return s - - def readFile( self, filename ): - with open(filename, 'r') as file: + + def readFile(self, filename): + with open(filename, "r") as file: self.readFileLike(file) def readFileLike(self, file): - '''Reads the obj data from the given file-like object. Must have the xreadlines() - method''' + """Reads the obj data from the given file-like object. Must have the xreadlines() + method""" lineNum = 0 - for line in file.xreadlines(): + for line in file: lineNum += 1 line = line.strip() - if ( not line ): + if not line: continue - elif ( line.startswith('vn') ): # vertex normal + elif line.startswith("vn"): # vertex normal match = NORM_PAT.match(line) - if ( match ): + if match: try: - self.normSet.append(Vector3( match.group(1), match.group(2), match.group(3) ) ) - self.object_line_numbers[self.normSet[-1]] = lineNum + normal = Vector3(match.group(1), match.group(2), match.group(3)) + self.normSet.append(normal) + self.object_line_numbers[normal] = lineNum except: - raise IOError, "Expected vertex normal definition, line %d -- read: %s" % ( lineNum, line) + raise IOError("Expected vertex normal definition, line %d -- read: %s" % (lineNum, line)) else: - raise IOError, "Expected vertex normal definition, line %d -- read: %s" % ( lineNum, line) - elif ( line.startswith('vt') ): # texture vertex + raise IOError("Expected vertex normal definition, line %d -- read: %s" % (lineNum, line)) + elif line.startswith("vt"): # texture vertex match = UV_PAT.match(line) - if ( match ): + if match: try: - self.uvSet.append(Vector2( match.group(1), match.group(2) ) ) - self.object_line_numbers[self.uvSet[-1]] = lineNum + texture = Vector2(match.group(1), match.group(2)) + self.uvSet.append(texture) + self.object_line_numbers[texture] = lineNum except: - raise IOError, "Expected texture vertex definition, line %d -- read: %s" % ( lineNum, line) + raise IOError("Expected texture vertex definition, line %d -- read: %s" % (lineNum, line)) else: - raise IOError, "Expected texture vertex definition, line %d -- read: %s" % ( lineNum, line) - elif ( line.startswith('v') ): # vertex + raise IOError("Expected texture vertex definition, line %d -- read: %s" % (lineNum, line)) + elif line.startswith("v"): # vertex match = VERT_PAT.match(line) - if ( match ): + if match: try: - v = Vector3( match.group(1), match.group(2), match.group(3) ) - self.vertSet.append( v ) + v = Vector3(match.group(1), match.group(2), match.group(3)) + self.vertSet.append(v) self.object_line_numbers[v] = lineNum except: - raise IOError, "Expected vertex definition, line %d -- read: %s" % ( lineNum, line) + raise IOError("Expected vertex definition, line %d -- read: %s" % (lineNum, line)) else: - raise IOError, "Expected vertex definition, line %d -- read: %s" % ( lineNum, line) - elif ( line.startswith('#') ): # comment + raise IOError("Expected vertex definition, line %d -- read: %s" % (lineNum, line)) + elif line.startswith("#"): # comment continue - elif ( line.startswith('mtllib') ): # material library + elif line.startswith("mtllib"): # material library match = MTLLIB_PAT.match(line) - if ( match ): + if match: try: - if ( self.mtllib ): - raise IOError, "Already have mtllib defined for file, line %d -- read: %s" % ( lineNum, line) + if self.mtllib: + raise IOError("Already have mtllib defined for file, line %d -- read: %s" % (lineNum, line)) self.mtllib = match.group(1) except: - raise IOError, "Expected mtllib definition, line %d -- read: %s" % ( lineNum, line) + raise IOError("Expected mtllib definition, line %d -- read: %s" % (lineNum, line)) else: - raise IOError, "Expected mtllib definition, line %d -- read: %s" % ( lineNum, line) - elif ( line.startswith('g') ): # group definition + raise IOError("Expected mtllib definition, line %d -- read: %s" % (lineNum, line)) + elif line.startswith("g"): # group definition match = GRP_PAT.match(line) - if ( match ): + if match: try: groupName = match.group(1) except: - raise IOError, "Expected group definition, line %d -- read: %s" % ( lineNum, line) - if ( self.groups.has_key( groupName ) ): - self.currGroup = self.groups[ groupName ] + raise IOError("Expected group definition, line %d -- read: %s" % (lineNum, line)) + if groupName in self.groups: + self.currGroup = self.groups[groupName] else: - self.currGroup = Group( groupName ) - self.groups[ groupName ] = self.currGroup -## self.currGroup.addMaterial( self.currMatName ) + self.currGroup = Group(groupName) + self.groups[groupName] = self.currGroup + # self.currGroup.addMaterial( self.currMatName ) else: - pass # an empty group name will be skipped - #raise IOError, "Expected group definition, line %d -- read: %s" % ( lineNum, line ) - elif ( line.startswith('usemtl') ): # material application - match = USEMTL_PAT.match( line ) - if ( match ): + pass # an empty group name will be skipped + # raise IOError, "Expected group definition, line %d -- read: %s" % ( lineNum, line ) + elif line.startswith("usemtl"): # material application + match = USEMTL_PAT.match(line) + if match: try: -## self.currGroup.addMaterial( match.group(1) ) + # self.currGroup.addMaterial( match.group(1) ) self.currMatName = match.group(1) except: - raise IOError, "Expected usemtl definition, line %d -- read: %s" % ( lineNum, line ) + raise IOError("Expected usemtl definition, line %d -- read: %s" % (lineNum, line)) else: - raise IOError, "Expected usemtl definition, line %d -- read: %s" % ( lineNum, line ) - elif ( line.startswith('f') and not line.startswith('fo') ): # face definition - tokens = line.split()[1:] # extract f + raise IOError("Expected usemtl definition, line %d -- read: %s" % (lineNum, line)) + elif line.startswith("f") and not line.startswith("fo"): # face definition + tokens = line.split()[1:] # extract f try: - verts, uvs, norms = getFaceData( tokens ) + verts, uvs, norms = getFaceData(tokens) except IOError: continue - #raise IOError, "Poorly formatted face definition, line %d -- read: %s" % ( lineNum, line ) - f = Face( verts, norms, uvs ) - if ( self.currGroup == None ): - groupName = 'no_name' - self.currGroup = Group( groupName ) - self.groups[ groupName ] = self.currGroup - self.currGroup.addFace( f, self.currMatName ) + # raise IOError, "Poorly formatted face definition, line %d -- read: %s" % ( lineNum, line ) + f = Face(verts, norms, uvs) + if self.currGroup is None: + groupName = "no_name" + self.currGroup = Group(groupName) + self.groups[groupName] = self.currGroup + self.currGroup.addFace(f, self.currMatName) self.object_line_numbers[f] = lineNum - - def writeOBJ( self, outfile ): + def writeOBJ(self, outfile): """Writes obj to the file object""" - outfile.write("# OBJ written by python obj reader version %s -- %s\n" % (VERSION, datetime.today() ) ) - outfile.write("# %d vertices\n" % (len(self.vertSet ) ) ) - outfile.write("# %d texture vertices\n" % (len(self.uvSet) ) ) - outfile.write("# %d vertex normals\n" % (len(self.normSet) ) ) + outfile.write("# OBJ written by python obj reader version %s -- %s\n" % (VERSION, datetime.today())) + outfile.write("# %d vertices\n" % (len(self.vertSet))) + outfile.write("# %d texture vertices\n" % (len(self.uvSet))) + outfile.write("# %d vertex normals\n" % (len(self.normSet))) gCount, fCount = self.faceStats() - outfile.write("# %d groups\n" % (gCount)) - outfile.write("# %d faces\n" % (fCount)) + outfile.write("# %d groups\n" % gCount) + outfile.write("# %d faces\n" % fCount) outfile.write("\n") outfile.write("# vertices\n") for v in self.vertSet: - outfile.write("v %g %g %g\n" % (v.x, v.y, v.z ) ) - if ( self.normSet ): - outfile.write("\n# vertex normals\n" ) + outfile.write("v %g %g %g\n" % (v.x, v.y, v.z)) + if self.normSet: + outfile.write("\n# vertex normals\n") for v in self.normSet: - outfile.write("vn %g %g %g\n" % (v.x, v.y, v.z) ) - if ( self.uvSet ): - outfile.write("\n# texture vertices\n" ) + outfile.write("vn %g %g %g\n" % (v.x, v.y, v.z)) + if self.uvSet: + outfile.write("\n# texture vertices\n") for v in self.uvSet: - outfile.write("vt %g %g\n" % (v.x, v.y) ) + outfile.write("vt %g %g\n" % (v.x, v.y)) for gName, grp in self.groups.items(): - outfile.write( "%s\n" % grp.OBJFormat() ) + outfile.write("%s\n" % grp.OBJFormat()) - def writePLYAscii( self, outfile, useNorms = False, useUvs = False, useMat = False ): + def writePLYAscii(self, outfile, useNorms=False, useUvs=False, useMat=False): """Writes ascii ply to the file object""" useNorms = useNorms and self.normSet useUvs = useUvs and self.uvSet outfile.write("ply\n") outfile.write("format ascii 1.0\n") -## outfile.write("comment written by python objreader version %s\n" % VERSION ) -## outfile.write("comment date: %s\n" % datetime.today() ) - outfile.write("element vertex %d\n" % ( len(self.vertSet) ) ) + # outfile.write("comment written by python objreader version %s\n" % VERSION ) + # outfile.write("comment date: %s\n" % datetime.today() ) + outfile.write("element vertex %d\n" % (len(self.vertSet))) outfile.write("property float x\n") outfile.write("property float y\n") outfile.write("property float z\n") - if ( useMat ): + if useMat: outfile.write("property uchar red\n") outfile.write("property uchar green\n") - outfile.write('property uchar blue\n') - if ( useNorms ): - outfile.write("element normal %d\n" % ( len( self.normSet) ) ) - outfile.write('property float x\n') - outfile.write('property float y\n') - outfile.write('property float z\n') - if ( useUvs ): - outfile.write("element uv %d\n" % ( len( self.uvSet ) ) ) - outfile.write('property float u\n') - outfile.write('property float v\n') + outfile.write("property uchar blue\n") + if useNorms: + outfile.write("element normal %d\n" % (len(self.normSet))) + outfile.write("property float x\n") + outfile.write("property float y\n") + outfile.write("property float z\n") + if useUvs: + outfile.write("element uv %d\n" % (len(self.uvSet))) + outfile.write("property float u\n") + outfile.write("property float v\n") gCount, fCount = self.faceStats() - outfile.write("element face %d\n" % ( fCount ) ) + outfile.write("element face %d\n" % fCount) outfile.write("property list uchar int vertex_index\n") - if ( useNorms ): + if useNorms: outfile.write("property list uchar int norm_index\n") - if ( useUvs ): + if useUvs: outfile.write("property list uchar int uv_index\n") outfile.write("end_header") - + for v in self.vertSet: - outfile.write("\n%g %g %g" % (v.x, v.y, v.z ) ) - if ( useNorms ): + outfile.write("\n%g %g %g" % (v.x, v.y, v.z)) + if useNorms: for n in self.normSet: - outfile.write("\n%g %g %g" % (n.x, n.y, n.z ) ) - if ( useUvs ): + outfile.write("\n%g %g %g" % (n.x, n.y, n.z)) + if useUvs: for uv in self.uvSet: - outfile.write("\n%g %g" % (uv.x, uv.y) ) -## if ( self.normSet ): -## outfile.write("\n# vertex normals\n" ) -## for v in self.normSet: -## outfile.write("vn %g %g %g\n" % (v.x, v.y, v.z) ) -## if ( self.uvSet ): -## outfile.write("\n# texture vertices\n" ) -## for v in self.uvSet: -## outfile.write("vt %g %g\n" % (v.x, v.y) ) + outfile.write("\n%g %g" % (uv.x, uv.y)) + # if ( self.normSet ): + # outfile.write("\n# vertex normals\n" ) + # for v in self.normSet: + # outfile.write("vn %g %g %g\n" % (v.x, v.y, v.z) ) + # if ( self.uvSet ): + # outfile.write("\n# texture vertices\n" ) + # for v in self.uvSet: + # outfile.write("vt %g %g\n" % (v.x, v.y) ) for gName, grp in self.groups.items(): - outfile.write( "%s" % grp.PLYAsciiFormat( useNorms, useUvs ) ) - outfile.write('\n') + outfile.write("%s" % grp.PLYAsciiFormat(useNorms, useUvs)) + outfile.write("\n") - def writePLYBinary( self, outfile, useNorms = False, useUvs = False ): + def writePLYBinary(self, outfile, useNorms=False, useUvs=False): """Writes ascii ply to the file object""" outfile.write("ply\x0a") outfile.write("format binary_big_endian 1.0\x0a") -## outfile.write("comment written by python objreader version %s\n" % VERSION ) -## outfile.write("comment date: %s\n" % datetime.today() ) - outfile.write("element vertex %d\x0a" % ( len(self.vertSet) ) ) + # outfile.write("comment written by python objreader version %s\n" % VERSION ) + # outfile.write("comment date: %s\n" % datetime.today() ) + outfile.write("element vertex %d\x0a" % (len(self.vertSet))) outfile.write("property float x\x0a") outfile.write("property float y\x0a") outfile.write("property float z\x0a") - if ( useMat ): + if useMat: outfile.write("property uchar red\x0a") outfile.write("property uchar green\x0a") - outfile.write('property uchar blue\x0a') + outfile.write("property uchar blue\x0a") gCount, fCount = self.faceStats() - outfile.write("element face %d\x0a" % ( fCount ) ) + outfile.write("element face %d\x0a" % fCount) outfile.write("property list uchar int vertex_indices\x0a") outfile.write("end_header\x0a") - + for v in self.vertSet: - outfile.write(pack('>fff', v.x, v.y, v.z)) -## if ( self.normSet ): -## outfile.write("\n# vertex normals\n" ) -## for v in self.normSet: -## outfile.write("vn %g %g %g\n" % (v.x, v.y, v.z) ) -## if ( self.uvSet ): -## outfile.write("\n# texture vertices\n" ) -## for v in self.uvSet: -## outfile.write("vt %g %g\n" % (v.x, v.y) ) + outfile.write(pack(">fff", v.x, v.y, v.z)) + # if ( self.normSet ): + # outfile.write("\n# vertex normals\n" ) + # for v in self.normSet: + # outfile.write("vn %g %g %g\n" % (v.x, v.y, v.z) ) + # if ( self.uvSet ): + # outfile.write("\n# texture vertices\n" ) + # for v in self.uvSet: + # outfile.write("vt %g %g\n" % (v.x, v.y) ) for gName, grp in self.groups.items(): - outfile.write( "%s" % grp.PLYBinaryFormat(useNorms, useUvs) ) + outfile.write("%s" % grp.PLYBinaryFormat(useNorms, useUvs)) + + # the geo format has several requirements - # the geo format has several requirements # 1. There must be a one-to-one correspondence between vertices and normals - # i.e. I can't have four vertices all sharing the same normal, + # i.e. I can"t have four vertices all sharing the same normal, # I would need four instances of the same normal # 2. I have to generate a tangent and binormal vector for each vertex # 3. All binary values are LITTLE Endian -# def writeGEO( self, outfile ): -# """Writes two a binary, Horde3D geo file""" -# geoMesh = Horde3DMesh() -# geoMesh.makeFromObjFile( self ) -# geoMesh.writeToFile( outfile ) - - def faceStats( self ): + # def writeGEO( self, outfile ): + # """Writes two a binary, Horde3D geo file""" + # geoMesh = Horde3DMesh() + # geoMesh.makeFromObjFile( self ) + # geoMesh.writeToFile( outfile ) + + def faceStats(self): """Returns group and face count""" faceCount = 0 groupCount = 0 for grpName, grp in self.groups.items(): - if ( len(grp) ): + if len(grp): faceCount += len(grp) groupCount += 1 return groupCount, faceCount - def materialCount( self ): - '''Reports the number of materials found.''' + def materialCount(self): + """Reports the number of materials found.""" matCount = 0 for grpName, grp in self.groups.items(): - matCount += len( grp.materials ) + matCount += len(grp.materials) return matCount - def triangulate( self ): + def triangulate(self): """Returns a triangulated version of this obj""" newOBJ = ObjFile() newOBJ.vertSet = self.vertSet @@ -608,88 +623,84 @@ def triangulate( self ): newOBJ.mtllib = self.mtllib for grpName, grp in self.groups.items(): g = grp.triangulate() - if ( g ): - newOBJ.groups[ grpName ] = g - return newOBJ - def getFaceIterator( self ): - return ObjFile.FaceIterator( self ) + if g: + newOBJ.groups[grpName] = g + return newOBJ - def getFaceNormal( self, face ): + def getFaceIterator(self): + return ObjFile.FaceIterator(self) + + def getFaceNormal(self, face): """Returns normal of the provided face""" # argument is an actual face object, not an index into it # assumes triangles - v1 = self.vertSet[ face.verts[0] ] - v2 = self.vertSet[ face.verts[1] ] - v3 = self.vertSet[ face.verts[2] ] + v1 = self.vertSet[face.verts[0]] + v2 = self.vertSet[face.verts[1]] + v3 = self.vertSet[face.verts[2]] e1 = v2 - v1 e2 = v3 - v1 norm = e1.cross(e2) norm.normalize_ip() return norm -def usage(): - print "ObjReader -- reads wavefront .obj files and processes them" - print "" - print "Usage: python ObjReader -arg1 value1, -arg2 value2..." - print "Options:" - print " -in file.obj - the wavefront obj file to operate on" - print " - no input, no action" - print " -bp file.ply - file to write binary ply data" - print " - by default no binary ply file written" - print " -ap file.ply - file to write ascii ply data" - print " - by default no ascii ply file written" - print " -obj file.obj - file to write triangulated obj data" - print " - defaults to file_t.obj" - print " -geo file.geo - file to write binary geo data" - print " - by default, no geo file written" - print " -nt - don't triangulate" - print " - without this command line, the obj will be triangulated" - print " -mat - Use materials, if available" - print " -uv - output uvs" - print " -norm - output normals" - print "" - print " If any output files are specified, that is exactly what will be written." - print " If no output files are specified, an obj of the triangulated data will be" - print " written to the name file_t.obj" + if __name__ == "__main__": - import sys, os - from commandline import ParamManager - pMan = ParamManager(sys.argv[1:]) - inputName = pMan['in'] - if ( inputName == None ): - usage() - sys.exit(1) - - objTName = pMan['obj'] - asciiPlyName = pMan['ap'] - binaryPlyName = pMan['bp'] - geoName = pMan['geo'] - noTris = pMan['nt'] - useMat = pMan['mat'] - useUvs = pMan['uv'] - useNorms = pMan['norm'] - - if (objTName == None and asciiPlyName == None and binaryPlyName == None ): - tokens = os.path.splitext( inputName[0] ) - objTName = [tokens[0] + '_t.obj'] - - obj = ObjFile( inputName[0] ) - if ( not noTris ): + import argparse + import rospy as rp + import os + + parser = argparse.ArgumentParser("ObjReader -- reads wavefront .obj files and processes them") + parser.add_argument("in", dest="i", help="the wavefront obj file (.obj) to operate on\n" + "no input, no action") + parser.add_argument("bp", help="file (.ply) to write binary ply data\n" + "by default no ascii ply file written") + parser.add_argument("ap", help="file (.ply) to write ascii ply data\n" + "by default no binary ply file written") + parser.add_argument("obj", help="file (.obj) to write triangulated obj data\n" + "defaults to file_t.obj") + # parser.add_argument("geo", help="file (.geo) to write binary geo data\n" + # "by default, no geo file written") + parser.add_argument("nt", help="don\"t triangulate\n" + "without this command line, the obj will be triangulated", + action="store_true") + parser.add_argument("mat", help="Use materials, if available", action="store_true") + parser.add_argument("uv", help="output uvs", action="store_true") + parser.add_argument("norm", help="output normals", action="store_true") + parser.set_defaults(nt=False, uv=False, norm=False) + args = parser.parse_args(rp.myargv()[1:]) + + inputName = args.i + + objTName = args.obj + asciiPlyName = args.ap + binaryPlyName = args.bp + # geoName = args.geo + noTris = args.nt + useMat = args.mat + useUvs = args.uv + useNorms = args.norm + + if objTName is None and asciiPlyName is None and binaryPlyName is None: + tokens = os.path.splitext(inputName[0]) + objTName = [tokens[0] + "_t.obj"] + + obj = ObjFile(inputName[0]) + if not noTris: obj = obj.triangulate() - if ( objTName ): - outFile = open( objTName[0], 'w' ) - obj.writeOBJ( outFile ) - outFile.close() - if ( asciiPlyName ): - outFile = open( asciiPlyName[0], 'w' ) - obj.writePLYAscii( outFile, useNorms, useUvs ) + if objTName: + outFile = open(objTName[0], "w") + obj.writeOBJ(outFile) outFile.close() - if ( binaryPlyName ): - outFile = open( binaryPlyName[0], 'wb' ) - obj.writePLYBinary( outFile, useNorms, useUvs ) + if asciiPlyName: + outFile = open(asciiPlyName[0], "w") + obj.writePLYAscii(outFile, useNorms, useUvs) outFile.close() - if ( geoName ): - outFile = open( geoName[0], 'wb' ) - obj.writeGEO( outFile ) + if binaryPlyName: + outFile = open(binaryPlyName[0], "wb") + obj.writePLYBinary(outFile, useNorms, useUvs) outFile.close() + # if geoName: + # outFile = open(geoName[0], "wb") + # obj.writeGEO(outFile) + # outFile.close() diff --git a/navMesh.py b/navMesh.py index 17f8802..a156b0f 100644 --- a/navMesh.py +++ b/navMesh.py @@ -1,13 +1,18 @@ -# The definitino of a navigation mesh +#! /usr/bin/env python3 + +# The definition of a navigation mesh import struct -from primitives import Vector2 +from .primitives import Vector2 +from numpy import array, arctan2, pi + class Node: - '''The node of a navigation mesh''' - def __init__( self ): + """The node of a navigation mesh""" + + def __init__(self): # polygon - self.poly = None # the obj face for this polygon + self.poly = None # the obj face for this polygon # the explicit definition of the 3D plane for this polygon self.A = 0.0 self.B = 0.0 @@ -17,224 +22,258 @@ def __init__( self ): self.edges = [] self.obstacles = [] - def addEdge( self, edgeID ): - '''Given the index of an internal edge, adds the edge to the definition''' - self.edges.append( edgeID ) + def addEdge(self, edgeID): + """Given the index of an internal edge, adds the edge to the definition""" + self.edges.append(edgeID) - def addObstacle( self, obstID ): - '''Given the index of an obstacle edge, adds the obstacle to the definition''' - if ( obstID not in self.obstacles ): - self.obstacles.append( obstID ) + def addObstacle(self, obstID): + """Given the index of an obstacle edge, adds the obstacle to the definition""" + if obstID not in self.obstacles: + self.obstacles.append(obstID) - def toString( self, ascii=True ): - '''Output the node data to a string''' - if ( ascii ): + def toString(self, ascii=True): + """Output the node data to a string""" + if ascii: return self.asciiString() else: return self.binaryString() - def asciiString( self, indent='' ): - '''Output the node data to an ascii string''' - s = '%s%.5f %.5f' % ( indent, self.center.x, self.center.y ) - s += '\n%s%d' % ( indent, len( self.poly.verts ) ) + def asciiString(self, indent=''): + """Output the node data to an ascii string""" + s = '%s%.5f %.5f' % (indent, self.center.x, self.center.y) + s += '\n%s%d' % (indent, len(self.poly.verts)) for v in self.poly.verts: - s += ' %d' % ( v - 1 ) - s += '\n%s%.5f %.5f %.5f' % ( indent, self.A, self.B, self.C ) - s += '\n%s%d' % ( indent, len( self.edges ) ) + s += ' %d' % (v - 1) + s += '\n%s%.5f %.5f %.5f' % (indent, self.A, self.B, self.C) + s += '\n%s%d' % (indent, len(self.edges)) for edge in self.edges: - s += ' %d' % ( edge ) - s += '\n%s%d' % ( indent, len( self.obstacles ) ) + s += ' %d' % edge + s += '\n%s%d' % (indent, len(self.obstacles)) for obst in self.obstacles: - s += ' %d' % ( obst ) + s += ' %d' % obst return s - def binaryString( self ): - '''Output the node data to a binary string''' - s = struct.pack( 'i', len( self.poly.verts ) ) + def binaryString(self): + """Output the node data to a binary string""" + s = struct.pack('i', len(self.poly.verts)) for v in self.poly.verts: - s += struct.pack( 'i', v - 1 ) - s += struct.pack('fffff', self.A, self.B, self.C, self.center.x, self.center.y ) - s += struct.pack( 'i', len( self.edges ) ) + s += struct.pack('i', v - 1) + s += struct.pack('fffff', self.A, self.B, self.C, self.center.x, self.center.y) + s += struct.pack('i', len(self.edges)) for edge in self.edges: - s += struct.pack( 'i', edge ) + s += struct.pack('i', edge) return s + class Edge: - '''The edge of a navigation mesh - an edge that is shared by two polygons''' - def __init__( self ): - self.v0 = -1 # index of the first vertex - self.v1 = -1 # index of the second vertex - self.n0 = None # the first adjacent node - self.n1 = None # the second adjacent node + """The edge of a navigation mesh - an edge that is shared by two polygons""" + + def __init__(self): + self.v0 = -1 # index of the first vertex + self.v1 = -1 # index of the second vertex + self.n0 = None # the first adjacent node + self.n1 = None # the second adjacent node + + def asciiString(self, nodeMap): + """Writes out the edge as an ascii string. - def asciiString( self, nodeMap ): - '''Writes out the edge as an ascii string. + @param nodeMap A mapping from a node instance to its file index""" + return '%d %d %d %d' % (self.v0, self.v1, nodeMap[self.n0], nodeMap[self.n1]) - @param nodeMap A mapping from a node instance to its file index''' - return '%d %d %d %d' % ( self.v0, self.v1, nodeMap[ self.n0 ], nodeMap[ self.n1 ] ) + def binaryString(self, nodeMap): + """Writes out the edge as a binary string - def binaryString( self, nodeMap ): - '''Writes out the edge as a binary string + @param nodeMap A mapping from a node instance to its file index""" + # TODO: Enforce fixed endianness + return struct.pack('iiii', self.v0, self.v1, nodeMap[self.n0], nodeMap[self.n1]) - @param nodeMap A mapping from a node instance to its file index''' - #TODO: Enforce fixed endianness - return struct.pack( 'iiii', self.v0, self.v1, nodeMap[ self.n0 ], nodeMap[ self.n1 ] ) class Obstacle: - '''The obstacle of a navigation mesh -- otherwise known as an edge with only a single adjacent polygon''' - def __init__( self ): - self.v0 = -1 # index of the first vertex - self.v1 = -1 # index of the second vertex - self.n0 = None # The adjacent node - self.next = -1 # index of the next obstacle in sequence - - def asciiString( self, nodeMap ): - '''Writes out the edge as an ascii string - - @param nodeMap A mapping from a node instance to its file index''' - return '%d %d %d %d' % ( self.v0, self.v1, nodeMap[ self.n0 ], self.next ) - - def binaryString( self, nodeMap ): - '''Writes out the edge as a binary string - - @param nodeMap A mapping from a node instance to its file index''' - #TODO: Enforce fixed endianness - return struct.pack( 'iiii', self.v0, self.v1, nodeMap[ self.n0 ], self.next ) - + """The obstacle of a navigation mesh -- otherwise known as an edge with only a single adjacent polygon""" + + def __init__(self): + self.v0 = -1 # index of the first vertex + self.v1 = -1 # index of the second vertex + self.n0 = None # The adjacent node + self.next = -1 # index of the next obstacle in sequence + + def asciiString(self, nodeMap): + """Writes out the edge as an ascii string + + @param nodeMap A mapping from a node instance to its file index""" + return '%d %d %d %d' % (self.v0, self.v1, nodeMap[self.n0], self.next) + + def binaryString(self, nodeMap): + """Writes out the edge as a binary string + + @param nodeMap A mapping from a node instance to its file index""" + # TODO: Enforce fixed endianness + return struct.pack('iiii', self.v0, self.v1, nodeMap[self.n0], self.next) + + def get_angle(self, other_obstacle, vertices): + """ + compute angle between this and another adjacent obstacle (both obstacles sharing one vertex) + vertices maps the vertex index to the actual vertex + """ + # make sure both vectors point away from shared vertex between edges + if self.v1 == other_obstacle.v0: + o0v0 = vertices[self.v1] + o0v1 = vertices[self.v0] + o1v0 = vertices[other_obstacle.v0] + o1v1 = vertices[other_obstacle.v1] + elif self.v0 == other_obstacle.v1: + o0v0 = vertices[self.v0] + o0v1 = vertices[self.v1] + o1v0 = vertices[other_obstacle.v1] + o1v1 = vertices[other_obstacle.v0] + else: + raise ValueError("Obstacles need to be adjacent so that they share one vertex") + vect_o0 = array(o0v1) - array(o0v0) + vect_o1 = array(o1v1) - array(o1v0) + + angle = arctan2(vect_o1[1], vect_o1[0]) - arctan2(vect_o0[1], vect_o0[0]) + # normalize to range -pi, pi + if angle >= pi: + angle -= 2 * pi + elif angle <= - pi: + angle += 2 * pi + # return absolute angle + return abs(angle) + class NavMesh: - '''A simple navigation mesh''' + """A simple navigation mesh""" + class NodeIterator: - '''An iterator for iterating across the nodes of the navigation mesh for output - strings. It respects the node groups.''' - def __init__( self, navMesh ): - self.groupNames = navMesh.groups.keys() - assert( len( self.groupNames ) > 0 ) - self.groupNames.sort() - self.currGroupID = 0 # the current group to operate on - self.currGroup = navMesh.groups[ self.groupNames[ 0 ] ] - self.currNode = 0 # the next face in the group to return + """An iterator for iterating across the nodes of the navigation mesh for output + strings. It respects the node groups.""" + + def __init__(self, navMesh): + self.groupNames = sorted(navMesh.groups.keys()) + assert (len(self.groupNames) > 0) + self.currGroupID = 0 # the current group to operate on + self.currGroup = navMesh.groups[self.groupNames[0]] + self.currNode = 0 # the next face in the group to return self.groups = navMesh.groups - def __iter__( self ): + def __iter__(self): return self - - def next( self ): - '''Returns a group name and a node''' - if ( self.currNode >= len( self.currGroup ) ): + + def __next__(self): + """Returns a group name and a node""" + if self.currNode >= len(self.currGroup): self.currGroupID += 1 - if ( self.currGroupID >= len( self.groups ) ): + if self.currGroupID >= len(self.groups): raise StopIteration else: - self.currGroup = self.groups[ self.groupNames[ self.currGroupID ] ] + self.currGroup = self.groups[self.groupNames[self.currGroupID]] self.currNode = 0 - node = self.currGroup[ self.currNode ] + node = self.currGroup[self.currNode] self.currNode += 1 - return self.groupNames[ self.currGroupID ], node - - def __init__( self ): + return self.groupNames[self.currGroupID], node + + def __init__(self): self.vertices = [] # the set of vertices in the mesh - self.groups = {} # a mapping from node group names to its nodes + self.groups = {} # a mapping from node group names to its nodes self.nodes = [] self.edges = [] self.obstacles = [] - def getNodeIterator( self ): - '''Returns an iterator for passing through all of the nodes in a fixed, - repeatable order''' - return NavMesh.NodeIterator( self ) - - def addNode( self, node, nodeGrp='defaultGrp' ): - '''Adds a node to the mesh and returns the index''' - idx = len( self.nodes ) - self.nodes.append( node ) - if ( self.groups.has_key( nodeGrp ) ): - self.groups[ nodeGrp ].append( node ) + def getNodeIterator(self): + """Returns an iterator for passing through all of the nodes in a fixed, + repeatable order""" + return NavMesh.NodeIterator(self) + + def addNode(self, node, nodeGrp='defaultGrp'): + """Adds a node to the mesh and returns the index""" + idx = len(self.nodes) + self.nodes.append(node) + if nodeGrp in self.groups: + self.groups[nodeGrp].append(node) else: - self.groups[ nodeGrp ] = [ node ] + self.groups[nodeGrp] = [node] return idx - def addEdge( self, edge ): - '''Adds a edge to the mesh and returns the index''' - idx = len( self.edges ) - self.edges.append( edge ) + def addEdge(self, edge): + """Adds a edge to the mesh and returns the index""" + idx = len(self.edges) + self.edges.append(edge) return idx - def groupOrder( self ): - '''Returns a dictionary which maps each node to its final + def groupOrder(self): + """Returns a dictionary which maps each node to its final file output index. Used to connect edges and obstacles to - nodes.''' + nodes.""" count = 0 nodeMap = {} for group, node in self.getNodeIterator(): - nodeMap[ node ] = count + nodeMap[node] = count count += 1 - return nodeMap - - def writeNavFile( self, fileName, ascii=True ): - '''Outputs the navigation mesh into a .nav file''' - if ( ascii ): - if ( not fileName.lower().endswith( '.nav' ) ): + return nodeMap + + def writeNavFile(self, fileName, ascii=True): + """Outputs the navigation mesh into a .nav file""" + if ascii: + if not fileName.lower().endswith('.nav'): fileName += '.nav' - self.writeNavFileAscii( fileName ) + self.writeNavFileAscii(fileName) else: - if ( not fileName.lower().endswith( '.nbv' ) ): + if not fileName.lower().endswith('.nbv'): fileName += '.nbv' - self.writeNavFileBinary( fileName ) + self.writeNavFileBinary(fileName) - def writeNavFileAscii( self, fileName ): - '''Writes the ascii navigation mesh file''' - f = open( fileName, 'w' ) + def writeNavFileAscii(self, fileName): + """Writes the ascii navigation mesh file""" + f = open(fileName, 'w') # vertices - f.write( '%d' % len( self.vertices ) ) + f.write('%d' % len(self.vertices)) for x, y in self.vertices: - f.write( '\n\t%.5f %.5f' % ( x, y ) ) + f.write('\n\t%.5f %.5f' % (x, y)) nodeMap = self.groupOrder() # edges - f.write( '\n%d' % len( self.edges ) ) + f.write('\n%d' % len(self.edges)) for e in self.edges: - f.write( '\n\t%s' % e.asciiString( nodeMap ) ) + f.write('\n\t%s' % e.asciiString(nodeMap)) # obstacles - f.write( '\n%d' % len( self.obstacles ) ) + f.write('\n%d' % len(self.obstacles)) for o in self.obstacles: - f.write( '\n\t%s' % o.asciiString( nodeMap ) ) + f.write('\n\t%s' % o.asciiString(nodeMap)) # node groups currGrp = '' for group, node in self.getNodeIterator(): - if ( group != currGrp ): - f.write( '\n%s' % group ) - f.write( '\n%d' % ( len( self.groups[ group ] ) ) ) + if group != currGrp: + f.write('\n%s' % group) + f.write('\n%d' % (len(self.groups[group]))) currGrp = group - f.write( '\n%s\n' % ( node.asciiString( '\t') ) ) - + # nodes + f.write('\n%s\n' % (node.asciiString('\t'))) + f.close() - def writeNavFileBinary( self, fileName ): - '''Writes the ascii navigation mesh file''' + def writeNavFileBinary(self, fileName): + """Writes the ascii navigation mesh file""" # TODO: Make this valid pass -## f = open( fileName, 'wb' ) -## # vertices -## f.write( struct.pack('i', len( self.vertices ) ) ) -## for x,y in self.vertices: -## f.write( struct.pack('ff', x, y ) ) -## # edges -## f.write( struct.pack('i', len( self.edges ) ) ) -## for e in self.edges: -## f.write( e.binaryString() ) -## # nodes -## f.write( struct.pack('i', len( self.nodes ) ) ) -## for n in self.nodes: -## f.write( n.binaryString() ) -## # obstacles -## f.write( struct.pack( 'i', len( self.obstacles ) ) ) -## for o in self.obstacles: -## f.write( struct.pack('i', len( o ) ) ) -## f.write( ''.join( map( lambda x: struct.pack( 'i',x ), o ) ) ) -## f.close() - - +# f = open( fileName, 'wb' ) +# # vertices +# f.write( struct.pack('i', len( self.vertices ) ) ) +# for x,y in self.vertices: +# f.write( struct.pack('ff', x, y ) ) +# # edges +# f.write( struct.pack('i', len( self.edges ) ) ) +# for e in self.edges: +# f.write( e.binaryString() ) +# # nodes +# f.write( struct.pack('i', len( self.nodes ) ) ) +# for n in self.nodes: +# f.write( n.binaryString() ) +# # obstacles +# f.write( struct.pack( 'i', len( self.obstacles ) ) ) +# for o in self.obstacles: +# f.write( struct.pack('i', len( o ) ) ) +# f.write( ''.join( map( lambda x: struct.pack( 'i',x ), o ) ) ) +# f.close() diff --git a/objToNavMesh.py b/objToNavMesh.py index dd56cbc..17bd021 100644 --- a/objToNavMesh.py +++ b/objToNavMesh.py @@ -1,15 +1,18 @@ +#! /usr/bin/env python3 + # Parses an OBJ file and outputs an NavMesh file definition # - see navMesh.py for the definition of that file format import sys -from ObjReader import ObjFile -from navMesh import Node, Edge, Obstacle, NavMesh import numpy as np -from primitives import Vector2 +from .ObjReader import ObjFile +from .navMesh import Node, Edge, Obstacle, NavMesh +from .primitives import Vector2 + def analyze_obj(obj_file, vertex_tolerance=1e-4): - '''Analyzes the obj. It assess various aspects of the OBJ to determine if it is + """Analyzes the obj. It assess various aspects of the OBJ to determine if it is sufficiently "clean" to have a navigation mesh. Some failed tests lead to warnings, others lead to exit errors. "Clean" means the following: @@ -26,16 +29,16 @@ def analyze_obj(obj_file, vertex_tolerance=1e-4): @param obj_file The parsed OBJ file to analyze. @param vertex_tolerance The minimum distance required between vertices. - ''' + """ warnings = [] errors = [] - ## This determines if adjacent faces have reversed winding. We do this by looking at - ## how the edges are implicitly defined. The face (v0, v1, v2, v3) has edges - ## (v0, v1), (v1, v2), (v2, v3), and (v3, v0). Given the first edge, (v0, v1), if it - ## is shared by another face, that face should have it ordered as (v1, v0). This - ## represents consistent winding. If two faces refer to the same edge in the same - ## order, then they have inconsistent winding. + # # This determines if adjacent faces have reversed winding. We do this by looking at + # # how the edges are implicitly defined. The face (v0, v1, v2, v3) has edges + # # (v0, v1), (v1, v2), (v2, v3), and (v3, v0). Given the first edge, (v0, v1), if it + # # is shared by another face, that face should have it ordered as (v1, v0). This + # # represents consistent winding. If two faces refer to the same edge in the same + # # order, then they have inconsistent winding. # A map from edge edge (v0, v1) to the face that referenced it. edge_to_face = {} # A map from each unique edge identifier (a, b) to the faces that reference it. @@ -43,7 +46,7 @@ def analyze_obj(obj_file, vertex_tolerance=1e-4): unique_edges = {} for face, _ in obj_file.getFaceIterator(): v_count = len(face.verts) - for v_idx in xrange(-1, v_count - 1): + for v_idx in range(-1, v_count - 1): edge = (face.verts[v_idx], face.verts[v_idx + 1]) if edge in edge_to_face: errors.append("The faces on lines {} and {} have inconsistent winding" @@ -61,9 +64,9 @@ def analyze_obj(obj_file, vertex_tolerance=1e-4): # Test for vertex distance against the given distance tolerance. Note: this is an # O(N^2) operation. In the future, this *could* be accelerated as necessary. - for i in xrange(len(obj_file.vertSet) - 1): + for i in range(len(obj_file.vertSet) - 1): v_i = obj_file.vertSet[i] - for j in xrange(i + 1, len(obj_file.vertSet)): + for j in range(i + 1, len(obj_file.vertSet)): v_j = obj_file.vertSet[j] delta = (v_i - v_j).length() if delta <= vertex_tolerance: @@ -85,68 +88,72 @@ def analyze_obj(obj_file, vertex_tolerance=1e-4): return False return True -def popEdge( e, vertMap, edges ): - '''Removes the edge, e, from all references in the vertMap''' - v0, v1 = edges[ e ] + +def popEdge(e, vertMap, edges): + """Removes the edge, e, from all references in the vertMap""" + v0, v1 = edges[e] try: - vertMap[ v0 ].pop( vertMap[ v0 ].index( e ) ) - if ( not vertMap[ v0 ] ): - vertMap.pop( v0 ) + vertMap[v0].pop(vertMap[v0].index(e)) + if not vertMap[v0]: + vertMap.pop(v0) except ValueError: pass - + try: - vertMap[ v1 ].pop( vertMap[ v1 ].index( e ) ) - if ( not vertMap[ v1 ] ): - vertMap.pop( v1 ) + vertMap[v1].pop(vertMap[v1].index(e)) + if not vertMap[v1]: + vertMap.pop(v1) except ValueError: pass -def pushEdge( e, vertMap, edges ): - '''Places an edge into the vertex-edge map''' - v0, v1 = edges[ e ] - if ( vertMap.has_key( v0 ) ): - assert( e not in vertMap[ v0 ] ) - vertMap[ v0 ].append( e ) + +def pushEdge(e, vertMap, edges): + """Places an edge into the vertex-edge map""" + v0, v1 = edges[e] + if v0 in vertMap: + assert (e not in vertMap[v0]) + vertMap[v0].append(e) else: - vertMap[ v0 ] = [e] - - if ( vertMap.has_key( v1 ) ): - assert( e not in vertMap[ v1 ] ) - vertMap[ v1 ].append( e ) + vertMap[v0] = [e] + + if v1 in vertMap: + assert (e not in vertMap[v1]) + vertMap[v1].append(e) else: - vertMap[ v1 ] = [e] + vertMap[v1] = [e] + -def extendEdge( e, o, edgeLoop, edges, vertMap ): - '''Extends the obstacle, o, with the given edge, e. The edgeLoop gets extended +def extendEdge(e, o, edgeLoop, edges, vertMap): + """Extends the obstacle, o, with the given edge, e. The edgeLoop gets extended by the edge and the vertMap is modified to reflect success. @return: -1 - path still valid 0 - invalid cycle created (i.e. e's final vertex in body of o 1 - valid cycle finished. - ''' - edgeLoop.append( e ) - popEdge( e, vertMap, edges ) - vPrev, vNext = edges[ e ] - if ( vPrev != o[-1] ): + """ + edgeLoop.append(e) + popEdge(e, vertMap, edges) + vPrev, vNext = edges[e] + if vPrev != o[-1]: tmp = vPrev vPrev = vNext vNext = tmp - assert( vPrev == o[-1] ) - if ( vNext == o[0] ): + assert (vPrev == o[-1]) + if vNext == o[0]: # reached cycle - obstacle grown to loop return 1 - elif ( vNext in o ): + elif vNext in o: # created a cycle, but not with the first vertex - edgeLoop.pop( -1 ) - pushEdge( e, vertMap, edges ) + edgeLoop.pop(-1) + pushEdge(e, vertMap, edges) return 0 else: - o.append( vNext ) + o.append(vNext) return -1 -def growObstacle( o, edgeLoop, edges, vertMap ): - '''Given an obstacle (and the corresponding edge loop) grows the obstacle to + +def growObstacle(o, edgeLoop, edges, vertMap): + """Given an obstacle (and the corresponding edge loop) grows the obstacle to a single, closed loop. @param o: the current obstacle (a list of vertex indices) @@ -154,102 +161,168 @@ def growObstacle( o, edgeLoop, edges, vertMap ): edge consisting of the ith and i+1st vertices in o. @param edges: the edge definitions (to which the edge indices refer) @param vertMap: the mapping from vertex index to edges which share it - @return: boolean, reporting if a closed obstacle was found. - ''' + @return: boolean, reporting if a closed obstacle was found. + """ v = o[-1] - if ( len( vertMap[ v ] ) == 1 ): + if len(vertMap[v]) == 1: # simple case -- take the only option - e = vertMap[ v ][ 0 ] - state = extendEdge( e, o, edgeLoop, edges, vertMap ) - if ( state == 0 ): + e = vertMap[v][0] + state = extendEdge(e, o, edgeLoop, edges, vertMap) + if state == 0: return False - elif ( state == 1 ): + elif state == 1: return True - if ( growObstacle( o, edgeLoop, edges, vertMap ) ): + if growObstacle(o, edgeLoop, edges, vertMap): return True else: # this path failed to produce a loop - edgeLoop.pop( -1 ) - pushEdge( e, vertMap ) + edgeLoop.pop(-1) + pushEdge(e, vertMap) else: - for e in vertMap[ v ]: - state = extendEdge( e, o, edgeLoop, edges, vertMap ) - if ( state == 0 ): + for e in vertMap[v]: + state = extendEdge(e, o, edgeLoop, edges, vertMap) + if state == 0: return False - elif ( state == 1 ): + elif state == 1: return True - if ( growObstacle( o, edgeLoop, edges, vertMap ) ): + if growObstacle(o, edgeLoop, edges, vertMap): return True else: # this path failed to produce a loop - edgeLoop.pop( -1 ) - pushEdge( e, vertMap ) + edgeLoop.pop(-1) + pushEdge(e, vertMap) return False - -def startObstacle( vertMap, edges, obstacles ): - '''Starts a new obstacle from the vertex-edge map. Modifies the vertex + + +def startObstacle(vertMap, edges, obstacles): + """Starts a new obstacle from the vertex-edge map. Modifies the vertex map in place (by removing used data) and returns the current vertex and - an obstacle, and, finally, adds that obstacle to the obstacles list.''' + an obstacle, and, finally, adds that obstacle to the obstacles list.""" v = vertMap.keys()[0] - e = vertMap[ v ][ 0 ] + e = vertMap[v][0] # remove edge from vert mapping - popEdge( e, vertMap, edges ) + popEdge(e, vertMap, edges) - o = list( edges[ e ] ) - obstacles.append( o ) + o = list(edges[e]) + obstacles.append(o) return o, e -def processObstacles( obstacles, vertObstMap, vertNodeMap, navMesh ): - '''Given a list of Obstacle instances, connects the obstacles into sequences such that each obstacle +def processObstacles(obstacles, vertObstMap, vertNodeMap, navMesh): + """Given a list of Obstacle instances, connects the obstacles into sequences such that each obstacle points to the appropriate "next" obstacle. Assigns obstacles to nodes based on vertex. - Finally, sets the obstacles to the navigation mesh.''' + Finally, sets the obstacles to the navigation mesh.""" # I'm assuming that the external edges form perfect, closed loops # That means if a vertex is incident to an obstacle, then it must be incident to two and only # two obstacles. This tests that assumption - degrees = map( lambda x: len( x ), vertObstMap.values() ) - assert( sum( map( lambda x: x % 2, degrees ) ) == 0 ) + degrees = map(lambda x: len(x), vertObstMap.values()) + assert (sum(map(lambda x: x % 2, degrees)) == 0) # now connect them up # - this assumes that they are all wound properly + # for vertID in vertObstMap.keys(): + # o0, o1 = vertObstMap[vertID] + # obst0 = obstacles[o0] + # obst1 = obstacles[o1] + # if obst0.v0 == vertID: + # obst1.next = o0 + # else: + # obst0.next = o1 + # # The obstacle should be in the set of every node built on this vertex + # for node in vertNodeMap[vertID]: + # node.addObstacle(o0) + # node.addObstacle(o1) + for vertID in vertObstMap.keys(): - o0, o1 = vertObstMap[ vertID ] - obst0 = obstacles[ o0 ] - obst1 = obstacles[ o1 ] - if ( obst0.v0 == vertID ): - obst1.next = o0 + obst_idx = vertObstMap[vertID] + obst_list = list(map(obstacles.__getitem__, obst_idx)) + if len(obst_list) == 2: + pairs = [(0, 1)] else: - obst0.next = o1 - # The obstacle should be in the set of every node built on this vertex - for node in vertNodeMap[ vertID ]: - node.addObstacle( o0 ) - node.addObstacle( o1 ) + assert len(obst_list) == 4, \ + "max 4 obstacles are allowed to intersect in one vertex with the current implementation" + combinations = [[(0, 1), (2, 3)], + [(0, 2), (1, 3)], + [(0, 3), (1, 2)]] + min_angle_sum = 2 * np.pi + best_c = -1 + for c, combination in enumerate(combinations): + angle_sum = 0 + for pair in combination: + if not (obst_list[pair[0]].v0 == obst_list[pair[1]].v1 + or obst_list[pair[0]].v1 == obst_list[pair[1]].v0): + angle_sum = min_angle_sum + break + angle_sum += obst_list[pair[0]].get_angle(obst_list[pair[1]], navMesh.vertices) + if angle_sum < min_angle_sum: + best_c = c + min_angle_sum = angle_sum + pairs = combinations[best_c] + + # shared = navMesh.vertices[vertID] # vertex that is shared between obstacles + for o0, o1 in pairs: + obst0, obst1 = obst_list[o0], obst_list[o1] + if obst0.v0 == vertID: + obst1.next = obst_idx[o0] + # # obst_second is the vertex from that obstacle edge that is not shared between o0 and o1 + # obst0_second = navMesh.vertices[obst0.v1] + # obst1_second = navMesh.vertices[obst1.v0] + else: + obst0.next = obst_idx[o1] + # # obst_second is the vertex from that obstacle edge that is not shared between o0 and o1 + # obst0_second = navMesh.vertices[obst0.v0] + # obst1_second = navMesh.vertices[obst1.v1] + + for node in vertNodeMap[vertID]: + node.addObstacle(obst_idx[o0]) + node.addObstacle(obst_idx[o1]) + + # if len(pairs) == 1: + # # The obstacle should be in the set of every node built on this vertex + # for node in vertNodeMap[vertID]: + # node.addObstacle(obst_idx[o0]) + # node.addObstacle(obst_idx[o1]) + # else: + # # Obstacle should only be in set of nodes that lie between the two obstacles + # # position determines the side on which a point (p_x, p_y) lies from that obstacle edge + # position_o0 = lambda p_x, p_y: np.sign((obst0_second[0] - shared[0]) * (p_y - shared[1]) + # - (obst0_second[1] - shared[1]) * (p_x - shared[0])) + # position_o1 = lambda p_x, p_y: np.sign((obst1_second[0] - shared[0]) * (p_y - shared[1]) + # - (obst1_second[1] - shared[1]) * (p_x - shared[0])) + # for node in vertNodeMap[vertID]: + # # determine whether node lies between obstacle 0 and 1 + # # node lies between if center is on different sides for both obstacles edges + # between = (position_o0(node.center.x, node.center.y) + # * position_o1(node.center.x, node.center.y) == -1) + # if between: + # node.addObstacle(obst_idx[o0]) + # node.addObstacle(obst_idx[o1]) # all obstacles now have a "next" obstacle - assert( len( filter( lambda x: x.next == -1, obstacles ) ) == 0 ) + assert (len(list(filter(lambda x: x.next == -1, obstacles))) == 0) navMesh.obstacles = obstacles def projectVertices(vertexList, y_up): - '''Given 3D vertices, projects them to 2D for the navigation mesh. Specifically, + """Given 3D vertices, projects them to 2D for the navigation mesh. Specifically, projects them to a plane perpendicular to the y-axis (if y_up is True, otherwise uses - the z-axis).''' - #TODO: Eventually the navigation mesh will require 3D data when it is no longer topologically planar + the z-axis).""" + # TODO: Eventually the navigation mesh will require 3D data when it is no longer topologically planar # The index of the 3D axis which maps to the 2d y-axis. Default to y-up (so we keep z.) y_2d_axis = 2 - if y_up == False: + if not y_up: # Z is up, so we keep the y-axis value. y_2d_axis = 1 - verts = map(lambda x: (x[0], x[y_2d_axis]), vertexList) + verts = list(map(lambda x: (x[0], x[y_2d_axis]), vertexList)) return verts -def buildNavMesh(objFile, y_up, vertex_distance): - '''Given an ObjFile object, constructs the navigation mesh.writeNavFile +def buildNavMesh(objFile, y_up): + """Given an ObjFile object, constructs the navigation mesh.writeNavFile The nodes will be grouped according to the obj face groups. @param objFile The parsed obj file with obj-style, 1-indexed vertex @@ -258,12 +331,7 @@ def buildNavMesh(objFile, y_up, vertex_distance): is defined on the xz-plane with elevation as y(x, z). If False, <0, 0, 1> is the up vector and the 2D polygon is on the yz-plane with z(x, y). - @param vertex_distance A tolerance communicating a lower bound on the expected - distances between all obj mesh vertices. If vertices are - found this distance or nearer, a warning will be issued. - ''' - if not analyze_obj(objFile, vertex_distance): - sys.exit(1) + """ def extract_2d(v): if y_up: @@ -280,7 +348,7 @@ def extract_up(v): navMesh = NavMesh() V = objFile.vertSet navMesh.vertices = projectVertices(V, y_up) - vertNodeMap = {} # maps a vertex index to all nodes that are incident to it + vertNodeMap = {} # maps a vertex index to all nodes that are incident to it edges = [] # a dicitionary mapping an edge definition to the faces that are incident to it # an "edge definition" is a two tuple of ints (a, b) such that: @@ -288,8 +356,8 @@ def extract_up(v): # a < b edgeMap = {} nodes = [] - for f, (face, grpName) in enumerate( objFile.getFaceIterator() ): - vCount = len( face.verts ) + for f, (face, grpName) in enumerate(objFile.getFaceIterator()): + vCount = len(face.verts) # create node node = Node() # compute plane @@ -299,42 +367,42 @@ def extract_up(v): M = [] b = [] center_2d = Vector2(0, 0) - vCount = len( face.verts ) - for v in xrange( vCount ): + vCount = len(face.verts) + for v in range(vCount): # build the matrix for this mesh # NOTE: The obj file seems to be storing the obj, 1-indexed vertex value. - vIdx = face.verts[ v ] - 1 - if ( not vertNodeMap.has_key( vIdx ) ): - vertNodeMap[ vIdx ] = [ node ] + vIdx = face.verts[v] - 1 + if vIdx not in vertNodeMap: + vertNodeMap[vIdx] = [node] else: - vertNodeMap[ vIdx ].append( node ) - vert = V[ vIdx ] + vertNodeMap[vIdx].append(node) + vert = V[vIdx] x_2d, y_2d = extract_2d(vert) center_2d += Vector2(x_2d, y_2d) M.append((x_2d, y_2d, 1)) b.append(extract_up(vert)) # define the edge - nextIdx = face.verts[ ( v + 1 ) % vCount ] - 1 - edge = ( min( vIdx, nextIdx ), max( vIdx, nextIdx ) ) - if ( not edgeMap.has_key( edge ) ): - edgeMap[ edge ] = [ (f,face) ] - elif ( len( edgeMap[ edge ] ) > 1 ): - raise AttributeError, "Edge %s has too many incident faces" % ( edge ) + nextIdx = face.verts[(v + 1) % vCount] - 1 + edge = (min(vIdx, nextIdx), max(vIdx, nextIdx)) + if edge not in edgeMap: + edgeMap[edge] = [(f, face)] + elif len(edgeMap[edge]) > 1: + raise AttributeError("Edge %s has too many incident faces" % edge) else: - edgeMap[ edge ].append( (f,face) ) + edgeMap[edge].append((f, face)) node.center = center_2d / vCount - if ( vCount == 3 ): + if vCount == 3: # solve explicitly try: - A, B, C = np.linalg.solve( M, b ) + A, B, C = np.linalg.solve(M, b) except np.linalg.linalg.LinAlgError: raise ValueError("Face defined on line {} is too close to being co-linear" .format(objFile.object_line_numbers[face])) else: # least squares - x, resid, rank, s = np.linalg.lstsq(M, b) + x, resid, rank, s = np.linalg.lstsq(M, b, rcond=None) # TODO: Use rank and resid to confirm quality of answer: # rank will measure linear independence # resid will report planarity. @@ -347,23 +415,23 @@ def extract_up(v): node.C = C navMesh.addNode(node, grpName) - print "Found %d edges" % ( len( edgeMap ) ) + print("Found %d edges" % len(edgeMap)) edges = edgeMap.keys() - internal = filter( lambda x: len( edgeMap[ x ] ) > 1, edges ) - external = filter( lambda x: len( edgeMap[ x ] ) == 1, edges ) - print "\tFound %d internal edges" % len( internal ) - print "\tFound %d external edges" % len( external ) + internal = list(filter(lambda x: len(edgeMap[x]) > 1, edges)) + external = list(filter(lambda x: len(edgeMap[x]) == 1, edges)) + print("\tFound %d internal edges" % len(internal)) + print("\tFound %d external edges" % len(external)) # process the internal edges - for i, e in enumerate( internal ): + for i, e in enumerate(internal): v0, v1 = e - A, B = edgeMap[ e ] + A, B = edgeMap[e] a, aFace = A b, bFace = B - na = navMesh.nodes[ a ] - na.addEdge( i ) - nb = navMesh.nodes[ b ] - nb.addEdge( i ) + na = navMesh.nodes[a] + na.addEdge(i) + nb = navMesh.nodes[b] + nb.addEdge(i) edge = Edge() edge.v0 = v0 edge.v1 = v1 @@ -372,65 +440,64 @@ def extract_up(v): # is that even guaranteed? edge.n0 = na edge.n1 = nb - navMesh.addEdge( edge ) + navMesh.addEdge(edge) # process the external edges (obstacles) # for each external edge, make sure the "winding" is opposite that of the face obstacles = [] - vertObstMap = {} # mapping from vertex to the obstacles that are incident to the vertex - for i, e in enumerate( external ): - f, face = edgeMap[ e ][0] + vertObstMap = {} # mapping from vertex to the obstacles that are incident to the vertex + for i, e in enumerate(external): + f, face = edgeMap[e][0] v0, v1 = e - oID = len( obstacles ) + oID = len(obstacles) o = Obstacle() - o.n0 = navMesh.nodes[ f ] - if ( vertObstMap.has_key( v0 ) ): - vertObstMap[ v0 ].append( oID ) + o.n0 = navMesh.nodes[f] + if v0 in vertObstMap: + vertObstMap[v0].append(oID) else: - vertObstMap[ v0 ] = [ oID ] - if ( vertObstMap.has_key( v1 ) ): - vertObstMap[ v1 ].append( oID ) + vertObstMap[v0] = [oID] + if v1 in vertObstMap: + vertObstMap[v1].append(oID) else: - vertObstMap[ v1 ] = [ oID ] + vertObstMap[v1] = [oID] - i0 = face.verts.index( v0 + 1 ) - vCount = len( face.verts ) - if ( face.verts[ ( i0 + 1 ) % vCount ] == (v1+1) ): + i0 = face.verts.index(v0 + 1) + vCount = len(face.verts) + if face.verts[(i0 + 1) % vCount] == (v1 + 1): o.v0 = v0 o.v1 = v1 else: o.v0 = v1 o.v1 = v0 - obstacles.append( o ) - - processObstacles( obstacles, vertObstMap, vertNodeMap, navMesh ) + obstacles.append(o) - print "Found %d obstacles" % len( obstacles ) -## for o in obstacles: -## print '\t', ' '.join( map( lambda x: str(x), o ) ) + processObstacles(obstacles, vertObstMap, vertNodeMap, navMesh) + + print("Found %d obstacles" % len(obstacles)) + # for o in obstacles: + # print '\t', ' '.join( map( lambda x: str(x), o ) ) return navMesh + def main(): - import os, optparse + import optparse parser = optparse.OptionParser() - parser.set_description( 'Given an obj which defines a navigation mesh, this outputs ' - 'the corresponding navigation mesh file. The mesh must be ' - 'defined in a y-up world.' ) + parser.set_description('Given an obj which defines a navigation mesh, this outputs ' + 'the corresponding navigation mesh file. The mesh must be ' + 'defined in a y-up world.') parser.add_option("-i", "--input", action="store", dest="objFileName", default='', help="Name of obj file to convert") parser.add_option("-o", "--output", action="store", dest="navFileName", default='output', help="The name of the output file. The extension " - "will automatically be added (.nav for ascii, .nbv for binary).") + "will automatically be added (.nav for ascii, .nbv for binary).") parser.add_option('-u', '--up', dest='up', default='Y', action='store', help='The direction of the up vector -- should be either Y or Z') - parser.add_option('-d', '--distance', dest='vertex_distance', action='store', - type=float, default=1e-5, - help='Vertices are expected to be farther apart than this value. ' - 'Must be a positive number.') -## parser.add_option( "-b", "--binary", help="Determines if the navigation mesh file is saved as a binary (by default, it saves an ascii file.", -## action="store_false", dest="outAscii", default=True ) + parser.add_option("-b", "--binary", + help="Determines if the navigation mesh file is saved as a binary " + "(by default, it saves an ascii file.", + action="store_false", dest="outAscii", default=True ) options, args = parser.parse_args() y_up = True @@ -442,30 +509,24 @@ def main(): parser.print_help() sys.exit(1) - if options.vertex_distance <= 0.0: - print('\nError: The vertex distance value must be strictly positive. Found {}\n' - .format(options.vertex_distance)) - parser.print_help() - sys.exit(1) - objFileName = options.objFileName - if ( objFileName == '' ): + if objFileName == '': parser.print_help() sys.exit(1) - print "Parsing", objFileName - obj = ObjFile( objFileName ) + print("Parsing", objFileName) + obj = ObjFile(objFileName) gCount, fCount = obj.faceStats() - print "\tFile has %d faces" % fCount + print("\tFile has %d faces" % fCount) - mesh = buildNavMesh(obj, y_up, options.vertex_distance) + mesh = buildNavMesh(obj, y_up) outName = options.navFileName -## ascii = options.outAscii + # ascii = options.outAscii ascii = True - mesh.writeNavFile( outName, ascii ) - + mesh.writeNavFile(outName, ascii) + if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/primitives.py b/primitives.py index 717f5fe..1a22c98 100644 --- a/primitives.py +++ b/primitives.py @@ -1,467 +1,485 @@ +#! /usr/bin/env python3 + from copy import deepcopy from struct import pack from math import sqrt + class Vector2(object): def __init__(self, x, y): self.x = float(x) self.y = float(y) - def asTuple( self ): - return (self.x, self.y) + def asTuple(self): + return self.x, self.y - def __eq__( self, v ): + def __eq__(self, v): return self.x == v.x and self.y == v.y - def __neq__( self, v ): + def __ne__(self, v): return self.x != v.x or self.y != v.y - def __sub__( self, v ): - return Vector2( self.x - v.x, self.y - v.y ) + def __sub__(self, v): + return Vector2(self.x - v.x, self.y - v.y) - def __isub__( self, v ): + def __isub__(self, v): self.x -= v.x self.y -= v.y return self - def __neg__( self ): - return Vector2( -self.x, -self.y ) + def __neg__(self): + return Vector2(-self.x, -self.y) - def __add__( self, v ): - return Vector2( self.x + v.x, self.y + v.y ) + def __add__(self, v): + return Vector2(self.x + v.x, self.y + v.y) - def __iadd__( self, v ): + def __iadd__(self, v): self.x += v.x self.y += v.y return self - def __div__( self, s ): - return Vector2( self.x / s, self.y / s ) + def __truediv__(self, s): + return Vector2(self.x / s, self.y / s) - def __mul__( self, s ): - return Vector2( self.x * s, self.y * s ) + def __mul__(self, s): + return Vector2(self.x * s, self.y * s) - def __rmul__( self, s ): - return Vector2( self.x * s, self.y * s ) + def __rmul__(self, s): + return Vector2(self.x * s, self.y * s) - def normalize_ip( self ): + def normalize_ip(self): lenRecip = 1.0 / self.magnitude() self.x *= lenRecip self.y *= lenRecip - def normalize( self ): + def normalize(self): """Returns a normalized version of the vector""" mag = self.magnitude() - if ( mag > 0.0 ): - return Vector2( self.x / mag, self.y / mag ) + if mag > 0.0: + return Vector2(self.x / mag, self.y / mag) else: - return Vector2( 0.0, 0.0 ) + return Vector2(0.0, 0.0) - def det( self, v ): + def det(self, v): """Computes the determinant of this vector with v""" return self.x * v.y - self.y * v.x - def dot( self, v ): + def dot(self, v): return self.x * v.x + self.y * v.y - def magnitude( self ): - return sqrt( self.x * self.x + self.y * self.y ) + def magnitude(self): + return sqrt(self.x * self.x + self.y * self.y) - def magSq( self ): - return self.x * self.x + self.y * self.y + def magSq(self): + return self.x * self.x + self.y * self.y - def __getitem__( self, index ): - if ( index == 0 ): + def __getitem__(self, index): + if index == 0: return self.x - elif (index == 1 ): + elif index == 1: return self.y else: - raise IndexError, "list index out of range" + raise IndexError("list index out of range") - def __setitem__( self, index, value ): - if ( index == 0 ): + def __setitem__(self, index, value): + if index == 0: self.x = value - elif (index == 1 ): + elif index == 1: self.y = value else: - raise IndexError, "list index out of range" + raise IndexError("list index out of range") def __str__(self): - return "<%g, %g>" % (self.x, self.y ) + return "<%g, %g>" % (self.x, self.y) - def __repr__( self ): + def __repr__(self): return str(self) - def isZero( self ): - '''Reports if the vector is zero''' + def __hash__(self): + return hash((self.x, self.y)) + + def isZero(self): + """Reports if the vector is zero""" return self.x == 0.0 and self.y == 0.0 - def negate( self ): - '''Negates the vector''' + def negate(self): + """Negates the vector""" self.x = -self.x self.y = -self.y + class Vector3(object): - def __init__( self, x, y, z ): + def __init__(self, x, y, z): self.x = float(x) self.y = float(y) self.z = float(z) - def __getitem__( self, index ): - if ( index == 0 ): + def __getitem__(self, index): + if index == 0: return self.x - elif (index == 1 ): + elif index == 1: return self.y - elif ( index == 2 ): + elif index == 2: return self.z else: - raise IndexError, "list index out of range" + raise IndexError("list index out of range") - def __setitem__( self, index, value ): - if ( index == 0 ): + def __setitem__(self, index, value): + if index == 0: self.x = value - elif (index == 1 ): + elif index == 1: self.y = value - elif (index == 2 ): + elif index == 2: self.z = value else: - raise IndexError, "list index out of range" + raise IndexError("list index out of range") def __str__(self): - return "<%6.3f, %6.3f, %6.3f>" % (self.x, self.y, self.z ) + return "<%6.3f, %6.3f, %6.3f>" % (self.x, self.y, self.z) - def __repr__( self ): + def __repr__(self): return str(self) - def __eq__( self, v ): + def __eq__(self, v): return self.x == v.x and self.y == v.y and self.z == v.z - - def __sub__( self, v ): - if ( isinstance( v, Vector3 ) ): - return Vector3( self.x - v.x, self.y - v.y, self.z - v.z ) - elif ( isinstance( v, Vector2 ) ): - return Vector2( self.x - v.x, self.y - v.y ) - def __div__( self, s ): - return Vector3( self.x / s, self.y / s, self.z / s ) + def __ne__(self, v): + return self.x != v.x or self.y != v.y or self.z != v.z - def __mul__( self, s ): - return Vector3( self.x * s, self.y * s, self.z * s ) + def __sub__(self, v): + if isinstance(v, Vector3): + return Vector3(self.x - v.x, self.y - v.y, self.z - v.z) + elif isinstance(v, Vector2): + return Vector2(self.x - v.x, self.y - v.y) - def __imul__( self, s ): + def __truediv__(self, s): + return Vector3(self.x / s, self.y / s, self.z / s) + + def __mul__(self, s): + return Vector3(self.x * s, self.y * s, self.z * s) + + def __imul__(self, s): self.x *= s self.y *= s self.z *= s return self - def __add__( self, v ): - return Vector3( self.x + v.x, self.y + v.y, self.z + v.z ) - - def __iadd__( self, v ): + def __add__(self, v): + return Vector3(self.x + v.x, self.y + v.y, self.z + v.z) + + def __iadd__(self, v): self.x += v.x self.y += v.y self.z += v.z return self - def asTuple( self ): - return (self.x, self.y, self.z) + def __hash__(self): + return hash((self.x, self.y, self.z)) - def dot( self, v ): + def asTuple(self): + return self.x, self.y, self.z + + def dot(self, v): return self.x * v.x + self.y * v.y + self.z * v.z - def cross( self, v ): + def cross(self, v): x = self.y * v.z - self.z * v.y y = self.z * v.x - self.x * v.z z = self.x * v.y - self.y * v.x - return Vector3( x, y, z ) + return Vector3(x, y, z) - def lengthSquared( self ): + def lengthSquared(self): return self.x * self.x + self.y * self.y + self.z * self.z - def length( self ): - return sqrt( self.lengthSquared() ) + def length(self): + return sqrt(self.lengthSquared()) - def magnitude( self ): + def magnitude(self): return self.length() - def normalize_ip( self ): + def normalize_ip(self): lenRecip = 1.0 / self.length() self.x *= lenRecip self.y *= lenRecip self.z *= lenRecip - def minAxis( self ): + def minAxis(self): """Returns the axis with the minimum value""" - dir = 0 + direction = 0 minVal = self.x - if ( self.y < minVal ): + if self.y < minVal: minVal = self.y - dir = 1 - if ( self.z < minVal ): - dir = 2 - return dir + direction = 1 + if self.z < minVal: + direction = 2 + return direction - def minAbsAxis( self ): + def minAbsAxis(self): """Returns the axis with the minimum absolute magnitude""" - dir = 0 + direction = 0 minVal = abs(self.x) - if ( abs(self.y) < minVal ): - minVal = sab(self.y) - dir = 1 - if ( abs(self.z) < minVal ): - dir = 2 - return dir - + if abs(self.y) < minVal: + minVal = abs(self.y) + direction = 1 + if abs(self.z) < minVal: + direction = 2 + return direction + + class Face(object): - def __init__( self, v = None, vn = None, vt = None ): - if ( v == None ): + def __init__(self, v=None, vn=None, vt=None): + if v is None: self.verts = [] else: self.verts = v - if ( vn == None ): + if vn is None: self.norms = [] else: self.norms = vn - if ( vt == None ): + if vt is None: self.uvs = [] else: self.uvs = vt - def triangulate( self ): + def triangulate(self): """Triangulates the face - returns a list of faces""" - if ( len(self.verts) == 3 ): - return [deepcopy( self ), ] + if len(self.verts) == 3: + return [deepcopy(self), ] else: newFaces = [] # blindly create a fan triangulation (v1, v2, v3), (v1, v3, v4), (v1, v4, v5), etc... for i in range(1, len(self.verts) - 1): - verts = [self.verts[0], self.verts[i], self.verts[i+1]] + verts = [self.verts[0], self.verts[i], self.verts[i + 1]] norms = None - if ( self.norms ): - norms = [self.norms[0], self.norms[i], self.norms[i+1]] + if self.norms: + norms = [self.norms[0], self.norms[i], self.norms[i + 1]] uvs = None - if ( self.uvs ): - uvs = [self.uvs[0], self.uvs[i], self.uvs[i+1]] - newFaces.append( Face( verts, norms, uvs ) ) + if self.uvs: + uvs = [self.uvs[0], self.uvs[i], self.uvs[i + 1]] + newFaces.append(Face(verts, norms, uvs)) return newFaces - - def OBJFormat( self ): + def OBJFormat(self): """Writes face definition in OBJ format""" s = 'f ' vIndex = 0 for v in self.verts: s += '%d' % v - if ( self.uvs ): + if self.uvs: s += '/%d' % self.uvs[vIndex] - if ( self.norms ): - if (not self.uvs ): + if self.norms: + if not self.uvs: s += '/' s += '/%d' % self.norms[vIndex] s += ' ' vIndex += 1 return s - def PLYAsciiFormat( self, useNorms = False, useUvs = False ): + def PLYAsciiFormat(self, useNorms=False, useUvs=False): """Writes face definition in PLY format""" s = '%d ' % (len(self.verts)) vIndex = 0 for v in self.verts: - s += '%d' % ( v - 1 ) -## if ( self.uvs ): -## s += '/%d' % self.uvs[vIndex] -## if ( self.norms ): -## if (not self.uvs ): -## s += '/' -## s += '/%d' % self.norms[vIndex] + s += '%d' % (v - 1) + # if ( self.uvs ): + # s += '/%d' % self.uvs[vIndex] + # if ( self.norms ): + # if (not self.uvs ): + # s += '/' + # s += '/%d' % self.norms[vIndex] s += ' ' vIndex += 1 - return s + return s - def PLYBinaryFormat( self, useNorms = False, useUvs = False ): + def PLYBinaryFormat(self, useNorms=False, useUvs=False): """Writes face definition in PLY format""" - s = pack('>b', len(self.verts) ) -## vIndex = 0 + s = pack('>b', len(self.verts)) + # vIndex = 0 for v in self.verts: - s += pack('>i', ( v - 1 ) ) -## if ( self.uvs ): -## s += '/%d' % self.uvs[vIndex] -## if ( self.norms ): -## if (not self.uvs ): -## s += '/' -## s += '/%d' % self.norms[vIndex] -## vIndex += 1 - return s + s += pack('>i', (v - 1)) + # if ( self.uvs ): + # s += '/%d' % self.uvs[vIndex] + # if ( self.norms ): + # if (not self.uvs ): + # s += '/' + # s += '/%d' % self.norms[vIndex] + # vIndex += 1 + return s + class Vertex: - def __init__( self, x, y, z ): + def __init__(self, x, y, z): self.pos = (x, y, z) - def formatOBJ( self ): + def formatOBJ(self): """Returns a string that represents this vertex""" - return "v %f %f %f" % ( self.pos[0], self.pos[1], self.pos[2] ) + return "v %f %f %f" % (self.pos[0], self.pos[1], self.pos[2]) - def asciiPlyHeader( self, count ): + def asciiPlyHeader(self, count): """Returns the header for this element in ply format""" - s = 'element vertex %d\n' % ( count ) + s = 'element vertex %d\n' % count s += 'property float x\n' s += 'property float y\n' s += 'property float z\n' return s - - def formatPLYAscii( self ): + + def formatPLYAscii(self): """Returns a string that represents this vertex in ascii ply format""" - return "%f %f %f" % ( self.pos[0], self.pos[1], self.pos[2] ) + return "%f %f %f" % (self.pos[0], self.pos[1], self.pos[2]) - def binPlyHeader( self, count ): + def binPlyHeader(self, count): """Returns the header for this element in binary ply format""" - s = 'element vertex %d\x0a' % ( count ) + s = 'element vertex %d\x0a' % count s += 'property float x\x0a' s += 'property float y\x0a' s += 'property float z\x0a' return s - def formatPlyBinary( self ): + def formatPlyBinary(self): """Returns a string that represents this vertex in binary PLY format""" - return pack('>fff', v.x, v.y, v.z) + return pack('>fff', *self.pos) + -class ColoredVertex( Vertex ): - DEF_COLOR = ( 0, 60, 120 ) - def __init__( self, color = None ): - Vertex.__init__( self ) - if ( color == None ): +class ColoredVertex(Vertex): + DEF_COLOR = (0, 60, 120) + + def __init__(self, color=None): + Vertex.__init__(self) + if color is None: self.color = ColoredVertex.DEF_COLOR else: self.color = color - def asciiPlyHeader( self, count ): + def asciiPlyHeader(self, count): """Returns the header for this element in ply format""" - s = Vertex.asciiPlyHeader( self, count ) + s = Vertex.asciiPlyHeader(self, count) s += 'property uchar red\n' s += 'property uchar green\n' s += 'property uchar blue\n' return s - - def formatPLYAscii( self ): + + def formatPLYAscii(self): """Returns a string that represents this vertex in ascii ply format""" - return "%f %f %f %d %d %d" % ( self.pos[0], self.pos[1], self.pos[2], - self.color[0], self.color[1], self.color[2] ) + return "%f %f %f %d %d %d" % (self.pos[0], self.pos[1], self.pos[2], + self.color[0], self.color[1], self.color[2]) - def binPlyHeader( self, count ): + def binPlyHeader(self, count): """Returns the header for this element in binary ply format""" - s = Vertex.binPlyHeader( self, count ) + s = Vertex.binPlyHeader(self, count) s += 'property uchar red\x0a' s += 'property uchar green\x0a' s += 'property uchar blue\x0a' return s - def formatPlyBinary( self ): + def formatPlyBinary(self): """Returns a string that represents this vertex in binary PLY format""" - return Vertex.formatPlyBinary( self ) + pack('>BBB', color[0], color[1], color[2]) - - + return Vertex.formatPlyBinary(self) + pack('>BBB', *self.color) + + class Segment: - '''A line segment''' - def __init__( self, p1, p2 ): + """A line segment""" + + def __init__(self, p1, p2): self.p1 = p1 self.p2 = p2 - def __str__( self ): - return "Segment (%s, %s)" % ( self.p1, self.p2 ) + def __str__(self): + return "Segment (%s, %s)" % (self.p1, self.p2) - def __repr__( self ): - return str( self ) + def __repr__(self): + return str(self) - def midPoint( self ): + def midPoint(self): """Returns the mid-point of the line""" try: - return ( self.p1 + self.p2 ) * 0.5 + return (self.p1 + self.p2) * 0.5 except TypeError: - print type( self.p1 ), type( self.p2 ) + print(type(self.p1), type(self.p2)) - def magnitude( self ): + def magnitude(self): """Returns length of the line""" - return ( self.p2 - self.p1 ).magnitude() - - def normal( self ): - '''Returns the normal of the line''' - disp = self.p2 - self.p1 - segLen = disp.magnitude() - if ( segLen ): - norm = disp / segLen - return Vector2( -norm.y, norm.x ) + return (self.p2 - self.p1).magnitude() + + def normal(self): + """Returns the normal of the line""" + displacement = self.p2 - self.p1 + segLen = displacement.magnitude() + if segLen: + norm = displacement / segLen + return Vector2(-norm.y, norm.x) else: - return Vector2( 0, 0 ) - - def pointDistance( self, p ): + return Vector2(0, 0) + + def pointDistance(self, p): """Computes the distance between this line segment and a point p""" - disp = self.p2 - self.p1 - segLen = disp.magnitude() - norm = disp / segLen - dispP = p - self.p1 - dp = norm.dot( dispP ) - if ( dp < 0 ): + displacement = self.p2 - self.p1 + segLen = displacement.magnitude() + norm = displacement / segLen + displacementP = p - self.p1 + dp = norm.dot(displacementP) + if dp < 0: return (p - self.p1).magnitude() - elif ( dp > segLen ): - return ( p - self.p2).magnitude() + elif dp > segLen: + return (p - self.p2).magnitude() else: A = -norm.y B = norm.x - C = -( A * self.p1.x + B * self.p1.y ) - return abs( A * p.x + B * p.y + C ) + C = -(A * self.p1.x + B * self.p1.y) + return abs(A * p.x + B * p.y + C) - def implicitEquation( self ): - '''Computes the implicit equation for the line on which this segment lies. + def implicitEquation(self): + """Computes the implicit equation for the line on which this segment lies. The implicit equation is Ax + By + C = 0. This function computes this equation and returns these coefficients. @returns A 3-tuple of floats. The floats (A, B, C ) in the implicit equation. - ''' - disp = self.p2 - self.p1 - segLen = disp.magnitude() - assert( segLen > 0 ) - dir = disp / segLen + """ + displacement = self.p2 - self.p1 + segLen = displacement.magnitude() + assert (segLen > 0) + norm = displacement / segLen A = -norm.y B = norm.x - C = -( A * self.p1.x + B * self.p1.y ) + C = -(A * self.p1.x + B * self.p1.y) return A, B, C - def originDirLen( self ): - '''Returns an alternative representation of the segment: origin, direction and length. + def originDirLen(self): + """Returns an alternative representation of the segment: origin, direction and length. @returns A 3-tuple of various values: (Vector2, Vector2, float). The first value is the origin of the segment. The second is a unit normal, the direction of the segment. The final float is the length of the segment. - ''' - disp = self.p2 - self.p1 - segLen = disp.magnitude() - assert( segLen > 0 ) - dir = disp / segLen - return self.p1, dir, segLen - - def flip( self ): - '''Reverses the direction of the line''' + """ + displacement = self.p2 - self.p1 + segLen = displacement.magnitude() + assert (segLen > 0) + direction = displacement / segLen + return self.p1, direction, segLen + + def flip(self): + """Reverses the direction of the line""" t = self.p1 self.p1 = self.p2 self.p2 = t -def segmentsFromString( s, SegmentClass ): - '''Given a string of floats, constructs a list of segments. For N segments there - must be 4N floats.''' + +def segmentsFromString(s, SegmentClass): + """Given a string of floats, constructs a list of segments. For N segments there + must be 4N floats.""" lines = [] tokens = s.split() - assert( len( tokens ) % 4 == 0 ) # four floats per line + assert (len(tokens) % 4 == 0) # four floats per line while tokens: x1, y1, x2, y2 = tokens[:4] - tokens = tokens[ 4: ] - lines.append( SegmentClass( Vector2( float(x1), float(y1) ), Vector2( float(x2), float(y2) ) ) ) + tokens = tokens[4:] + lines.append(SegmentClass(Vector2(float(x1), float(y1)), Vector2(float(x2), float(y2)))) return lines - + + if __name__ == "__main__": - print "TESTING PRIMITIVES" - v2 = Vector2( 0.3, 0.9 ) - print v2 - v3 = Vector3( 1.2, 15.3, 100.0 ) - print v3 - \ No newline at end of file + print("TESTING PRIMITIVES") + v2 = Vector2(0.3, 0.9) + print(v2) + v3 = Vector3(1.2, 15.3, 100.0) + print(v3)