## grom is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by the ## Free Software Foundation; either version 2 of the License, or (at your ## option) any later version. stl-to-dxf is distributed in the hope that it ## will be useful, but WITHOUT ANY WARRANTY; without even the implied ## warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See ## the GNU General Public License for more details. You should have ## received a copy of the GNU General Public License along with stl-to-dxf; if ## not, write to the Free Software Foundation, Inc., 59 Temple Place, ## Suite 330, Boston, MA 02111-1307 USA ## ## grom is Copyright (C) 2006 Jarl Stefansson ## jarl@dallur.com from math import sqrt ################################################################ # # Make it so that all Operators are compatible with all numeric types # Make it so that all Operators are compatible with all objects # Make it so that you can create all objects by passing things in lists # Filter which objects types are allowed to be passed to operators # Use lambda to enable creation of groups passing multiple # Sync method names with those in vpython or create aliases # Verify that when one of the x/y/z attributes in a point is change the data stuff changes to ################################################################ class CartesianZero(object): """ A singleton representing 0,0,0 in a cartesian coordinate system""" # Since we use zero coordinate a whole lot and call it often it makes sense to use a singleton for performance class __impl: """ The singleton interface """ pass __instance = None # Any point of cartesian zero is equal to cartesian zero def __Point__(self, which = None, other = None): return self def __init__(self): """ Create singleton instance """ self.x = 0.0 self.y = 0.0 self.z = 0.0 # Check whether we already have an instance if CartesianZero.__instance is None: # Create and remember instance CartesianZero.__instance = CartesianZero.__impl() # Store instance reference as the only member in the handle self.__dict__['_CartesianZero__instance'] = CartesianZero.__instance class Primitive(object): """Generic Geometric Object Class""" def __init__(self): shared_init() # Stores init parameters which are shared by all objects derrived from Primitive def shared_init(self): self.closed = None self.color = None self.index = 0 # Iter enables indexing and accessing of values in a list like style def __iter__(self): return self # Getitem is the function called to use the indexing capability offereed by iter def __getitem__(self, key): return self.data[key] # Next enables getting the data values one by one by iterating over them def next(self): if self.index == len(self.data): self.index = 0 raise StopIteration returnData = self.data[self.index] self.index += 1 return returnData # An error thrown when a __Point__ method is passed an object it is unable to handle def PointTypeError(self): raise TypeError , 'Unable to convert ' + str(type(self)) + ' to a Point object' sys.exit(1) # A generic distance method to get the distance between any two points def distance(self, other = CartesianZero()): return self.calculate_distance(other) # Returnes False if any of the objects passed as *other is not a numeric type def isnumerical(self, *other): for input_value in other: if not isinstance(input_value, (int, float, long)): return False break return True # Advaned flattener which handles all iterable object instances correctly including dictionaries def flatten(self, other): """Recursively Flatten numerical data from iterable data types to a list, ignore non-numerical data types except Classes that have __iter__ methods""" result = [] for item in other: if isinstance(item, (list, tuple, Primitive)): result.extend(self.flatten(item)) elif isinstance(item, dict): result.extend(self.flatten(item.items())) elif self.isnumerical(item): result.append(item) return result # Advanced flattener which does not flatten Object Instances but only type instances (list/tuple/dict), this is usefull for retaining object relationships def flattenTypes(self, other): result = [] for item in other: if isinstance(item, Primitive) or self.isnumerical(item): result.append(item) elif isinstance(item, (list, tuple)): result.extend(self.flattenTypes(item)) elif isinstance(item, dict): result.extend(self.flatten(item.items())) else: raise TypeError , 'Object type ' + str(type(item)) + ' is not a valid argument for ' + str(type(self)) return result def inputHandler(self, *other): """Parses input objects/types and delegates the work of processing them to parseTypes which in turn uses iterateArguments""" return self.parseTypes(other) def parseTypes(self, other): """Parses all types of iterable types/objects and returns an intelligently parsed set of Points""" result = [] NewGroupFlag = True # These things are a bit tricky, self recursing for loop which handles different types in a different way for item in other: if isinstance(item, Primitive): result.append(item) elif self.isnumerical(item) or isinstance(item, str): if NewGroupFlag == True: # Data in a dictionary, first priority is data with x/y/z as key values if isinstance(other, dict): coordinates = {'x':0.0,'y':0.0,'z':0.0} for key in ['x','X','y','Y','z','Z']: try: coordinates[key] = float(other[key]) NewGroupFlag = False except KeyError: pass # If there were no x/y/z values in the keys table we parse the values in the orcer x/y/z which is traditional if NewGroupFlag == True: result.extend(self.iterateArguments(other.values(), coordinates )) NewGroupFlag = False elif NewGroupFlag == False: result.append(Point(coordinates['x'],coordinates['y'],coordinates['z'])) else: # Contents of tuples/lists is handled here NewGroupFlag = False result.extend(self.iterateArguments(other)) elif isinstance(item, (list, tuple)): # Recursively call myself to recurse into lists/tuples result.extend(self.parseTypes(item)) elif isinstance(item, dict): # Recursively call myself to recurse into dictionaries result.extend(self.parseTypes(item)) else: raise TypeError , 'Object type ' + str(type(item)) + ' is not a valid argument for ' + str(type(self)) return result def iterateArguments(self, normalList, coordinates = {'x':0.0,'y':0.0,'z':0.0}): """Takes a list/dict/tuple with coordinates in the form of {'x':0.0,'y':0.0,'z':0.0} and a list of normalized numerical values and returns a list of Point objects""" # Make sure we aer dealing with a list because of wrap-around issues with slicing tuples normalList = list(normalList) result = [] iterations = 0 #Try to parse values in pairs of 3, if there are less we try 2, if that fails we use the one field only, order is assumed to be x,y,z while iterations < len(normalList): coordinates = {'x':0.0,'y':0.0,'z':0.0} try: coordinates = {'x':0.0,'y':0.0,'z':0.0} coordinates['x'] = normalList[iterations] coordinates['y'] = normalList[iterations+1] coordinates['z'] = normalList[iterations+2] except IndexError: try: coordinates['x'] = normalList[iterations] coordinates['y'] = normalList[iterations+1] except IndexError: try: coordinates['x'] = normalList[iterations] except IndexError: # This is one of those "FUNNY" errors :P raise IndexError , 'This should only happen if for some reason a numerical/string value does not contain a value :P' iterations += 3 result.append(Point(coordinates['x'],coordinates['y'],coordinates['z'])) return result class Point(Primitive): """A vertex/knot/vector/point object for a point in 3D space""" # The native input is 0-3 floating point values, any other input will be directed to the inputHandler() # When you get a point from a point it always returns itself def __Point__(self, which = None, other = None): return self # Initial settings def __init__(self, *arguments): # Include shared init settings from Primitive self.shared_init() # If we don't get exactly three numerical arguments we let the inputHandler sort everything out if len(arguments) == 3 and self.isnumerical(arguments[0]) and self.isnumerical(arguments[1]) and self.isnumerical(arguments[2]): self.x = float(arguments[0]) self.y = float(arguments[1]) self.z = float(arguments[2]) self.data = [self.x,self.y,self.z] else: inputHandler(arguments) # Return properly formatted string def __str__(self): return ' Point(' + str(self.x) + ', ' + str(self.y) + ', ' + str(self.z) + ')' # Add two Points def __add__(self, other): return Point(self.x + other.__Point__().x, self.y + other.__Point__().y, self.z + other.__Point__().z) # Subtract two Points def __sub__(self, other): return Point(self.x - other.__Point__().x, self.y - other.__Point__().y, self.z - other.__Point__().z) # Dot product def __mul__(self, other): return Point(self.x * other.__Point__().x, self.y * other.__Point__().y, self.z * other.__Point__().z) # Scalar multiplication def __rmul__(self, other): return Point(other.__Point__().y * self.x + other.__Point__().y * self.y + other.__Point__().y * self.z) # Division def __div__(self, other): return Point(self.x / other.__Point__().x , self.y / other.__Point__().y , self.z / other.__Point__().z) # Modulo def __mod__(self, other): return Point(self.x % other.__Point__().x, self.y % other.__Point__().y, self.z % other.__Point__().z) # Exponentiation def __pow__(self, other): return Point(self.x ** other.__Point__().x, self.y ** other.__Point__().y, self.z ** other.__Point__().z) # Negative def __neg__(self): return Point(neg(self.x), neg(self.y), neg(self.z)) # Positive def __pos__(self): return Point(pos(self.x), pos(self.y), pos(self.z)) # Absolute def __abs__(self): return Point(abs(self.x), abs(self.y), abs(self.z)) # Compare two Points, 0 for same, 1 for self larger, -1 for other larger def __cmp__(self, other): if (self.x - other.x) == 0 and (self.y - other.y) == 0 and (self.z - other.z) == 0: return 0 elif self.x > other.x and self.y > other.y and self.z > other.z: return 1 else: return -1 # Length of the "Vector" between 0,0,0 and the Point def length(self): return calculate_distance(self) # Returns a point which is mid way between the two points def average(self, other): return Point(self.x - (self.x - other.x) / 2 ,self.y - (self.y - other.y) / 2 ,self.z - (self.z - other.z) / 2) # Calculate the distances correctly to any object from a point, int(0) is assumed to be Point(0,0,0) def calculate_distance(self, other = CartesianZero()): return sqrt((self.x - other.__Point__("start").x)**2 + (self.x - other.__Point__("start").y)**2 + (self.z - other.__Point__("start").z)**2) class Line(Primitive): """A line/ray/vector which is defined by two Points""" # Return a specific point from a line, first/last/middle or closest to a point, if closest is invoked without a point it returns the closest to 0,0,0 def __Point__(self, which = 0, other = CartesianZero()): if which in ["first", "start", "0", (0), [0]]: return self.point0 elif which in ["last", "end", "-1", (-1), [-1]]: return self.point1 elif which in ["center", "middle"]: return self.point0.average(self.point1) elif which in ["closest", "nearest", "near"] and isinstance(other, Point) or isinstance(other, CartesianZero): # faulty logic, should return the point on the line that is closest but not the closest defining point if self.point0.distance(other) > self.point1.distance(other): return self.point0 else: return self.point1 else: PointTypeError() # Initial settings def __init__(self, point0, point1): # Include shared init settings from Primitive self.shared_init() self.point0 = point0 self.point1 = point1 self.data = point0, point1 # Return properly formatted string def __str__(self): return ' Line[' + str(self[0]) + str(self[1]) + ']' # Length of a line or the distance from an ray start to the vector defining point def length(self): return calculate_distance(self.point0,self.point1) class Curve(Primitive): """A non-straight line defined as a NURBS curve""" # All curves should be defined as NURBS curve knot and knot-vectors, aka Series of non-connected Rays/Lines # Return a specific point from a curve, first/last/middle or closest to a point def ___Point__(self, which = 0, other = CartesianZero()): if which in ["first", "start", "0", (0), [0]]: return self[0] elif which in ["last", "end", "-1", (-1), [-1]]: return self[-1] elif which in ["center", "middle"]: # Missing implementation return self[0] elif which in ["closest", "nearest", "near"] and isinstance(other, Point) or isinstance(other, CartesianZero): # Missing implementation return self[0] else: PointTypeError() # Initial settings def __init__(self, lines): self.shared_init() self.data = lines # Return properly formatted string def __str__(self): return_string = list() for id in range(len(self.data)): return_string.append(self[id].__str__()) return ' Curve{' + str(return_string) + '}' # Enable appending to the curve def append(self, lines): for line in lines: self.data.append(line) class Polyform(Primitive): """A Geometric object consisting of any number of points listed in sequence""" # A polyform is essentially a non-interpolated polyline which can be a solid # Return a specific point from a polyform, first/last/middle or closest to a point def __Point__(self, which = 0, other = CartesianZero()): if which in ["first", "start", "0", (0), [0]]: return self[0] elif which in ["last", "end", "-1", (-1), [-1]]: return self[-1] elif which in ["center", "middle"]: # Missing implementation return self[0] elif which in ["closest", "nearest", "near"] and isinstance(other, Point) or isinstance(other, CartesianZero): # Missing implementation return self[0] else: PointTypeError() # Initial settings def __init__(self, points): # Include shared init settings from Primitive self.shared_init() self.data = points # Return properly formatted string def __str__(self): return_string = list() for id in range(len(self.data)): return_string.append(self.data[id].__str__()) return str(return_string) # Enable appending to the polyform def append(self, point): self.polyform.append(point) class Group(Primitive): """A collection of Polyforms, Curves and Lines""" # A group contains objects with a permanently fixed position relative to each other # All objects including itself(group) can be nested within a group # Groups where all objects form closed paths are considered to be solid objects or surfaces # Return a specific point from a curve, first/last/middle or closest to a point def __Point__(self, which, other = CartesianZero()): if which in ["first", "start", "0", (0), [0]]: newinstance = self while isinstance(newinstance, Group): newinstance = newinstance[0] return newinstance.__Point__[0] elif which in ["last", "end", "-1", (-1), [-1]]: newinstance = self while isinstance(newinstance, Group): newinstance = newinstance[-1] return newinstance.__Point__[-1] elif which in ["center", "middle"]: # Missing implementation return self[0] elif which in ["closest", "nearest", "near"] and isinstance(other, Point) or isinstance(other, CartesianZero): # Missing implementation return self[0] else: PointTypeError() #Initial settings def __init__(self, geoObjects): # Include shared init settings from Primitive self.shared_init() self.data = geoObjects # Return properly formatted string def __str__(self): return_string = list() for id in range(len(self.data)): return_string.append(self[id].__str__()) return ' Group<' + str(return_string) + '>' # Enable appending to groups def append(self, geoObject): self.data.append(geoObject) ################# TEST ##################### print "" print "###################### TEST ###########################" print "Create Objects" print " Create 10 points" mypoint0 = Point(0,0,0) mypoint1 = Point(10,10,10) mypoint2 = Point(20,20,20) mypoint3 = Point(30,20,30) mypoint4 = Point(40,20,30) mypoint5 = Point(50,20,30) mypoint6 = Point(60,20,30) mypoint7 = Point(70,20,30) mypoint8 = Point(80,20,30) mypoint9 = Point(90,20,30) print " Create 5 lines" myline0 = Line(mypoint0,mypoint1) myline1 = Line(mypoint2,mypoint3) myline2 = Line(mypoint4,mypoint5) myline3 = Line(mypoint6,mypoint7) myline4 = Line(mypoint8,mypoint9) print " Create 2 curves, one with 2 lines and the other with 3 lines" mycurve0 = Curve([myline0,myline1]) mycurve1 = Curve([myline2,myline3,myline4]) print " Create 2 polyforms, one with 3 points and the other with 7 points" mypolyform0 = Polyform([mypoint0,mypoint1,mypoint2]) mypolyform1 = Polyform([mypoint3,mypoint4,mypoint5,mypoint6,mypoint7,mypoint8,mypoint9]) print " Create 2 groups, one with 3 points, 2 lines, one curve and one polyline, the other everything including the other group" mygroup0 = Group([mypoint0,mypoint1,mypoint2,myline0,myline1,mycurve0,mypolyform0]) mygroup1 = Group([mypoint0,mypoint1,mypoint2,mypoint3,mypoint4,mypoint5,mypoint6,mypoint7,mypoint8,mypoint9,myline0,myline1,myline2,myline3,myline4,mycurve0,mycurve1,mypolyform0,mypolyform1,mygroup0]) print "Create two lists, a tuple and a dictionary, put list1 in another list, join it and list0 with tuple0 in a list, join these with dict0 in a tuple with mypoint3" mylist0 = [1,2,3] mylist1 = [4,5,6] mytuple0 = (7,8,9) mydict0 = {'jack': 4098, 'sape': 4139} flatteningtuple = (([mylist0,mytuple0,[mylist1]],mydict0,mypoint3)) print "Pretty formatting objects for display" newList = [] for value in mypoint0.flattenTypes(flatteningtuple): newList.append(str(value)) mydict1 = {'x':1,'y':3,'z':7} mydict2 = {'a':0,'b':3,'c':7} mydict3 = {'a':0,'b':3} mydict4 = {'a':4} mydict5 = {'n':1,'r':3,'q':7,'a':5} mytuple = (1,45,2) mylist = [1,44,33] mylist1 = [67,47] mystring = "dsdfd" myint = 2 myfloat = 23.6 mylong = long(23423434232342343343234) def printPointInType(argument): for item in (mypoint1.inputHandler(argument)): return str(item) print "" print "Cast types to string" print " mypoint0 is: " + str(mypoint0) print " myline0 is: " + str(myline0) print " mycurve0 is: " + str(mycurve0) print " mypolyform0 is: " + str(mypolyform0) print " mygroup0 is: " + str(mygroup0) print "" print "Test the __iter__ methods" print " first float of mypoint3 is: " + str(mypoint3[0]) print " first point of myline3 is: " + str(myline3[0]) print " first line of mycurve1 is: " + str(mycurve1[0]) print " first point of mypolyform1 is: " + str(mypolyform1[0]) print " first object of mygroup1 is: " + str(mygroup1[0]) print "" print "Complex Calc Test, mypoint1 multiplied by the first point in myline1 divided by mypoint9 to the power of mypoint1" print " " + str(mypoint1 * myline1 / mypoint9 ** mypoint1) print "" print "Distance Calculations" print " distance from mypoint0 to myline3: " + str(mypoint0.distance(myline3)) print " distance from mypoint0 to start of myline3 (should be same as before): " + str(mypoint0.distance(myline3[0])) print " distance from mypoint1 to the middle of myline4: " + str(mypoint1.distance(myline4.__Point__("center"))) print " distance from mypoint9 to the closest point of myline1: " + str(mypoint9.distance(myline1.__Point__("closest"))) print "" print "Flattening of hierarchies" print " flattened the tuple of different sequential data types looks like: " + str(mypoint3.flatten(flatteningtuple)) print " flattened mygroup0 looks like: " + str(mygroup0.flatten(mygroup0)) print " flattened mygroup1 looks like: " + str(mygroup1.flatten(mygroup1)) print " flattening types only with flattenTypes looks like: " + str(mypoint0.flattenTypes(flatteningtuple)) print " flattening types only with flattenTypes with str formatting looks like: " + str(newList) print "Testing the InputHandler" print " point from 3 int list: " + printPointInType(mylist) print " point from 2 int list: " + printPointInType(mylist1) print " point from 3 int tuple: " + printPointInType(mytuple) print " point from 3 int dict in x,y,z format: " + printPointInType(mydict1) print " point from 2 int dict in a,b,c, format: " + printPointInType(mydict2) print " point from 2 int dict in a,b format:" + printPointInType(mydict3) print " point from 1 int dict in 'a' format: " + printPointInType(mydict4) print " point from 4 int dict in n,r,q,a format" + printPointInType(mydict5) print " point from 1 int: " + printPointInType(myint) print " point from 1 float: " + printPointInType(myfloat) print " point from 1 long: " + printPointInType(mylong)