├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── Makefile ├── README.md ├── geometry ├── __init__.py ├── boxes.py ├── core.py ├── intervals.py ├── line.py ├── matrix.py ├── mesh.py ├── plane.py ├── point.py ├── point2d.py ├── point3d.py ├── points.py ├── rhino.py ├── vector.py ├── vector2d.py └── vector3d.py ├── requirements-test.txt ├── setup.py └── tests ├── __init__.py ├── test_imports.py └── test_vector.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.swo 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # .travis.yml for python-geometry 2 | 3 | language: python 4 | 5 | python: 6 | - "2.6" 7 | - "2.7" 8 | - "3.2" 9 | - "3.3" 10 | 11 | install: 12 | - "pip install ." 13 | - "pip install -r requirements-test.txt" 14 | 15 | script: 16 | make test_travis 17 | 18 | after_success: 19 | coveralls 20 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Ben Golder 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | The views and conclusions contained in the software and documentation are those 25 | of the authors and should not be interpreted as representing official policies, 26 | either expressed or implied, of the FreeBSD Project. 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test test_travis clean 2 | 3 | test_travis: 4 | nosetests \ 5 | --nocapture \ 6 | --with-coverage \ 7 | --cover-package=geometry 8 | 9 | clean: 10 | rm .coverage 11 | rm geometry/*.pyc 12 | rm tests/*.pyc 13 | 14 | test: 15 | make test_travis 16 | make clean 17 | 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-geometry 2 | 3 | [![Build Status](https://travis-ci.org/bengolder/python-geometry.png?branch=master)](https://travis-ci.org/bengolder/python-geometry) 4 | [![Coverage Status](https://coveralls.io/repos/bengolder/python-geometry/badge.png?branch=master)](https://coveralls.io/r/bengolder/python-geometry?branch=master) 5 | 6 | --------- 7 | 8 | 9 | A 2d and 3d geometry library in Python. 10 | 11 | Next steps for this library: 12 | 13 | * complete unit test coverage 14 | * adding a polyline class 15 | * adding a polygon class 16 | * adding geometry algorithms, including 17 | 18 | * nearest neighbors 19 | * segment intersections 20 | * triangulation 21 | 22 | * adding a quaternion class 23 | * adding visual unit tests 24 | * adding mesh classes 25 | * adding bezier and nurbs curves 26 | 27 | -------------------------------------------------------------------------------- /geometry/__init__.py: -------------------------------------------------------------------------------- 1 | from .intervals import ( 2 | Interval, 3 | Scale, 4 | ) 5 | from .boxes import ( 6 | Box2d, 7 | Box3d, 8 | ) 9 | from .vector2d import ( 10 | Vector2d, 11 | PageX, 12 | PageY, 13 | ) 14 | from .vector3d import ( 15 | Vector3d, 16 | WorldX, 17 | WorldY, 18 | WorldZ 19 | ) 20 | from .point2d import ( 21 | Point2d, 22 | ) 23 | from .point3d import ( 24 | Point3d, 25 | ) 26 | from .points import ( 27 | PointSet, 28 | ) 29 | from .matrix import ( 30 | Matrix, 31 | MatrixError, 32 | ) 33 | from .plane import ( 34 | Plane3d, 35 | ) 36 | from .line import ( 37 | Line3d, 38 | LineSegment2d, 39 | ) 40 | 41 | __all__ = [ 42 | 'Interval', 43 | 'Scale', 44 | 'Box2d', 45 | 'Box3d', 46 | 'Vector2d', 47 | 'Vector3d', 48 | 'Point2d', 49 | 'Point3d', 50 | 'PointSet', 51 | 'PageX', 52 | 'PageY', 53 | 'WorldX', 54 | 'WorldY', 55 | 'WorldZ' 56 | 'Matrix', 57 | 'Line3d', 58 | 'LineSegment2d', 59 | 'Plane3d', 60 | ] 61 | 62 | 63 | -------------------------------------------------------------------------------- /geometry/boxes.py: -------------------------------------------------------------------------------- 1 | from .point2d import Point2d 2 | from .point3d import Point3d 3 | from .intervals import Interval, Scale 4 | 5 | """ 6 | increase to encompass a point 7 | increase/decrease by a margin 8 | addition between two boxes 9 | boolean operations? 10 | two corner points 11 | initialized with: 12 | intervals 13 | interval tuples 14 | width/height/depth 15 | points 16 | arbitrary geometry 17 | """ 18 | 19 | class Box2d(object): 20 | def __init__(self, width=200, height=100, *args, **kwargs): 21 | self.x = Interval(0, width) 22 | self.y = Interval(0, height) 23 | 24 | def center(self): 25 | """Get or set the center of the box""" 26 | return Point2d( 27 | self.x(0.5), 28 | self.y(0.5), 29 | ) 30 | 31 | class Box3d(object): 32 | def __init__(self, width=200, length=200, height=200, 33 | *args, **kwargs): 34 | self.x = Interval(0, width) 35 | self.y = Interval(0, length) 36 | self.z = Interval(0, height) 37 | 38 | def center(self): 39 | """Get or set the center of the box""" 40 | return Point3d( 41 | self.x(0.5), 42 | self.y(0.5), 43 | self.z(0.5), 44 | ) 45 | 46 | 47 | -------------------------------------------------------------------------------- /geometry/core.py: -------------------------------------------------------------------------------- 1 | """ 2 | """ 3 | 4 | import numbers 5 | import sys 6 | 7 | PY3 = sys.version_info[0] == 3 8 | 9 | if PY3: 10 | string_type = str 11 | else: 12 | string_type = basestring 13 | 14 | # For float comparison: 15 | def isRoughlyZero(number): 16 | return round(number, 7) == 0 17 | # though there are better ways to do this. 18 | # It would be nice if this could handle all sorts of numbers 19 | # see: 20 | # http://floating-point-gui.de/errors/comparison/ 21 | # http://stackoverflow.com/questions/9528421/value-for-epsilon-in-python 22 | # http://stackoverflow.com/questions/4028889/floating-point-equality-in-python 23 | # http://stackoverflow.com/questions/3049101/floating-point-equality-in-python-and-in-general 24 | 25 | 26 | -------------------------------------------------------------------------------- /geometry/intervals.py: -------------------------------------------------------------------------------- 1 | import numbers 2 | import math 3 | 4 | class Interval(object): 5 | """ 6 | Intervals are one-dimensional spaces on a number line 7 | 8 | They can be instantiated with: 9 | no arguments - which defaults to an interval of 0.0 to 1.0 10 | a single number argument - which becomes an interval from 0.0 to 11 | that number 12 | a single iterable - the interval derives the max and min of the 13 | iterable 14 | two arguments, which become the start and end of the interval 15 | more arguments - this derives the max and min of all the 16 | arguments. 17 | divide using an integer (into equal pieces) 18 | divide using a float (into pieces with a remainder) 19 | convert a set of numbers, instead of just one 20 | 21 | The interval is half-open by default. In other words, the start value is 22 | included in the interval, but the end value is not . [start, end) 23 | """ 24 | def __init__(self, *args): 25 | if len(args) == 0: 26 | start = 0.0 27 | end = 1.0 28 | elif len(args) == 1: 29 | if hasattr(args[0], '__iter__'): 30 | start = min(args[0]) 31 | end = max(args[0]) 32 | elif isinstance( args[0], numbers.Number ): 33 | # assume we have a width 34 | start = 0.0 35 | end = start + args[0] 36 | else: 37 | start = 0.0 38 | end = 1.0 39 | 40 | elif len(args) == 2: 41 | # assume we have a start and end 42 | start = args[0] 43 | end = args[1] 44 | else: # assume we have an iterable of numbers 45 | # use the bounds of the numbers 46 | start = min(args) 47 | end = max(args) 48 | self.start = start 49 | self.end = end 50 | self.length = end - start 51 | self.start_open = True 52 | self.end_open = False 53 | 54 | def fraction(self, *args): 55 | """given a value between the interval start and end, 56 | this returns a fraction between 0 and 1of the distance along the interval, 57 | """ 58 | if len( args ) > 1: 59 | return [(arg - self.start) / self.length for arg in args] 60 | else: 61 | return ( args[0] - self.start ) / self.length 62 | 63 | def contains( self, value): 64 | """tests if a value falls within the interval's bounds""" 65 | return self.start <= value < self.end 66 | 67 | def __call__( self, *args): 68 | """given a fractional value along the interval (between 69 | 0.0 and 1.0), this returns the actual value""" 70 | if len( args ) > 1: 71 | return [((self.length * arg) + self.start) for arg in args] 72 | else: 73 | return (self.length * args[0]) + self.start 74 | 75 | def divide(self, number ): 76 | """ 77 | given an integer n, divide into n equal parts 78 | given a float n, divide into m parts of size n, and one remainder 79 | returns an iterator that yeilds new Interval objects 80 | """ 81 | if isinstance( number, int ): 82 | # divide into n parts 83 | step = self.length / number 84 | start = self.start 85 | end = self.start 86 | for i in range(number): 87 | steps = i + 1 88 | if steps < number: 89 | end = steps * step 90 | else: 91 | end = self.end 92 | interval = Interval( start, end ) 93 | start = end 94 | yield interval 95 | else: 96 | # use this number to divide 97 | steps = int( math.ceil( abs(self.length / number) ) ) 98 | start = self.start 99 | end = self.start 100 | for i in range(steps): 101 | this_step = i + 1 102 | if this_step < steps: 103 | end = this_step * number 104 | else: 105 | end = self.end 106 | interval = Interval( start, end ) 107 | start = end 108 | yield interval 109 | 110 | def include(self, number): 111 | """Returns a new interval containing the number 112 | If this interval contains the number already, 113 | it will return a copy of this interval. 114 | """ 115 | if self.contains(number): 116 | return Interval(self.start, self.end) 117 | else: 118 | return Interval(self.start, self.end, number) 119 | 120 | def scale(self, number): 121 | """return a new interval with start * number and end * number""" 122 | return Interval( self.start * number, self.end * number ) 123 | 124 | def shift(self, number): 125 | """return a new interval by adding number to start and end 126 | """ 127 | return Interval( self.start + number, self.end + number ) 128 | 129 | def __repr__(self): 130 | start = "[" if self.start_open else "(" 131 | end = "]" if self.end_open else ")" 132 | return "Interval%s%s, %s%s" % (start, self.start, self.end, end) 133 | 134 | 135 | 136 | class Scale(object): 137 | def __init__(self, domain=(0.0, 1.0), range=(0.0, 1.0) ): 138 | self.domain = Interval(*domain) 139 | self.range = Interval(*range) 140 | 141 | def __call__(self, value ): 142 | """converts a value in the domain to a proportional 143 | value in the range""" 144 | return self.range( self.domain.fraction( value ) ) 145 | 146 | def reverse(self, value ): 147 | """converts a value in the range to a proportional 148 | value in the domain""" 149 | return self.domain( self.range.fraction( value ) ) 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /geometry/line.py: -------------------------------------------------------------------------------- 1 | from .vector3d import Vector3d 2 | from .point3d import Point3d 3 | 4 | class Line3d(object): 5 | """A 3d line object 6 | 7 | This class is used to represent an infinitely extensive line. 8 | Lines can be represented with a point and a vector parallel to the 9 | direction of the line. 10 | 11 | Lines can also be initialized with two points. 12 | 13 | If initialized with just a vector, the line will pass through the origing 14 | (0,0,0) and will be parallel with the given vector. 15 | 16 | """ 17 | def __init__(self, *args): 18 | if isinstance(args[0], Vector3d): 19 | # assume we have a parallel vector then a point 20 | self.vector = args[0] 21 | self.point = args[1] 22 | else: 23 | # assume we have two points 24 | self.vector = args[1] - args[0] 25 | self.point = args[0] 26 | 27 | def __repr__(self): 28 | return 'Line3d( %s, %s )' % (self.vector, self.point) 29 | 30 | 31 | 32 | 33 | class LineSegment2d(object): 34 | def __init__(self, start_point, end_point): 35 | self.coords = (start_point, end_point) 36 | def __repr__(self): 37 | return 'LineSegment2d( %s, %s )' % self.coords 38 | 39 | -------------------------------------------------------------------------------- /geometry/matrix.py: -------------------------------------------------------------------------------- 1 | """This module contains classes for matrices in pure python (no numpy). 2 | Matrices are represented as lists of lists 3 | 4 | Relevant reading: 5 | * matrix api of numpy - http://stackoverflow.com/questions/3127404/how-to-represent-matrices-in-python 6 | * matrices can just be vectors of vectors in python - http://www.math.okstate.edu/~ullrich/PyPlug/ 7 | * array module in python - http://docs.python.org/2/library/array.html 8 | """ 9 | 10 | import math 11 | import numbers 12 | import itertools 13 | 14 | from .core import isRoughlyZero 15 | 16 | class MatrixError(Exception): 17 | def __init__(self, msg): 18 | self.msg = msg 19 | def __str__(self): 20 | return self.msg 21 | 22 | class Matrix(object): 23 | """A class for all kinds of matrix objects. 24 | Matrices are immutable. 25 | """ 26 | def __init__(self, table=None, rows=3, columns=3): 27 | """Nested iterables of values can be passed, or you can designate a size 28 | of the matrix. The default is 3x3 identity matrix.""" 29 | if table: 30 | # just use the given table, iterate through it and convert it to 31 | # tuples 32 | self.table = tuple( [ tuple( r ) for r in table ] ) 33 | if not self.is_rectangular(): 34 | msg = """The iterable used to produce this Matrix had unclear 35 | dimensions. Either the rows or the columns (or both) are not even 36 | lenghts. All the rows must be the same length as each other and 37 | all the columns must be the same length as each other.""" 38 | raise MatrixError( msg ) 39 | else: 40 | # build an identity matrix 41 | self.table = [] 42 | for i in range(rows): 43 | row = [] 44 | for j in range(columns): 45 | if i == j: 46 | val = 1.0 47 | else: 48 | val = 0.0 49 | row.append( val ) 50 | self.table.append( tuple(row) ) 51 | self.table = tuple(self.table) 52 | 53 | def __iter__(self): 54 | """iterate through the table of the matrix""" 55 | return self.table.__iter__() 56 | 57 | def full_iter(self): 58 | """iterate through without regard for columns or rows. This basically 59 | treats the Matrix as one big row.""" 60 | for row in self: 61 | for item in row: 62 | yield item 63 | 64 | def __getitem__(self, key): 65 | """gets the row from the table of the Matrix""" 66 | return self.table[key] 67 | 68 | def __len__(self): 69 | """gets the number of rows in the matrix""" 70 | return len(self.table) 71 | 72 | def is_rectangular(self): 73 | """This method is used to ensure that all rows are the smae length and 74 | all columns are the same length.""" 75 | rows = self.rows() 76 | for col in self.iter_cols(): 77 | if len(col) != rows: 78 | return False 79 | cols = self.cols() 80 | for row in self: 81 | if len(row) != cols: 82 | return False 83 | return True 84 | 85 | def is_square(self): 86 | """check if this matrix has the same width and length""" 87 | return len(self) == self.cols() 88 | 89 | def cols(self): 90 | """return the number of columns in this matrix""" 91 | return len( self.table[0] ) 92 | 93 | def rows(self): 94 | """return the number of rows in this matrix""" 95 | return len( self.table ) 96 | 97 | def transpose(self): 98 | """Return a new Matrix with columns and rows transposed""" 99 | return Matrix( tuple(zip( *self.table ) ) ) 100 | 101 | def iter_cols(self): 102 | """iterate through a transposed version of this Matrix""" 103 | return itertools.izip( *self.table ) 104 | 105 | def row_map(self, function, *args): 106 | """map a function to each row of this matrix, and return the resulting 107 | matrix. This can input the corresponding rows from any number of 108 | matrices. 109 | """ 110 | return Matrix( map(function, self, *args) ) 111 | 112 | def cell_map(self, function, *args): 113 | """map a function to each cell of this matrix, and return the resulting 114 | matrix. additional iterables can be passed, but the function take one 115 | argument for each iterable. this can be used to iterate through two 116 | Matrices simultaneously, creating a new Matrix with the same dimension 117 | and new values calculated from each pair of cells from the two 118 | matrices. 119 | """ 120 | row_map = lambda *r: map(function, *r) 121 | return Matrix( itertools.imap( row_map, self, *args ) ) 122 | 123 | def __mul__( self, other ): 124 | """get matrix product, be sure that sizes fit. 125 | If multiplied with another matrix, this produces a matrix product. 126 | if multiplied by anything else, the other is assumed to be a number. 127 | """ 128 | if isinstance( other, Matrix ): 129 | # matrix product 130 | cols = self.cols() 131 | rows = other.rows() 132 | # check if it will work 133 | if cols != rows: 134 | msg = """To find the product of two matrices, the number of 135 | columns in this matrix must match the number of rows in the 136 | other matrix. This matrix has %s columns and the other matrix 137 | has %s rows.""" % (cols, rows) 138 | raise MatrixError( msg ) 139 | # get the product 140 | new = [] 141 | # for each row in this 142 | for i in range(len(self)): 143 | newrow = [] 144 | # for each column in other 145 | for j in range(other.cols()): 146 | subvals = [] 147 | # for each value in this row (or in that column) 148 | for n in range(cols): 149 | # multiply this value by that value 150 | subval = self[i][n] * other[n][j] 151 | subvals.append(subval) 152 | # sum the new values 153 | value = sum(subvals) 154 | newrow.append(value) 155 | new.append( newrow ) 156 | # return a new Matrix object 157 | return Matrix( new ) 158 | else: 159 | # assume it's a number 160 | mult = lambda x: x * other 161 | return self.cell_map( mult ) 162 | 163 | def __add__( self, other ): 164 | """add this matrix to another, or add a number 165 | """ 166 | if isinstance(other, Matrix): 167 | add = lambda x, y: x + y 168 | return self.cell_map(add, other) 169 | else: 170 | # assume it's a number 171 | add = lambda x: x + other 172 | return self.cell_map( add ) 173 | 174 | def __sub__(self, other): 175 | """subtract another Matrix from this one, or subtract a number. 176 | """ 177 | if isinstance(other, Matrix): 178 | sub = lambda x, y: x - y 179 | return self.cell_map(sub, other) 180 | else: 181 | # assume it's a number 182 | sub = lambda x: x - other 183 | return self.cell_map( sub ) 184 | 185 | def __repr__(self): 186 | row_repr = lambda r: ', '.join([str(c) for c in r]) 187 | return '' % '\n'.join( 188 | ['(%s)' % row_repr(r) for r in self] 189 | ) 190 | 191 | 192 | -------------------------------------------------------------------------------- /geometry/mesh.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numbers 3 | 4 | from .core import isRoughlyZero 5 | from .vector3d import Vector3d 6 | from .point3d import Point3d 7 | from .points import PointSet 8 | 9 | class Edge(object): 10 | """This is a class roughly equivalent to a line segment. It is basically a 11 | connection between two points. It should behave like a tuple of two 12 | points. 13 | """ 14 | def __init__(self, start, end): 15 | """The initialization methods assumes that it will receive two points 16 | """ 17 | self.points = (start, end) 18 | 19 | def __getitem__(self, key): 20 | return self.points.__getitem__(key) 21 | 22 | def __len__(self): 23 | return self[0].distanceTo(self[1]) 24 | 25 | class PlanarFace(object): 26 | """This is meant to hold triangles and polygon facets for mesh-lik 27 | structures. 28 | """ 29 | pass 30 | 31 | class Edge(object): 32 | """An object that links two points""" 33 | def __init__(start, end): 34 | self.points = (start, end) 35 | self.start = self.points[0] 36 | self.end = self.points[1] 37 | 38 | @property 39 | def length(self): 40 | """Get the length of this Edge (the distance between two points) 41 | """ 42 | return self.end.distanceTo(self.start) 43 | 44 | class Polyline(PointSet): 45 | """An object that contains a point set and links between the points.""" 46 | pass 47 | 48 | 49 | class Polygon(PointSet): 50 | """An object that basically contains a point set and links between points 51 | that result in a closed shape, but has methods for using it 52 | in polygon-related activities. 53 | """ 54 | pass 55 | 56 | class MeshVertex(Point3D): 57 | """A point3d for use within a mesh that has all the functionality of a 58 | point, but also contains adjacency information. 59 | """ 60 | def __init__(self): 61 | self.adjacentEdges = [] 62 | 63 | class MeshFace(object): 64 | """A mesh face has a set of edges that are all connected into a closed 65 | polygon. 66 | """ 67 | def __init__(self): 68 | self.edges = [] 69 | 70 | class MeshEdge(Edge): 71 | """An edge with adjacency information. 72 | """ 73 | def __init__(self): 74 | self.adjacentFaces = [] 75 | self.adjacentEdges = [] 76 | 77 | 78 | class WingedEdgeMesh(object): 79 | """A mesh that stores adjacency information between faces, edges and 80 | vertices. 81 | """ 82 | def __init__(self): 83 | self.faces = [] 84 | self.edges = [] 85 | self.vertices = [] 86 | pass 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /geometry/plane.py: -------------------------------------------------------------------------------- 1 | """This module implements a plane object and related functions 2 | """ 3 | from .line import Line3d 4 | from .vector3d import Vector3d 5 | from .point3d import Point3d 6 | from .core import isRoughlyZero 7 | 8 | class Plane3d(object): 9 | """A 3d plane object 10 | This plane class has an infinite size. 11 | 12 | A plane can be initialized with point on the plane and a vector normal to 13 | the plane, or with three points 14 | 15 | Planes are infinite and currently have no origin. 16 | """ 17 | def __init__(self, *args, **kwargs): 18 | 19 | if len(args) == 2: 20 | # assume we have a point and vector 21 | self.point = args[0] 22 | self.normal = args[1] 23 | 24 | elif len(args) == 3: 25 | # assume we have 3 points 26 | v1 = args[2] - args[1] 27 | v2 = args[1] - args[0] 28 | normal = v1.cross( v2 ) 29 | self.point = args[0] 30 | self.normal = normal 31 | 32 | self.d = -(self.normal.dot(self.point)) 33 | 34 | 35 | def angleTo(self, other): 36 | """measures the angle between this plane and another plane. This uses 37 | that angle between two normal vectors. 38 | Units expressed in radians. 39 | """ 40 | if isinstance(other, Plane3d): 41 | otherVect = other.normal 42 | elif isinstance(other, Vector3d): 43 | otherVect = other 44 | return self.normal.angleTo(otherVect) 45 | 46 | 47 | def intersect(self, other): 48 | """Finds the intersection of this plane with another object. 49 | """ 50 | if isinstance(other, Plane3d): 51 | # return the line intersection of two planes 52 | # first, get the cross product of the two plane normals 53 | # which is a vector parallel to L 54 | vector = self.normal.cross(other.normal) 55 | absCoords = [abs(c) for c in vector] 56 | if isRoughlyZero(sum(absCoords)): 57 | # the planes are parallel and do not intersect 58 | return None 59 | else: 60 | # the planes intersect in a line 61 | # first find the largest coordinate in the vector 62 | cNum = None 63 | cMax = 0 64 | for i, c in enumerate(absCoords): 65 | if c > cMax: 66 | cMax = c 67 | cNum = i 68 | dims = ["x","y","z"] 69 | biggestDim = dims.pop(cNum) 70 | p = {} 71 | p[biggestDim] = 0 72 | if biggestDim == "x": 73 | p["y"] = (other.d * self.normal.z - self.d * other.normal.z)/vector.x 74 | p["z"] = (self.d * other.normal.y - other.d * self.normal.y)/vector.x 75 | elif biggestDim == "y": 76 | p["x"] = (self.d * other.normal.z - other.d * self.normal.z)/vector.y 77 | p["z"] = (other.d * self.normal.x - self.d * other.normal.x)/vector.y 78 | else: # biggest dim is z 79 | p["x"] = (other.d * self.normal.y - self.d * other.normal.y)/vector.z 80 | p["y"] = (self.d * other.normal.x - other.d * self.normal.x)/vector.z 81 | point = Point3d(**p) 82 | return Line3d(vector, point) 83 | 84 | elif isinstance(other, Line3d): 85 | # return the point intersection of a line and a plane 86 | pass 87 | pass 88 | 89 | def __repr__(self): 90 | return 'Plane3d( %s, %s )' % (self.point, self.normal) 91 | 92 | -------------------------------------------------------------------------------- /geometry/point.py: -------------------------------------------------------------------------------- 1 | 2 | class PointBase(object): 3 | """Should not be instantiated directly. 4 | """ 5 | def vectorTo(self, other): 6 | """Find the vector to another point. 7 | """ 8 | return other - self 9 | 10 | -------------------------------------------------------------------------------- /geometry/point2d.py: -------------------------------------------------------------------------------- 1 | from .vector2d import Vector2d 2 | from .point import PointBase 3 | 4 | class Point2d(Vector2d, PointBase): 5 | def __init__(self, *args, **kwargs): 6 | Vector2d.__init__(self, *args, **kwargs) 7 | PointBase.__init__(self) 8 | 9 | def __repr__(self): 10 | return 'Point2d(%s, %s)' % self.coords 11 | 12 | def distanceTo(self, other): 13 | """Find the distance between this point and another. 14 | """ 15 | return (other - self).length 16 | 17 | PageOrigin = Point2d(0.0,0.0) 18 | -------------------------------------------------------------------------------- /geometry/point3d.py: -------------------------------------------------------------------------------- 1 | from .vector3d import Vector3d 2 | from .point import PointBase 3 | 4 | class Point3d(Vector3d, PointBase): 5 | """Functionally similar to a Vector3d, but concpetually distinct, and 6 | includes several additional methods. 7 | """ 8 | def __init__(self, *args, **kwargs): 9 | Vector3d.__init__() 10 | PointBase.__init__() 11 | 12 | def __repr__(self): 13 | return 'Point3d(%s, %s, %s)' % self.coords 14 | 15 | def distanceTo(self, other): 16 | """Find the distance between this point and another. 17 | """ 18 | if isinstance(other, Plane3d): 19 | # get distance between point and plane 20 | pass 21 | elif isinstance(other, Line3d): 22 | # get distance between point and line 23 | pass 24 | else: 25 | return (other - self).length 26 | 27 | 28 | -------------------------------------------------------------------------------- /geometry/points.py: -------------------------------------------------------------------------------- 1 | from .point3d import Point3d 2 | 3 | class PointSet(object): 4 | """This class is meant to hold a set of *unique* points. 5 | 6 | Basically this provides hooks to using a python 'set' containing tuples of 7 | the point coordinates. 8 | """ 9 | def __init__(self, points=None): 10 | # can be initialized with an empty set. 11 | # this set needs to contain tuples of point coords 12 | # and then extend and manage the use of that set 13 | # intialize the dictionary and list first 14 | # becasue any input points will be placed there to start 15 | self.pointList = [] 16 | self.pointDict = {} 17 | if points: 18 | # parse the points to create the pointList and pointDict 19 | # we want to be able to accept points as tuples, as lists, as 20 | # Point3d objects, and as Vector3d objects. I guess if I add them 21 | # as iterables, that would be the simplest. 22 | self.points = points 23 | 24 | @property 25 | def points(self): 26 | # go through the list and get each tuple as Point3d object 27 | return self.pointList 28 | 29 | @points.setter 30 | def points(self, values): 31 | """This method parses incoming values being used to define points and 32 | filters them if necessary. 33 | 34 | This creates an internal PointList and PointDict, which together mimick 35 | the OrderedDict in Python 3.0. I'm combining these two pieces for backwards 36 | compatibility. 37 | """ 38 | for i, val in enumerate(values): 39 | # Vector3ds will need to be unwrapped 40 | if isinstance(val, Vector3d): 41 | point = Point3d(*val) 42 | else: 43 | # just assume it is some sort of iterable 44 | point = Point3d(*(val[v] for v in range(3))) 45 | # here will build the dictionary, using indices as the only 46 | # value any given tuple refers to 47 | self.pointDict[point] = i 48 | # and here we also build up a list, for lookups by index 49 | self.pointList.append(point) 50 | 51 | def __getitem__(self, key): 52 | """This builds a dictionary / list-like api on the PointSet. 53 | The `key` might be an integer or a coordinate tuple, or a point. 54 | 55 | There is a design question here: should it initialize the object as a 56 | Point3d? 57 | 58 | An if Point3ds are already hashable, why am I using simple tuples when 59 | I could use Point3ds? 60 | """ 61 | # if it's a tuple or point3d and return the index 62 | if isinstance(key, tuple) or isinstance(key, Vector3d): 63 | return self.pointDict[key] 64 | else: 65 | # assume it is an index or slice 66 | return self.pointList[key] 67 | 68 | def __iter__(self): 69 | """Uses the internal list for iteration""" 70 | return self.pointList.__iter__() 71 | 72 | def __len__(self): 73 | """returns the number of Points it has.""" 74 | return self.pointList.__len__() 75 | 76 | def __contains__(self, other): 77 | """checks to see if this set has an item that matches other 78 | other in self 79 | """ 80 | return self.pointDict.__contains__(other) 81 | 82 | def issubset(self, other): 83 | """Used to see if all the items of an iterable are contained in this 84 | pointSet. 85 | self <= other 86 | """ 87 | for n in other: 88 | if n not in self: 89 | return False 90 | return True 91 | 92 | def __le__(self, other): 93 | return self.issubset(other) 94 | 95 | def issuperset(self, other): 96 | """Used to see of all of the items in this object are contained in an 97 | other object. 98 | self >= other 99 | """ 100 | for n in self: 101 | if n not in other: 102 | return False 103 | return True 104 | def __ge__(self, other): 105 | return self.issuperset(other) 106 | 107 | def union(self, other): 108 | """returns a new PointSet with the points from self and the points from 109 | other. 110 | self | other 111 | """ 112 | newList = [] 113 | for p in self: 114 | newList.append(p) 115 | for p in other: 116 | newList.append(p) 117 | return PointSet(newList) 118 | def __or__(self, other): 119 | return self.union(other) 120 | 121 | def intersection(self, other): 122 | """returns the set intersection between self and other 123 | self & other 124 | """ 125 | newList = [] 126 | for p in other: 127 | if p in self: 128 | newList.append(p) 129 | return PointSet(newList) 130 | def __and__(self, other): 131 | return self.intersection(other) 132 | 133 | def difference(self, other): 134 | """ 135 | self - other 136 | """ 137 | newList = [] 138 | for p in self: 139 | if p not in other: 140 | newList.append(p) 141 | return PointSet(newList) 142 | def __sub__(self, other): 143 | return self.difference(other) 144 | 145 | def symmetric_difference(self, other): 146 | """ Returns all the points in self and other that are not in the 147 | intersection of self and other. 148 | self ^ other 149 | """ 150 | newList = [] 151 | for p in self: 152 | if p not in other: 153 | newList.append(p) 154 | for p in other: 155 | if p not in self: 156 | newList.append(p) 157 | return PointSet(newList) 158 | def __xor__(self, other): 159 | return self.symmetric_difference(other) 160 | 161 | def copy(self): 162 | """ returns a shallow copy of this pointset 163 | """ 164 | newList = [] 165 | for p in self: 166 | newList.append(p) 167 | return PointSet(newList) 168 | 169 | def extend(self, other): 170 | """Adds an iterable of new points to the set. 171 | """ 172 | length = len(self) 173 | for i, p in enumerate(other): 174 | self.pointList.append(p) 175 | self.pointDict[p] = i + length 176 | def __ior__(self, other): 177 | return self.extend(other) 178 | 179 | def append(self, other): 180 | """Adds a new point to the set. 181 | """ 182 | length = len(self) 183 | self.pointList.append(other) 184 | self.pointDict[other] = length 185 | 186 | -------------------------------------------------------------------------------- /geometry/rhino.py: -------------------------------------------------------------------------------- 1 | """This module handles object conversions to and from Rhino""" 2 | 3 | import Rhino.Geometry as rg 4 | 5 | from vector import Vector3d, Point3d, PointSet 6 | from plane import Plane3d 7 | from line import Line3d 8 | 9 | def importFromRhino(objs): 10 | """This function detects the type of an incoming Rhino 11 | object and converts it into the appropriate type 12 | 13 | the only argument can be either a single object or a 14 | list or tuple of objects 15 | """ 16 | if type(objs) in (list, tuple): 17 | return [convertFromRhino(obj) for obj in objs] 18 | else: 19 | return convertFromRhino(objs) 20 | 21 | def convertFromRhino(obj): 22 | return importAndConvert[type(obj)](obj) 23 | 24 | def exportToRhino(objs): 25 | """This function detects the type of outgoing objects and 26 | converts them to the appropriate type of Rhinoo object 27 | """ 28 | if type(objs) in (list, tuple): 29 | return [convertToRhino(obj) for obj in objs] 30 | else: 31 | return convertToRhino(objs) 32 | 33 | def convertToRhino(obj): 34 | return convertAndExport[type(obj)](obj) 35 | 36 | 37 | importAndConvert = { 38 | rg.Vector3d: lambda g: Vector3d(g.X, g.Y, g.Z), 39 | rg.Point3d: lambda g: Point3d(g.X, g.Y, g.Z), 40 | rg.Point: lambda g: importFromRhino(g.Location), 41 | rg.Plane: lambda g: Plane3d( 42 | importFromRhino(g.Origin), 43 | importFromRhino(g.Normal)), 44 | rg.Line: lambda g: Line3d( 45 | importFromRhino(g.Direction), 46 | importFromRhino(g.From)), 47 | } 48 | 49 | convertAndExport = { 50 | Vector3d: lambda g: rg.Vector3d(*g.asList()), 51 | Point3d: lambda g: rg.Point3d(*g.asList()), 52 | Plane3d: lambda g: rg.Plane( 53 | convertToRhino(g.point), 54 | convertToRhino(g.normal)), 55 | Line3d: lambda g: rg.Line( 56 | convertToRhino(g.point), 57 | convertToRhino(g.vector)), 58 | } 59 | 60 | -------------------------------------------------------------------------------- /geometry/vector.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numbers 3 | 4 | from .core import isRoughlyZero 5 | 6 | class VectorBase(object): 7 | """Should not be instantiated directly 8 | """ 9 | @property 10 | def length(self): 11 | """get the vector length / amplitude 12 | """ 13 | # only calculate the length if asked to. 14 | return math.sqrt(sum(n**2 for n in self)) 15 | 16 | def normalized(self): 17 | """just returns the normalized version of self without editing self in 18 | place. 19 | """ 20 | return self * (1 / self.length) 21 | 22 | def toLength(self, number): 23 | """Get a parallel vector with the input amplitude.""" 24 | # depends on normalized() and __mul__ 25 | # create a vector as long as the number 26 | return self.normalized() * number 27 | 28 | def __iter__(self): 29 | """For iterating, the vectors coordinates are represented as a tuple.""" 30 | return self.coords.__iter__() 31 | 32 | def dot(self, other): 33 | """Gets the dot product of this vector and another. 34 | """ 35 | return sum((p[0] * p[1]) for p in zip(self, other)) 36 | 37 | def __add__(self, other): 38 | """we want to add single numbers as a way of changing the length of the 39 | vector, while it would be nice to be able to do vector addition with 40 | other vectors. 41 | """ 42 | if isinstance(other, numbers.Number): 43 | # then add to the length of the vector 44 | # multiply the number by the normalized self, and then 45 | # add the multiplied vector to self 46 | return self.normalized() * other + self 47 | elif isinstance(other, self.__class__): 48 | # add all the coordinates together 49 | # there are probably more efficient ways to do this 50 | return self.__class__(*(sum(p) for p in zip(self, other))) 51 | else: 52 | raise TypeError( 53 | "unsupported operand (+/-) for types %s and %s" % ( 54 | self.__class__, type(other))) 55 | 56 | def __sub__(self, other): 57 | """Subtract a vector or number 58 | """ 59 | return self.__add__(other * -1) 60 | 61 | def __mul__(self, other): 62 | """if with a number, then scalar multiplication of the vector, 63 | if with a Vector, then dot product, I guess for now, because 64 | the asterisk looks more like a dot than an X. 65 | >>> v2 = Vector3d(-4.0, 1.2, 3.5) 66 | >>> v1 = Vector3d(2.0, 1.1, 0.0) 67 | >>> v2 * 1.25 68 | Vector3d(-5.0, 1.5, 4.375) 69 | >>> v2 * v1 #dot product 70 | -6.6799999999999997 71 | """ 72 | if isinstance(other, numbers.Number): 73 | # scalar multiplication for numbers 74 | return self.__class__( *((n * other) for n in self)) 75 | 76 | elif isinstance(other, self.__class__): 77 | # dot product for other vectors 78 | return self.dot(other) 79 | else: 80 | raise TypeError( 81 | "unsupported operand (multiply/divide) for types %s and %s" % ( 82 | self.__class__, type(other))) 83 | 84 | def __hash__(self): 85 | return self.coords.__hash__() 86 | 87 | def __eq__(self, other): 88 | """I am allowing these to compared to tuples, and to say that yes, they 89 | are equal. the idea here is that a Vector3d _is_ a tuple of floats, but 90 | with some extra methods. 91 | """ 92 | return self.coords == other.coords 93 | 94 | def angleTo(self, other): 95 | """computes the angle between two vectors 96 | cos theta = (n * m) / (n.length * m.length) 97 | """ 98 | cosTheta = (self * other) / (self.length * other.length) 99 | return math.acos(cosTheta) 100 | 101 | 102 | -------------------------------------------------------------------------------- /geometry/vector2d.py: -------------------------------------------------------------------------------- 1 | from .vector import VectorBase 2 | 3 | class Vector2d(VectorBase): 4 | def __init__(self, x=0.0, y=0.0): 5 | VectorBase.__init__(self) 6 | self.coords = (x, y) 7 | 8 | @property 9 | def x(self): 10 | return self[0] 11 | @x.setter 12 | def x(self, value): 13 | self.coords = (value,) + self.coords[1:] 14 | 15 | @property 16 | def y(self): 17 | return self[1] 18 | @y.setter 19 | def y(self, value): 20 | self.coords = self.coords[:1] + (value,) + self.coords[2:] 21 | 22 | def asDict(self): 23 | """return dictionary representation of the vector""" 24 | return dict( zip( ('x','y'), self.coords ) ) 25 | 26 | def __getitem__(self, key): 27 | """Treats the vector as a tuple or dict for indexes and slicing. 28 | """ 29 | # dictionary 30 | if key in ('x','y'): 31 | return self.asDict()[key] 32 | # slicing and index calls 33 | else: 34 | return self.coords.__getitem__(key) 35 | 36 | def toX(self, number): 37 | """For getting a copy of the same vector but with a new x value""" 38 | return self.__class__(number, self[1]) 39 | 40 | def toY(self, number): 41 | """For getting a copy of the same vector but with a new y value""" 42 | return self.__class__(self[0], number) 43 | 44 | def __repr__(self): 45 | return 'Vector2d(%s, %s)' % self.coords 46 | 47 | PageX = Vector2d(1.0, 0.0) 48 | PageY = Vector2d(0.0, 1.0) 49 | -------------------------------------------------------------------------------- /geometry/vector3d.py: -------------------------------------------------------------------------------- 1 | """This module is for the Vector class""" 2 | from .vector2d import Vector2d 3 | 4 | class Vector3d(Vector2d): 5 | """A 3d vector object 6 | """ 7 | def __init__(self, x=0.0, y=0.0, z=0.0): 8 | Vector2d.__init__(self) 9 | self.coords = (x, y, z) 10 | 11 | @property 12 | def z(self): 13 | return self[2] 14 | @z.setter 15 | def z(self, value): 16 | self.coords = (self.x, self.y, value) 17 | 18 | def toX(self, number): 19 | """For getting a copy of the same vector but with a new x value""" 20 | return self.__class__(number, self[1], self[2]) 21 | 22 | def toY(self, number): 23 | """For getting a copy of the same vector but with a new y value""" 24 | return self.__class__(self[0], number, self[2]) 25 | 26 | def toZ(self, number): 27 | """For getting a copy of the same vector but with a new z value""" 28 | return self.__class__(self[0], self[1], number) 29 | 30 | def asDict(self): 31 | """return dictionary representation of the vector""" 32 | return dict( zip( list('xyz'), self.coords ) ) 33 | 34 | def __getitem__(self, key): 35 | """Treats the vector as a tuple or dict for indexes and slicing. 36 | """ 37 | if key in ('x','y','z'): 38 | return self.asDict()[key] 39 | else: 40 | return self.coords.__getitem__(key) 41 | 42 | def cross(self, other): 43 | """Gets the cross product between two vectors 44 | """ 45 | x = (self[1] * other[2]) - (self[2] * other[1]) 46 | y = (self[2] * other[0]) - (self[0] * other[2]) 47 | z = (self[0] * other[1]) - (self[1] * other[0]) 48 | return self.__class__(x, y, z) 49 | 50 | def __repr__(self): 51 | return 'Vector3d(%s, %s, %s)' % self.coords 52 | 53 | 54 | WorldX = Vector3d(1.0, 0.0, 0.0) 55 | WorldY = Vector3d(0.0, 1.0, 0.0) 56 | WorldZ = Vector3d(0.0, 0.0, 1.0) 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | nose 2 | coveralls 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name = "geometry", 5 | version = "0.0.1", 6 | packages = ['geometry'], 7 | 8 | # metadata for upload to PyPI 9 | author = "Ben Golder, Stefano Borini", 10 | author_email = "benjamin.j.golder@gmail.com, stefano.borini@ferrara.linux.it", 11 | description = "2D and 3D geometry library", 12 | license = "BSD 2", 13 | keywords = "geometry 3d 2d", 14 | url = "https://github.com/bengolder/python-geometry/", 15 | 16 | # could also include long_description, download_url, classifiers, etc. 17 | 18 | ) 19 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bengolder/python-geometry/4b1b44f6234fbc77a846997224fe732000aeae53/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_imports.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | class TestImport(unittest.TestCase): 4 | def setUp(self): 5 | pass 6 | 7 | def test_imports(self): 8 | from geometry import ( 9 | Interval, 10 | Scale, 11 | Box2d, 12 | Box3d, 13 | Vector2d, 14 | Vector3d, 15 | Point2d, 16 | Point3d, 17 | PointSet, 18 | PageX, 19 | PageY, 20 | WorldX, 21 | WorldY, 22 | WorldZ, 23 | Matrix, 24 | Line3d, 25 | LineSegment2d, 26 | Plane3d, 27 | ) 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/test_vector.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import random 3 | import math 4 | 5 | from geometry import Vector2d, Vector3d, Point2d, Point3d 6 | 7 | class CoordGenerator: 8 | def __init__(self, scale=100, dim=3, number_type=float): 9 | self.bounds = ( 10 | scale / -2, 11 | scale / 2 12 | ) 13 | self.dim = dim 14 | self.number_type = number_type 15 | 16 | def __call__(self): 17 | if self.number_type == int: 18 | coords = [random.randint(*self.bounds) for i in range(self.dim)] 19 | else: 20 | coords = [random.uniform(*self.bounds) for i in range(self.dim)] 21 | return coords 22 | 23 | def point(self): 24 | if self.dim == 3: 25 | return Point3d(*self()) 26 | else: 27 | return Point2d(*self()) 28 | 29 | 30 | class TestVectors(unittest.TestCase): 31 | 32 | def setUp(self): 33 | self.gen = CoordGenerator() 34 | self.coords = [self.gen() for i in range(10)] 35 | self.d2 = {'x':45, 'y':-453} 36 | self.d3 = {'x':543, 'y':-9872183, 'z':-32.543} 37 | self.vectors2d = [Vector2d(c[:2]) for c in self.coords] 38 | self.vectors3d = [Vector3d(c) for c in self.coords] 39 | self.v2 = Vector2d(**self.d2) 40 | self.v3 = Vector3d(**self.d3) 41 | 42 | def assertEqualDicts(self, a, b): 43 | for ka in a: 44 | self.assertTrue( ka in b) 45 | self.assertEqual( a[ka], b[ka] ) 46 | for kb in b: 47 | self.assertTrue( kb in a) 48 | self.assertEqual( a[kb], b[kb] ) 49 | 50 | def test_vector_members(self): 51 | self.assertEqual( self.v2.y, -453 ) 52 | self.assertEqual( self.v2[0], 45 ) 53 | # should not have been converted to a float 54 | self.assertNotEqual( type(self.v3['x']), type(543.0) ) 55 | v = Vector2d( 3, 4 ) 56 | self.assertEqual( v.length, 5 ) 57 | w = v.toLength( 10 ) 58 | self.assertAlmostEqual( w.x, 6 ) 59 | coords = (6, 8, 10) 60 | for i, c in enumerate(w): 61 | self.assertAlmostEqual(c, coords[i]) 62 | m = v.toLength(0.0) 63 | call_normal = lambda x: x.normalized() 64 | self.assertRaises(ZeroDivisionError, call_normal, m) 65 | 66 | 67 | def test_vector_operators(self): 68 | # test +, *, - 69 | v = Vector2d( -6, -8) 70 | u = Vector2d( 3, 4) 71 | w = v + u 72 | self.assertAlmostEqual(w.length, 5) 73 | w = v - u 74 | self.assertAlmostEqual(w.length, 15) 75 | w = v + 10 76 | self.assertAlmostEqual(w.length, 20) 77 | self.assertAlmostEqual(v * u, -50) 78 | self.assertEqual(v * u, u * v) 79 | w = u * 2 80 | self.assertAlmostEqual(w.length, 10) 81 | coords = [c for c in u] 82 | w = Vector2d( *coords ) 83 | self.assertTrue( w.coords == u.coords ) 84 | self.assertTrue( w == u ) 85 | self.assertFalse( w is u ) 86 | self.assertAlmostEqual(v.angleTo(u), math.pi) 87 | add = lambda n: w + n 88 | mul = lambda n: w * n 89 | self.assertRaises(TypeError, add, 'string') 90 | self.assertRaises(TypeError, mul, 'string') 91 | d = {w:10} 92 | self.assertEqual(d[u], 10) 93 | 94 | def test_vector2d_items(self): 95 | v = Vector2d(3, 6) 96 | d = {'x':3, 'y':6} 97 | self.assertEqual( v.x, 3) 98 | self.assertEqual( v.y, 6) 99 | self.assertEqual( v['x'], 3) 100 | self.assertEqualDicts( v.asDict(), d ) 101 | self.assertEqual( v.toX(-20.5).x, -20.5 ) 102 | self.assertEqual( v.toY(-20.5).y, -20.5 ) 103 | v.x = 9 104 | v.y = -12 105 | self.assertEqual( str(v), 'Vector2d(9, -12)') 106 | 107 | def test_vector3d_items(self): 108 | v = Vector3d(-3, 6, 19) 109 | d = {'x':-3, 'y':6, 'z':19} 110 | self.assertEqual( v.z, 19 ) 111 | self.assertEqualDicts( v.asDict(), d ) 112 | self.assertEqual( v.toX(-20.5).x, -20.5 ) 113 | self.assertEqual( v.toY(-20.5).y, -20.5 ) 114 | self.assertEqual( v.toZ(-20.5).z, -20.5 ) 115 | v.z = -24 116 | self.assertEqual( v['z'], -24 ) 117 | self.assertEqual( str(v), 'Vector3d(-3, 6, -24)') 118 | 119 | 120 | class TestPoints(TestVectors): 121 | 122 | def test_point_constructor(self): 123 | pass 124 | 125 | def test_point_operators(self): 126 | pass 127 | 128 | --------------------------------------------------------------------------------