# coding=utf-8
"""2D Mesh"""
from __future__ import division
try:
from itertools import izip as zip # python 2
except ImportError:
xrange = range # python 3
from .._mesh import MeshBase
from ..triangulation import earcut
from .pointvector import Point2D, Vector2D
from .line import LineSegment2D
from .polyline import Polyline2D
from .polygon import Polygon2D
[docs]
class Mesh2D(MeshBase):
"""2D Mesh object.
Args:
vertices: A list or tuple of Point2D objects for vertices.
faces: A list of tuples with each tuple having either 3 or 4 integers.
These integers correspond to indices within the list of vertices.
colors: An optional list of colors that correspond to either the faces
of the mesh or the vertices of the mesh. Default is None.
Properties:
* vertices
* faces
* colors
* is_color_by_face
* min
* max
* center
* area
* centroid
* face_areas
* face_centroids
* face_area_centroids
* face_vertices
* vertex_connected_faces
* edges
* naked_edges
* internal_edges
* non_manifold_edges
"""
__slots__ = ('_min', '_max', '_center', '_centroid')
def __init__(self, vertices, faces, colors=None):
"""Initialize Mesh2D."""
self._vertices = self._check_vertices_input(vertices)
self._faces = self._check_faces_input(faces)
self._is_color_by_face = False # default if colors is None
self.colors = colors
self._min = None
self._max = None
self._center = None
self._area = None
self._centroid = None
self._face_areas = None
self._face_centroids = None
self._face_area_centroids = None
self._vertex_connected_faces = None
self._edge_indices = None
self._edge_types = None
self._edges = None
self._naked_edges = None
self._internal_edges = None
self._non_manifold_edges = None
[docs]
@classmethod
def from_dict(cls, data):
"""Create a Mesh2D from a dictionary.
Args:
data: A python dictionary in the following format
.. code-block:: python
{
"type": "Mesh2D",
"vertices": [(0, 0), (10, 0), (0, 10)],
"faces": [(0, 1, 2)],
"colors": [{"r": 255, "g": 0, "b": 0}]
}
"""
colors = None
if 'colors' in data and data['colors'] is not None and len(data['colors']) != 0:
try:
from ladybug.color import Color
except ImportError:
raise ImportError('Colors are specified in input Mesh2D dictionary '
'but failed to import ladybug.color')
colors = tuple(Color.from_dict(col) for col in data['colors'])
fcs = tuple(tuple(f) for f in data['faces']) # cast to immutable type
return cls(tuple(Point2D.from_array(pt) for pt in data['vertices']), fcs, colors)
[docs]
@classmethod
def from_face_vertices(cls, faces, purge=True):
"""Create a mesh from a list of faces with each face defined by Point2Ds.
Args:
faces: A list of faces with each face defined as a list of 3 or 4 Point2D.
purge: A boolean to indicate if duplicate vertices should be shared between
faces. Default is True to purge duplicate vertices, which can be slow
for large lists of faces but results in a higher-quality mesh with
a smaller size in memory. Note that vertices are only considered
duplicate if the coordinate values are equal to one another
within floating point tolerance. To remove duplicate vertices
within a specified tolerance other than floating point, the
from_purged_face_vertices method should be used instead.
"""
vertices, face_collector = cls._interpret_input_from_face_vertices(faces, purge)
return cls(tuple(vertices), tuple(face_collector))
[docs]
@classmethod
def from_purged_face_vertices(cls, faces, tolerance):
"""Create a mesh from a list of faces with each face defined by Point3Ds.
This method is slower than 'from_face_vertices' but will result in a mesh
with fewer vertices and a smaller size in memory. This method is similar to
using the 'purge' option in 'from_face_vertices' but will result in more shared
vertices since it uses a tolerance to check equivalent vertices rather than
comparing within floating point tolerance.
Args:
faces: A list of faces with each face defined as a list of 3 or 4 Point3D.
tolerance: A number for the minimum difference between coordinate
values at which point vertices are considered equal to one another.
"""
vertices, faces = cls._interpret_input_from_face_vertices_with_tolerance(
faces, tolerance)
return cls(tuple(vertices), tuple(faces))
[docs]
@classmethod
def from_polygon_triangulated(cls, boundary_polygon, hole_polygons=None):
"""Initialize a triangulated Mesh2D from a Polygon2D.
The triangles of the mesh faces will always completely fill the shape
defines by the input boundary_polygon with holes subtracted from it.
Args:
boundary_polygon: A Polygon2D object representing the boundary of the shape.
hole_polygons: Optional list of Polygon2D objects representing holes
within the boundary_polygon.
"""
assert isinstance(boundary_polygon, Polygon2D), 'boundary_polygon must be a ' \
'Polygon2D to use from_polygon_triangulated. Got {}.'.format(
type(boundary_polygon))
if hole_polygons is None and boundary_polygon.is_convex: # fan triangulation!
_faces = []
for i in xrange(1, len(boundary_polygon) - 1):
_faces.append((0, i, i + 1))
_new_mesh = cls(boundary_polygon.vertices, _faces)
else: # slower ear-clipping method
if hole_polygons is not None:
for hole in hole_polygons:
assert isinstance(hole, Polygon2D), 'Hole must be a Polygon2D ' \
'to use from_polygon_triangulated. Got {}.'.format(type(hole))
_vertices, _faces = Mesh2D._ear_clipping_triangulation(
boundary_polygon, hole_polygons)
_new_mesh = cls(_vertices, _faces)
return _new_mesh
[docs]
@classmethod
def from_polygon_grid(cls, polygon, x_dim, y_dim, generate_centroids=True):
"""Initialize a gridded Mesh2D from a Polygon2D.
Note that this gridded mesh will usually not completely fill the polygon.
Essentially, this method generates a grid over the domain of the polygon
and then removes any points that do not lie within the polygon.
Args:
polygon: A Polygon2D object.
x_dim: The x dimension of the grid cells as a number.
y_dim: The y dimension of the grid cells as a number.
generate_centroids: Set to True to have the face centroids generated
alongside the grid of vertices, which is much faster than having
them generated upon request as they typically are. However, if you
have no need for the face centroids, you would save memory by setting
this to False. Default is True.
"""
assert isinstance(polygon, Polygon2D), 'Expected Polygon2D for' \
' Mesh2D.from_polygon_grid. Got {}'.format(type(polygon))
# figure out how many x and y cells to make
_x_dim, _num_x = Mesh2D._domain_dimensions(polygon.max.x - polygon.min.x, x_dim)
_y_dim, _num_y = Mesh2D._domain_dimensions(polygon.max.y - polygon.min.y, y_dim)
_poly_min = polygon.min
# generate the gid of points and faces
_verts = Mesh2D._grid_vertices(_poly_min, _num_x, _num_y, _x_dim, _y_dim)
_faces = Mesh2D._grid_faces(_num_x, _num_y)
_centroids = None
if generate_centroids is True: # calculate centroids if requested
_centroids = Mesh2D._grid_centroids(
_poly_min, _num_x, _num_y, _x_dim, _y_dim)
# figure out which vertices lie inside the polygon
# for tolerance reasons, we scale the polygon by a very small amount
# this avoids the fringe cases noted in the Polygon2d.is_point_inside description
tol_pt = Vector2D(0.0000001, 0.0000001)
scaled_poly = Polygon2D(
tuple(pt.scale(1.000001, _poly_min) - tol_pt for pt in polygon.vertices))
_pattern = [scaled_poly.is_point_inside(_v) for _v in _verts]
# build the mesh
_mesh_init = cls(_verts, _faces)
_mesh_init._face_centroids = _centroids
_mesh_init._face_area_centroids = _centroids
_new_mesh, _face_pattern = _mesh_init.remove_vertices(_pattern)
_new_mesh._face_areas = x_dim * y_dim
return _new_mesh
[docs]
@classmethod
def from_grid(cls, base_point=Point2D(), num_x=1, num_y=1, x_dim=1, y_dim=1,
generate_centroids=True):
"""Initialize a Mesh2D from parameters that define a grid.
Args:
base_point: The base point from which the mesh grid will be generated.
Default is (0, 0).
num_x: An integer for the number of mesh cells to generate in the
x direction. Default is 1.
num_y: An integer for the number of mesh cells to generate in the
y direction. Default is 1.
x_dim: The x dimension of the grid cells as a number. Default is 1.
y_dim: The y dimension of the grid cells as a number. Default is 1.
generate_centroids: Set to True to have the face centroids generated
alongside the grid of vertices, which is much faster than having
them generated upon request as they typically are. However, if you
have no need for the face centroids, you would save memory by setting
this to False. Default is True.
"""
_verts = Mesh2D._grid_vertices(base_point, num_x, num_y, x_dim, y_dim)
_faces = Mesh2D._grid_faces(num_x, num_y)
_centroids = None
if generate_centroids is True:
_centroids = Mesh2D._grid_centroids(base_point, num_x, num_y, x_dim, y_dim)
_new_mesh = cls(tuple(_verts), tuple(_faces))
_new_mesh._face_areas = x_dim * y_dim
_new_mesh._face_centroids = _centroids
_new_mesh._face_area_centroids = _centroids
return _new_mesh
@property
def min(self):
"""A Point2D for the minimum bounding rectangle vertex around this geometry."""
if self._min is None:
self._calculate_min_max()
return self._min
@property
def max(self):
"""A Point2D for the maximum bounding rectangle vertex around this geometry."""
if self._max is None:
self._calculate_min_max()
return self._max
@property
def center(self):
"""A Point2D for the center of the bounding rectangle around this geometry."""
if self._center is None:
min, max = self.min, self.max
self._center = Point2D((min.x + max.x) / 2, (min.y + max.y) / 2)
return self._center
@property
def face_areas(self):
"""A tuple of face areas that parallels the faces property."""
if self._face_areas is None:
self._face_areas = tuple(self._face_area(face) for face in self.faces)
elif isinstance(self._face_areas, (float, int)): # grid of faces with same area
self._face_areas = tuple(self._face_areas for face in self.faces)
return self._face_areas
@property
def centroid(self):
"""The centroid of the mesh as a Point2D (aka. center of mass).
Note that the centroid is more time consuming to compute than the center
(or the middle point of the bounding rectangle). So the center might be
preferred over the centroid if you just need a rough point for the middle
of the mesh.
"""
if self._centroid is None:
_weight_x = 0
_weight_y = 0
for _c, _a in zip(self.face_area_centroids, self.face_areas):
_weight_x += _c.x * _a
_weight_y += _c.y * _a
self._centroid = Point2D(_weight_x / self.area, _weight_y / self.area)
return self._centroid
@property
def face_edges(self):
"""List of polylines with one Polyline2D for each face.
This is faster to compute compared to the edges and results in effectively
the same type of wireframe visualization.
"""
_all_verts = self._vertices
f_edges = []
for face in self._faces:
verts = tuple(_all_verts[v] for v in face) + (_all_verts[face[0]],)
f_edges.append(Polyline2D(verts))
return f_edges
@property
def edges(self):
""""Tuple of all edges in this Mesh3D as LineSegment3D objects."""
if self._edges is None:
if self._edge_indices is None:
self._compute_edge_info()
self._edges = tuple(LineSegment2D.from_end_points(
self.vertices[seg[0]], self.vertices[seg[1]])
for seg in self._edge_indices)
return self._edges
@property
def naked_edges(self):
""""Tuple of all naked edges in this Mesh3D as LineSegment3D objects.
Naked edges belong to only one face in the mesh (they are not
shared between faces).
"""
if self._naked_edges is None:
self._naked_edges = self._get_edge_type(0)
return self._naked_edges
@property
def internal_edges(self):
""""Tuple of all internal edges in this Mesh3D as LineSegment3D objects.
Internal edges are shared between two faces in the mesh.
"""
if self._internal_edges is None:
self._internal_edges = self._get_edge_type(1)
return self._internal_edges
@property
def non_manifold_edges(self):
""""Tuple of all non-manifold edges in this mesh as LineSegment3D objects.
Non-manifold edges are shared between three or more faces.
"""
if self._non_manifold_edges is None:
if self._edges is None:
self.edges
nm_edges = []
for i, type in enumerate(self._edge_types):
if type > 1:
nm_edges.append(self._edges[i])
self._non_manifold_edges = tuple(nm_edges)
return self._non_manifold_edges
[docs]
def triangulated(self):
"""Get a version of this Mesh2D where all quads have been triangulated."""
_new_faces = []
for face in self.faces:
if len(face) == 3:
_new_faces.append(face)
else:
_triangles = Mesh2D._quad_to_triangles([self._vertices[i] for i in face])
_triangles = [tuple(face[vertex_idx] for vertex_idx in new_face)
for new_face in _triangles]
_new_faces.extend(_triangles)
_new_faces = tuple(_new_faces)
_new_colors = self.colors
if self.is_color_by_face is True:
_new_colors = []
for i, face in enumerate(self.faces):
if len(face) == 3:
_new_colors.append(self.colors[i])
else:
_new_colors.extend([self.colors[i]] * 2)
_new_colors = tuple(_new_colors)
_new_mesh = Mesh2D(self.vertices, _new_faces, _new_colors)
return _new_mesh
[docs]
def remove_vertices(self, pattern):
"""Get a version of this mesh where vertices are removed according to a pattern.
Args:
pattern: A list of boolean values denoting whether a vertex should
remain in the mesh (True) or be removed from the mesh (False).
The length of this list must match the number of this mesh's vertices.
Returns:
A tuple with two elements
- new_mesh:
A mesh where the vertices have been removed according
to the input pattern.
- face_pattern:
A list of boolean values that corresponds to the
original mesh faces noting whether the face is in the new mesh (True)
or has been removed from the new mesh (False).
"""
_new_verts, _new_faces, _new_colors, _new_f_cent, _new_f_area, face_pattern = \
self._remove_vertices(pattern)
new_mesh = Mesh2D(_new_verts, _new_faces, _new_colors)
new_mesh._face_centroids = _new_f_cent
new_mesh._face_areas = _new_f_area
return new_mesh, face_pattern
[docs]
def remove_faces(self, pattern):
"""Get a version of this mesh where faces are removed according to a pattern.
Args:
pattern: A list of boolean values denoting whether a face should
remain in the mesh (True) or be removed from the mesh (False).
The length of this list must match the number of this mesh's faces.
Returns:
A tuple with two elements
- new_mesh:
A mesh where the faces have been removed according
to the input pattern.
- vertex_pattern:
A list of boolean values that corresponds to the
original mesh vertices noting whether the vertex is in the new mesh
(True) or has been removed from the new mesh (False).
"""
vertex_pattern = self._vertex_pattern_from_remove_faces(pattern)
_new_verts, _new_faces, _new_colors, _new_f_cent, _new_f_area, face_pattern = \
self._remove_vertices(vertex_pattern, pattern)
new_mesh = Mesh2D(_new_verts, _new_faces, _new_colors)
new_mesh._face_centroids = _new_f_cent
new_mesh._face_areas = _new_f_area
return new_mesh, vertex_pattern
[docs]
def remove_faces_only(self, pattern):
"""Get a version of this mesh where faces are removed and vertices are unaltered.
This is faster than the Mesh2D.remove_faces method but will likely result
a lower-quality mesh where several vertices exist in the mesh that are not
referenced by any face. This may be preferred if pure speed of removing
faces is a priority over smallest size of the mesh in memory.
Args:
pattern: A list of boolean values denoting whether a face should
remain in the mesh (True) or be removed from the mesh (False).
The length of this list must match the number of this mesh's faces.
Returns:
new_mesh -- A mesh where the faces have been removed according
to the input pattern.
"""
_new_faces, _new_colors, _new_f_cent, _new_f_area = \
self._remove_faces_only(pattern)
new_mesh = Mesh2D(self.vertices, _new_faces, _new_colors)
new_mesh._face_centroids = _new_f_cent
new_mesh._face_areas = _new_f_area
return new_mesh
[docs]
def rotate(self, angle, origin):
"""Get a mesh that is rotated counterclockwise by a certain angle.
Args:
angle: An angle for rotation in radians.
origin: A Point2D for the origin around which the point will be rotated.
"""
_verts = tuple([pt.rotate(angle, origin) for pt in self.vertices])
return self._mesh_transform(_verts)
[docs]
def scale(self, factor, origin=None):
"""Scale a mesh by a factor from an origin point.
Args:
factor: A number representing how much the mesh should be scaled.
origin: A Point representing the origin from which to scale.
If None, it will be scaled from the World origin (0, 0).
"""
if origin is None:
_verts = tuple(
Point2D(pt.x * factor, pt.y * factor) for pt in self.vertices)
else:
_verts = tuple(pt.scale(factor, origin) for pt in self.vertices)
return self._mesh_scale(_verts, factor)
[docs]
def to_dict(self):
"""Get Mesh2D as a dictionary."""
colors = None
if self.colors is not None:
colors = [col.to_dict() for col in self.colors]
return {'type': 'Mesh2D',
'vertices': [pt.to_array() for pt in self.vertices],
'faces': self.faces, 'colors': colors}
[docs]
@staticmethod
def join_meshes(meshes):
"""Join an array of Mesh2Ds into a single Mesh2D.
Args:
meshes: An array of meshes to be joined into one.
Returns:
A single Mesh2D object derived from the input meshes.
"""
# set up empty lists of objects to be filled
verts = []
faces = []
colors = []
# loop through all of the meshes and get new faces
total_v_i = 0
for mesh in meshes:
verts.extend(mesh._vertices)
for fc in mesh._faces:
faces.append(tuple(v_i + total_v_i for v_i in fc))
total_v_i += len(mesh._vertices)
if mesh._colors:
colors.extend(mesh._colors)
# create the new mesh
if len(colors) != 0:
new_mesh = Mesh2D(verts, faces, colors)
else:
new_mesh = Mesh2D(verts, faces)
# attempt to transfer the centroids and normals
if all(msh._face_centroids is not None for msh in meshes):
new_mesh._face_centroids = tuple(pt for msh in meshes for pt in msh)
if all(msh._face_areas is not None for msh in meshes):
new_mesh._face_areas = tuple(a for msh in meshes for a in msh.face_areas)
return new_mesh
def _calculate_min_max(self):
"""Calculate maximum and minimum Point2D for this object."""
min_pt = [self.vertices[0].x, self.vertices[0].y]
max_pt = [self.vertices[0].x, self.vertices[0].y]
for v in self.vertices[1:]:
if v.x < min_pt[0]:
min_pt[0] = v.x
elif v.x > max_pt[0]:
max_pt[0] = v.x
if v.y < min_pt[1]:
min_pt[1] = v.y
elif v.y > max_pt[1]:
max_pt[1] = v.y
self._min = Point2D(min_pt[0], min_pt[1])
self._max = Point2D(max_pt[0], max_pt[1])
def _get_edge_type(self, edge_type):
"""Get all of the edges of a certain type in this mesh."""
if self._edges is None:
self.edges
sel_edges = []
for i, type in enumerate(self._edge_types):
if type == edge_type:
sel_edges.append(self._edges[i])
return tuple(sel_edges)
def _face_area(self, face):
"""Return the area of a face."""
return Mesh2D._get_area(tuple(self._vertices[i] for i in face))
def _tri_face_centroid(self, face):
"""Compute the centroid of a triangular face."""
return Mesh2D._tri_centroid(tuple(self._vertices[i] for i in face))
def _quad_face_centroid(self, face):
"""Compute the centroid of a quadrilateral face."""
return Mesh2D._quad_centroid(tuple(self._vertices[i] for i in face))
def _mesh_transform(self, verts):
"""Transform mesh in a way that transfers properties and avoids extra checks."""
_new_mesh = Mesh2D(verts, self.faces)
self._transfer_properties(_new_mesh)
return _new_mesh
def _mesh_scale(self, verts, factor):
"""Scale mesh in a way that transfers properties and avoids extra checks."""
_new_mesh = Mesh2D(verts, self.faces)
self._transfer_properties_scale(_new_mesh, factor)
return _new_mesh
def _check_vertices_input(self, vertices):
if not isinstance(vertices, tuple):
vertices = tuple(vertices)
for vert in vertices:
assert isinstance(vert, Point2D), \
'Expected Point2D for {} vertex. Got {}.'.format(
self.__class__.__name__, type(vert))
return vertices
@staticmethod
def _ear_clipping_triangulation(polygon, holes=None):
"""Triangulate a polygon and holes using the ear clipping method."""
# flatten the list of vertices and holes into a single list for earcut
vert_coords, hole_indices = [], None
for pt in polygon:
vert_coords.extend((pt.x, pt.y))
if holes is not None:
hole_indices = []
for hole in holes:
hole_indices.append(int(len(vert_coords) / 2))
for pt in hole:
vert_coords.extend((pt.x, pt.y))
# run the ear clipping triangulation
result_tri = earcut(vert_coords, hole_indices)
vertices = tuple(Point2D(*vert_coords[st:st + 2])
for st in range(0, len(vert_coords), 2))
faces = tuple(tuple(result_tri[st:st + 3])
for st in range(0, len(result_tri), 3))
return vertices, faces
@staticmethod
def _quad_to_triangles(verts):
"""Return two triangles that represent any quadrilateral."""
# check if the quad is convex
convex = True
pt1, pt2, pt3 = verts[1], verts[2], verts[3]
start_val = True if (pt2.x - pt1.x) * (pt3.y - pt2.y) - \
(pt2.y - pt1.y) * (pt3.x - pt2.x) > 0 else False
for i, pt3 in enumerate(verts[:3]):
pt1 = verts[i - 2]
pt2 = verts[i - 1]
val = True if (pt2.x - pt1.x) * (pt3.y - pt2.y) - \
(pt2.y - pt1.y) * (pt3.x - pt2.x) > 0 else False
if val is not start_val:
convex = False
break
if convex is True:
# if the quad is convex, either diagonal splits it into triangles
return [(0, 1, 2), (2, 3, 0)]
else:
# if it is concave, we need to select the right diagonal of the two
return Mesh2D._concave_quad_to_triangles(verts)
@staticmethod
def _concave_quad_to_triangles(verts):
"""Return two triangles that represent a concave quadrilateral."""
quad_poly = Polygon2D(verts)
diagonal = LineSegment2D.from_end_points(quad_poly[0], quad_poly[2])
if quad_poly.is_point_inside(diagonal.midpoint, Vector2D(1, 0.00001)):
# if the diagonal midpoint is inside the quad, it splits it into two ears
return [(0, 1, 2), (2, 3, 0)]
else:
# if not, then the other diagonal splits it into two ears
return [(1, 2, 3), (3, 0, 1)]
@staticmethod
def _face_center(verts):
"""Get the center of a list of Point3D vertices."""
_cent_x = sum([v.x for v in verts])
_cent_y = sum([v.y for v in verts])
v_count = len(verts)
return Point2D(_cent_x / v_count, _cent_y / v_count)
@staticmethod
def _quad_centroid(verts):
"""Get the centroid of a list of 4 Point2D vertices."""
_tri_i = Mesh2D._quad_to_triangles(verts)
_tri_verts = ([verts[i] for i in _tri_i[0]], [verts[i] for i in _tri_i[1]])
_tri_c = [Mesh2D._tri_centroid(tri) for tri in _tri_verts]
_tri_a = [Mesh2D._get_area(tri) for tri in _tri_verts]
_tot_a = sum(_tri_a)
_cent_x = (_tri_c[0].x * _tri_a[0] + _tri_c[1].x * _tri_a[1]) / _tot_a
_cent_y = (_tri_c[0].y * _tri_a[0] + _tri_c[1].y * _tri_a[1]) / _tot_a
return Point2D(_cent_x, _cent_y)
@staticmethod
def _tri_centroid(verts):
"""Get the centroid of a list of 3 Point2D vertices."""
_cent_x = sum([v.x for v in verts])
_cent_y = sum([v.y for v in verts])
return Point2D(_cent_x / 3, _cent_y / 3)
@staticmethod
def _get_area(verts):
"""Return the area of a list of Point2D vertices."""
_a = 0
for i, pt in enumerate(verts):
_a += verts[i - 1].x * pt.y - verts[i - 1].y * pt.x
return abs(_a / 2)
@staticmethod
def _domain_dimensions(_dom, _dim):
"""Get corrected dimensions and number of cells over a domain."""
_num = int(_dom / _dim)
_num = 1 if _num == 0 else _num
_dim = _dom / _num
return _dim, _num
@staticmethod
def _grid_vertices(base_point, num_x, num_y, x_dim, y_dim):
"""Generate Point2D vertices for a grid."""
_verts = []
_x = base_point.x
for i in xrange(num_x + 1):
_y = base_point.y
for j in xrange(num_y + 1):
_verts.append(Point2D(_x, _y))
_y += y_dim
_x += x_dim
return _verts
@staticmethod
def _grid_faces(num_x, num_y):
"""Generate face tuples for a grid."""
_faces = []
_c = 0
for i in xrange(num_x):
for j in xrange(num_y):
_faces.append((_c, _c + num_y + 1, _c + num_y + 2, _c + 1))
_c += 1
_c += 1
return _faces
@staticmethod
def _grid_centroids(base_point, num_x, num_y, x_dim, y_dim):
"""Generate Point2D centroids for a grid."""
_centroids = []
_x_half = x_dim / 2
_y_half = y_dim / 2
_x = base_point.x
for i in xrange(num_x):
_y = base_point.y
for j in xrange(num_y):
_centroids.append(Point2D(_x + _x_half, _y + _y_half))
_y += y_dim
_x += x_dim
return tuple(_centroids)
def __copy__(self):
_new_mesh = Mesh2D(self.vertices, self.faces)
self._transfer_properties(_new_mesh)
_new_mesh._face_centroids = self._face_centroids
_new_mesh._centroid = self._centroid
return _new_mesh
def __key(self):
"""A tuple based on the object properties, useful for hashing."""
return tuple(hash(pt) for pt in self._vertices) + \
tuple(hash(face) for face in self._faces)
def __hash__(self):
return hash(self.__key())
def __eq__(self, other):
return isinstance(other, Mesh2D) and self.__key() == other.__key()
def __repr__(self):
return 'Ladybug Mesh2D ({} faces) ({} vertices)'.format(
len(self.faces), len(self))