Skip to content

Commit

Permalink
fix(face): Fix automatic normal calculation in Face3D
Browse files Browse the repository at this point in the history
The calculation was producing vectors with 0 magnitude in the case that the first 3 vertices were co-linear.
  • Loading branch information
chriswmackey authored and Chris Mackey committed Oct 30, 2019
1 parent 6049465 commit bb5aaa1
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 18 deletions.
3 changes: 3 additions & 0 deletions ladybug_geometry/geometry2d/_2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ def __getitem__(self, key):

def __iter__(self):
return iter(self.vertices)

def __copy__(self):
return Base2DIn2D(self._vertices)

def ToString(self):
"""Overwrite .NET ToString."""
Expand Down
10 changes: 8 additions & 2 deletions ladybug_geometry/geometry2d/pointvector.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Vector2D(object):
* y
* magnitude
* magnitude_squared
* is_zero
"""
__slots__ = ('_x', '_y')

Expand Down Expand Up @@ -57,6 +58,11 @@ def magnitude(self):
def magnitude_squared(self):
"""Get the magnitude squared of the vector."""
return self.x ** 2 + self.y ** 2

@property
def is_zero(self):
"""Boolean to note whether the vector has a magnitude of zero."""
return self.x == 0 and self.y == 0

def normalize(self):
"""Get a copy of the vector that is a unit vector (magnitude=1)."""
Expand Down Expand Up @@ -228,12 +234,12 @@ def __mul__(self, other):
def __div__(self, other):
assert type(other) in (int, float), \
'Cannot divide types {} and {}'.format(self.__class__.__name__, type(other))
return Vector2D(operator.div(self.x, other), operator.div(self.y, other))
return Vector2D(self.x / other, self.y / other)

def __rdiv__(self, other):
assert type(other) in (int, float), \
'Cannot divide types {} and {}'.format(self.__class__.__name__, type(other))
return Vector2D(operator.div(other, self.x), operator.div(other, self.y))
return Vector2D(other / self.x, other / self.y)

def __floordiv__(self, other):
assert type(other) in (int, float), \
Expand Down
3 changes: 3 additions & 0 deletions ladybug_geometry/geometry3d/_2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ def __getitem__(self, key):

def __iter__(self):
return iter(self.vertices)

def __copy__(self):
return Base2DIn3D(self._vertices)

def ToString(self):
"""Overwrite .NET ToString."""
Expand Down
24 changes: 18 additions & 6 deletions ladybug_geometry/geometry3d/face.py
Original file line number Diff line number Diff line change
Expand Up @@ -912,7 +912,7 @@ def get_mesh_grid(self, x_dim, y_dim=None, offset=None, flip=False,
vert_3d = tuple(self._plane.xy_to_xyz(pt)
for pt in grid_mesh2d.vertices)
else:
_off_num = -offset if flip is True else offset
_off_num = -1 * offset if flip is True else offset
_off_plane = self.plane.move(self.plane.n * _off_num)
vert_3d = tuple(_off_plane.xy_to_xyz(pt)
for pt in grid_mesh2d.vertices)
Expand Down Expand Up @@ -1628,13 +1628,25 @@ def _plane_from_vertices(vertices):
The first 3 vertices will be used to make the plane.
"""
try:
pt1, pt2, pt3 = vertices[:3]
v1 = pt2 - pt1
v2 = pt3 - pt1
n = v1.cross(v2)
i = 0
n = Vector3D(0, 0, 0)
while n.is_zero:
n = Face3D._normal_from_3pts(*vertices[i:i + 3])
i += 1
except Exception as e:
raise ValueError('Incorrect vertices input for Face3D:\n\t{}'.format(e))
return Plane(n, pt1)
return Plane(n, vertices[0])

@staticmethod
def _normal_from_3pts(pt1, pt2, pt3):
"""Get a normal vecort from 3 vertices.
The vector will have a magnitude of 0 if vertices are colinear.
"""
v1 = pt2 - pt1
v2 = pt3 - pt1
n = v1.cross(v2)
return n

@staticmethod
def _corner_pt_verts(corner_pt, verts3d, verts2d):
Expand Down
8 changes: 4 additions & 4 deletions ladybug_geometry/geometry3d/line.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,23 +71,23 @@ def length(self):
"""The length of the line segment."""
return self.v.magnitude

def is_horizontal(edge, tolerance):
def is_horizontal(self, tolerance):
"""Test whether this line segment is horizontal within a certain tolerance.
Args:
tolerance: The maximum difference between the z values of the start and
end coordinates at which the line segment is considered horizontal.
"""
return abs(edge.v.z) <= tolerance
return abs(self.v.z) <= tolerance

def is_vertical(edge, tolerance):
def is_vertical(self, tolerance):
"""Test whether this line segment is vertical within a certain tolerance.
Args:
tolerance: The maximum difference between the x and y values of the start
and end coordinates at which the line segment is considered horizontal.
"""
return abs(edge.v.x) <= tolerance and abs(edge.v.y) <= tolerance
return abs(self.v.x) <= tolerance and abs(self.v.y) <= tolerance

def flip(self):
"""Get a copy of this line segment that is flipped."""
Expand Down
14 changes: 8 additions & 6 deletions ladybug_geometry/geometry3d/pointvector.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Vector3D(object):
* z
* magnitude
* magnitude_squared
* is_zero
"""
__slots__ = ('_x', '_y', '_z')

Expand Down Expand Up @@ -67,6 +68,11 @@ def magnitude(self):
def magnitude_squared(self):
"""Get the magnitude squared of the vector."""
return self.x ** 2 + self.y ** 2 + self.z ** 2

@property
def is_zero(self):
"""Boolean to note whether the vector has a magnitude of zero."""
return self.x == 0 and self.y == 0 and self.z == 0

def normalize(self):
"""Get a copy of the vector that is a unit vector (magnitude=1)."""
Expand Down Expand Up @@ -249,16 +255,12 @@ def __mul__(self, other):
def __div__(self, other):
assert type(other) in (int, float), \
'Cannot divide types {} and {}'.format(self.__class__.__name__, type(other))
return Vector3D(operator.div(self.x, other),
operator.div(self.y, other),
operator.div(self.z, other))
return Vector3D(self.x / other, self.y / other, self.z / other)

def __rdiv__(self, other):
assert type(other) in (int, float), \
'Cannot divide types {} and {}'.format(self.__class__.__name__, type(other))
return Vector3D(operator.div(other, self.x),
operator.div(other, self.y),
operator.div(other, self.z))
return Vector3D(other / self.x, other / self.y, other / self.z)

def __floordiv__(self, other):
assert type(other) in (int, float), \
Expand Down
12 changes: 12 additions & 0 deletions tests/face3d_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ def test_face3d_init_from_vertices():
assert face.vertices[0] == face[0]


def test_face3d_init_from_vertices_colinear():
"""Test the initalization of Face3D objects with colinear vertices."""
pts = (Point3D(0, 0, 2), Point3D(0, 1, 2), Point3D(0, 2, 2), Point3D(2, 2, 2),
Point3D(2, 0, 2))
face = Face3D(pts)

assert not face.normal.is_zero
assert face.plane.n == Vector3D(0, 0, -1)
assert face.plane.n == face.normal
assert face.plane.o == Point3D(0, 0, 2)


def test_face3d_init_from_extrusion():
"""Test the initalization of Face3D from_extrusion."""
line_seg = LineSegment3D(Point3D(0, 0, 0), Vector3D(2, 0, 0))
Expand Down

0 comments on commit bb5aaa1

Please sign in to comment.