"""Functions to translate ladybug geometry objects into VTK polydata objects."""
import vtk
import math
from typing import List, Union
from ladybug_geometry.geometry2d import Point2D, LineSegment2D, Polyline2D, \
Polygon2D, Mesh2D
from ladybug_geometry.geometry3d import Point3D, Polyline3D, Arc3D, LineSegment3D,\
Mesh3D, Polyface3D, Cone, Cylinder, Sphere, Face3D, Plane, Vector3D
from .polydata import PolyData
[docs]
def from_point2d(point: Point2D) -> PolyData:
"""Create Polydata from a Ladybug Point2D object.
Args:
point: A ladybug Point object.
Returns:
Polydata containing a single point.
"""
vtk_point = vtk.vtkPoints()
vtk_vertice = vtk.vtkCellArray()
vtk_point.InsertNextPoint(point.x, point.y, 0)
vtk_vertice.InsertNextCell(1, [1])
polydata = PolyData()
polydata.SetPoints(vtk_point)
polydata.SetVerts(vtk_vertice)
polydata.Modified()
return polydata
def _polyline_from_points(points: List[Point2D]) -> PolyData:
"""Create Polydata from a list of Ladybug Point2D objects.
Args:
points: A list of Ladybug Point2D objects.
Returns:
Polydata containing a polyline created by joining the points.
"""
pts = vtk.vtkPoints()
for point in points:
pts.InsertNextPoint(point.x, point.y, 0)
polyline = vtk.vtkPolyLine()
polyline.GetPointIds().SetNumberOfIds(len(points))
for i in range(len(points)):
polyline.GetPointIds().SetId(i, i)
cells = vtk.vtkCellArray()
cells.InsertNextCell(polyline)
polydata = PolyData()
polydata.SetPoints(pts)
polydata.SetLines(cells)
return polydata
[docs]
def from_points2d(points: List[Point2D], join: bool = False) -> PolyData:
"""Create Polydata from a list of Ladybug Point2D objects.
Args:
points: A list of Ladybug Point2D objects.
join: Boolean to indicate whether the points should be joined into a polyline.
Returns:
Polydata containing all points or a polyline.
"""
if join:
return _polyline_from_points(points)
vtk_points = vtk.vtkPoints()
vtk_vertices = vtk.vtkCellArray()
for point in points:
vtk_points.InsertNextPoint(point.x, point.y, 0)
vtk_vertices.InsertNextCell(len(points), list(range(len(points))))
polydata = PolyData()
polydata.SetPoints(vtk_points)
polydata.SetVerts(vtk_vertices)
polydata.Modified()
return polydata
[docs]
def from_line2d(line: LineSegment2D) -> PolyData:
"""Create Polydata from a Ladybug LineSegment2D object.
Args:
line: A Ladybug LineSegment2D object.
Returns:
Polydata containing a line.
"""
return from_points2d(line.vertices, join=True)
[docs]
def from_polyline2d(polyline: Polyline2D) -> PolyData:
"""Create Polydata from a Ladybug Polyline2D object.
Args:
polyline: A Ladybug Polyline2D object.
Returns:
Polydata containing a polyline.
"""
return from_points2d(polyline.vertices, join=True)
[docs]
def from_polygon2d(polygon: Polygon2D) -> PolyData:
"""Create Polydata from a Ladybug Polygon2D object.
Args:
polygon: A Ladybug Polygon2D object.
Returns:
Polydata containing a polygon.
"""
verts = polygon.vertices + (polygon[0],)
return from_points2d(verts, join=True)
[docs]
def from_point3d(point: Point3D) -> PolyData:
"""Create Polydata from a Ladybug Point3D object.
Args:
point: A ladybug Point object.
Returns:
Polydata containing a single point.
"""
vtk_point = vtk.vtkPoints()
vtk_vertice = vtk.vtkCellArray()
vtk_point.InsertNextPoint(tuple(point))
vtk_vertice.InsertNextCell(1, [1])
polydata = PolyData()
polydata.SetPoints(vtk_point)
polydata.SetVerts(vtk_vertice)
polydata.Modified()
return polydata
def _polyline_from_points3d(points: List[Point3D]) -> PolyData:
"""Create Polydata from a list of Ladybug Point3D objects.
Args:
points: A list of Ladybug Point3D objects.
Returns:
Polydata containing a polyline created by joining the points.
"""
pts = vtk.vtkPoints()
for pt in points:
pts.InsertNextPoint(tuple(pt))
polyline = vtk.vtkPolyLine()
polyline.GetPointIds().SetNumberOfIds(len(points))
for i in range(len(points)):
polyline.GetPointIds().SetId(i, i)
cells = vtk.vtkCellArray()
cells.InsertNextCell(polyline)
polydata = PolyData()
polydata.SetPoints(pts)
polydata.SetLines(cells)
return polydata
[docs]
def from_points3d(points: List[Point3D], join: bool = False) -> PolyData:
"""Create Polydata from a list of Ladybug Point3D objects.
Args:
points: A list of Ladybug Point3D objects.
join: Boolean to indicate whether the points should be joined into a polyline.
Returns:
Polydata containing all points or a polyline.
"""
if join:
return _polyline_from_points3d(points)
vtk_points = vtk.vtkPoints()
vtk_vertices = vtk.vtkCellArray()
for point in points:
vtk_points.InsertNextPoint(tuple(point))
vtk_vertices.InsertNextCell(len(points), list(range(len(points))))
polydata = PolyData()
polydata.SetPoints(vtk_points)
polydata.SetVerts(vtk_vertices)
polydata.Modified()
return polydata
[docs]
def from_line3d(line: LineSegment3D) -> PolyData:
"""Create Polydata from a Ladybug LineSegment3D object.
Args:
line: A Ladybug LineSegment3D object.
Returns:
Polydata containing a line.
"""
return from_points3d(line.vertices, join=True)
[docs]
def from_polyline3d(polyline: Polyline3D) -> PolyData:
"""Create Polydata from a Ladybug Polyline3D object.
Args:
polyline: A Ladybug Polyline3D object.
Returns:
Polydata containing a polyline.
"""
return from_points3d(polyline.vertices, join=True)
[docs]
def from_arc3d(arc3d: Arc3D, resolution: int = 3) -> PolyData:
"""Create Polydata from a Ladybug Arc3D object.
Args:
arc3d: A Ladybug Arc3D object.
resolution: The number of degrees per subdivision. The default is 3 that creates
120 segments for an full circle.
Returns:
Polydata containing an arc.
"""
arc = vtk.vtkArcSource()
arc.UseNormalAndAngleOn()
arc.SetCenter(arc3d.c.x, arc3d.c.y, arc3d.c.z)
polar_vector = LineSegment3D.from_end_points(arc3d.c, arc3d.p1).v
arc.SetPolarVector(round(polar_vector.x, 2), round(
polar_vector.y, 2), round(polar_vector.z, 2))
normal = arc3d.plane.n
arc.SetNormal(round(normal.x, 2), round(normal.y, 2), round(normal.z, 2))
arc.SetAngle(math.degrees(arc3d.angle))
vtk_resolution = max(int(math.degrees(arc3d.angle) / resolution), 2)
arc.SetResolution(vtk_resolution)
arc.Update()
polydata = PolyData()
polydata.ShallowCopy(arc.GetOutput())
# delete the array named 'Texture Coordinates'
# that's generated automatically for some reason
polydata.GetPointData().RemoveArray('Texture Coordinates')
return polydata
[docs]
def from_mesh3d(mesh: Mesh3D) -> PolyData:
"""Create Polydata from a Ladybug mesh.
Args:
mesh: A Ladybug Mesh3D object.
Returns:
Polydata containing face and points of a mesh.
"""
points = vtk.vtkPoints()
polygon = vtk.vtkPolygon()
cells = vtk.vtkCellArray()
for ver in mesh.vertices:
points.InsertNextPoint(*ver)
for face in mesh.faces:
polygon.GetPointIds().SetNumberOfIds(len(face))
for count, i in enumerate(face):
polygon.GetPointIds().SetId(count, i)
cells.InsertNextCell(polygon)
polydata = PolyData()
polydata.SetPoints(points)
polydata.SetPolys(cells)
return polydata
[docs]
def from_mesh2d(mesh: Mesh2D) -> PolyData:
"""Create Polydata from a Ladybug mesh 2D.
Args:
mesh: A Ladybug Mesh2D object.
Returns:
Polydata containing face and points of a mesh.
"""
points = vtk.vtkPoints()
polygon = vtk.vtkPolygon()
cells = vtk.vtkCellArray()
for ver in mesh.vertices:
points.InsertNextPoint(ver[0], ver[1], 0)
for face in mesh.faces:
polygon.GetPointIds().SetNumberOfIds(len(face))
for count, i in enumerate(face):
polygon.GetPointIds().SetId(count, i)
cells.InsertNextCell(polygon)
polydata = PolyData()
polydata.SetPoints(points)
polydata.SetPolys(cells)
return polydata
[docs]
def from_face3d(face: Face3D) -> PolyData:
"""Create Polydata from a Ladybug face.
Args:
face: A Ladybug Face3D object.
Returns:
Polydata containing face and points of a face.
"""
if face.has_holes or not face.is_convex:
return from_mesh3d(face.triangulated_mesh3d)
points = vtk.vtkPoints()
polygon = vtk.vtkPolygon()
cells = vtk.vtkCellArray()
vertices_count = len(face.vertices)
polygon.GetPointIds().SetNumberOfIds(vertices_count)
for ver in face.vertices:
points.InsertNextPoint(*ver)
for count in range(vertices_count):
polygon.GetPointIds().SetId(count, count)
cells.InsertNextCell(polygon)
polydata = PolyData()
polydata.SetPoints(points)
polydata.SetPolys(cells)
return polydata
[docs]
def from_polyface3d(polyface: Polyface3D) -> PolyData:
"""Create Polydata from a Ladybug Polyface.
Args:
polyface: A Ladybug Polyface3D object.
Returns:
A Polydata representing the PolyFace3D.
"""
points = vtk.vtkPoints()
polygon = vtk.vtkPolygon()
cells = vtk.vtkCellArray()
for ver in polyface.vertices:
points.InsertNextPoint(*ver)
# starting number to count for the new vertices that are being added for
# triangulated meshes
addition = len(polyface.vertices)
for face, face_geo in zip(polyface.face_indices, polyface.faces):
if face_geo.has_holes or not face_geo.is_convex:
meshed_face = face_geo.triangulated_mesh3d
for ver in meshed_face.vertices:
points.InsertNextPoint(*ver)
for face in meshed_face.faces:
polygon.GetPointIds().SetNumberOfIds(len(face))
for count, i in enumerate(face):
polygon.GetPointIds().SetId(count, i + addition)
cells.InsertNextCell(polygon)
addition += len(meshed_face.vertices)
else:
face = face[0]
polygon.GetPointIds().SetNumberOfIds(len(face))
for count, i in enumerate(face):
polygon.GetPointIds().SetId(count, i)
cells.InsertNextCell(polygon)
polydata = PolyData()
polydata.SetPoints(points)
polydata.SetPolys(cells)
return polydata
[docs]
def from_cone(cone: Cone, resolution: int = 2, cap: bool = True) -> PolyData:
"""Create Polydata from a Ladybug Cone.
Args:
cone: A Ladybug Cone object.
resolution: The number of segments into which the cone will be divided.
If set to 0, a line will be created.
If set to 1, a single triangle will be created.
If set to 2, two crossed triangles will be created.
If set to greater than 2, a 3D cone with the number of sides equal to
the resolution will be created. Defaults to 2.
cap: Boolean to indicate whether the cone should capped or not. Default to True.
Returns:
Polydata containing a cone.
"""
cone_source = vtk.vtkConeSource()
cone_source.SetResolution(resolution)
cone_source.SetRadius(cone.radius)
cone_source.SetHeight(cone.height)
cone_source.SetDirection(tuple(cone.axis))
center = cone.vertex.move(cone.axis.reverse())
cone_source.SetCenter(tuple(center))
if not cap:
cone_source.CappingOff()
cone_source.Update()
polydata = PolyData()
polydata.ShallowCopy(cone_source.GetOutput())
return polydata
[docs]
def from_sphere(sphere: Sphere, resolution: int = 50) -> PolyData:
"""Create Polydata from a Ladybug Sphere.
Args:
sphere: A Ladybug Sphere object.
resolution: The number of segments into which the sphere will be divided.
Defaults to 50.
Returns:
Polydata containing a sphere.
"""
sphere_source = vtk.vtkSphereSource()
sphere_source.SetCenter(tuple(sphere.center))
sphere_source.SetRadius(sphere.radius)
sphere_source.SetPhiResolution(resolution)
sphere_source.SetThetaResolution(resolution)
sphere_source.Update()
polydata = PolyData()
polydata.ShallowCopy(sphere_source.GetOutput())
return polydata
[docs]
def from_cylinder(
cylinder: Cylinder, resolution: int = 50, cap: bool = True
) -> PolyData:
"""Create Polydata from a Ladybug Cylinder.
Args:
cylinder: A Ladybug Cylinder object.
resolution: The number of segments into which the cylinder will be divided.
Defaults to 50.
cap: Boolean to indicate whether the cylinder should capped or not.
Default to True.
Returns:
Polydata containing a cylinder.
"""
cylinder_source = vtk.vtkCylinderSource()
cylinder_source.SetCenter(tuple(cylinder.center))
cylinder_source.SetRadius(cylinder.radius)
cylinder_source.SetHeight(cylinder.height)
cylinder_source.SetResolution(resolution)
if not cap:
cylinder_source.CappingOff()
cylinder_source.Update()
polydata = PolyData()
polydata.ShallowCopy(cylinder_source.GetOutput())
return polydata
[docs]
def from_text(
text: str, *, plane: Union[Point3D, Point2D, Plane], height: float = 2,
horizontal_alignment: int = 0, vertical_alignment: int = 2
) -> PolyData:
"""Create a VTK text object from a text string and a ladybug Point3D.
Args:
text: A text string.
plane: A ladybug Plane, Point3D or Point2D object to locate and orient the text
in the VTK scene.
height: A number for the height of the text in the scene. Defaults is set to 2.
horizontal_alignment: An optional integer to specify the horizontal alignment
of the text. Choose from: (0 = Left, 1 = Center, 2 = Right).
vertical_alignment: An optional integer to specify the vertical alignment of
the text. Choose from: (0 = Top, 1 = Middle, 2 = Bottom)
Returns:
A Polydata object containing the text.
"""
def _apply_transformation(
source: vtk.vtkVectorText,
plane: Plane,
height: float,
offset_multiline: bool = False
) -> vtk.vtkTransformPolyDataFilter:
transform = vtk.vtkTransform()
transform.PostMultiply()
# VTK generates the texts initially on a plane at the origin
original_plane = Plane(
n=Vector3D(0, 0, 1), o=Point3D(0, 0, 0), x=Vector3D(1, 0, 0)
)
rotated_plane = original_plane
if plane.n != original_plane.n:
# match the normal of the two plane
angle_rad = original_plane.n.angle(plane.n)
angle = math.degrees(angle_rad)
if angle > 0.1:
vector = original_plane.n.cross(plane.n)
if vector == Vector3D(0, 0, 0):
vector = Vector3D(1, 0, 0)
transform.RotateWXYZ(angle, vector.x, vector.y, vector.z)
rotated_plane = original_plane.rotate(vector, angle_rad, original_plane.o)
# now match the x axis
angle_rad = rotated_plane.x.angle(plane.x)
angle = math.degrees(angle_rad)
if angle > 0.1:
vector = rotated_plane.n
# this is an edge case that I couldn't really figure out why is happening
# we should try to find a more generic solution. Use the simple_box.vsf
# sample file for testing
if rotated_plane.x.angle(Vector3D(0, 0, 1)) < 0.01 and \
plane.x.angle(Vector3D(0, -1, 0)) < 0.01 and \
vector.angle(Vector3D(-1, 0, 0)) < 0.01:
vector = Vector3D(1, 0, 0)
transform.RotateWXYZ(angle, vector.x, vector.y, vector.z)
rotated_plane = rotated_plane.rotate(vector, angle_rad, original_plane.o)
transform.Scale(height, height, height)
# add a transformation to move the text to the new plane origin
almost_horizontal = math.degrees(rotated_plane.x.angle(Vector3D(1, 0, 0))) < 5
if offset_multiline:
if almost_horizontal and vertical_alignment == 0:
# horizontal text with top adjustment. No need to move
line_count = 0
else:
line_count = len(text.split('\n')) - 1
move_vector = rotated_plane.y * (line_count * 1.5 * height)
move_vector = move_vector.reverse()
moved_point = plane.o.move(move_vector)
transform.Translate(*moved_point)
else:
transform.Translate(*plane.o)
transformFilter = vtk.vtkTransformPolyDataFilter()
transformFilter.SetInputConnection(source.GetOutputPort())
transformFilter.SetTransform(transform)
transformFilter.Update()
tf = transformFilter.GetOutput()
return tf
if isinstance(plane, Point3D):
plane = Plane(o=plane)
elif isinstance(plane, Point2D):
plane = Plane(o=Point3D(plane.x, plane.y, 0))
assert isinstance(plane, Plane), 'The plane for text must be from a Ladybug Plane.'
source = vtk.vtkVectorText()
source.SetText(text)
# initial transform to project the text from origin to the target plane
transform_filter = _apply_transformation(source, plane, height)
# this vector is the initial difference between the left lower corner og the
# generated text and the desired text insertion point. In theory, it should be 0
# but VTK doesn't generate the text at the exact 0, 0, 0 - the difference is usually
# very small
bounds = list(transform_filter.GetBounds())
left_lower_corner = Point3D(bounds[0], bounds[2], bounds[4])
offset_vector = plane.o - left_lower_corner
# make adjustments for text justification
if not (horizontal_alignment == 0 and vertical_alignment == 2):
# find the size of the text
bounds = list(transform_filter.GetBounds())
bottom_left = Point3D(bounds[0], bounds[2], bounds[4])
top_right = Point3D(bounds[1], bounds[3], bounds[5])
bottom_left_2d = plane.xyz_to_xy(bottom_left)
top_right_2d = plane.xyz_to_xy(top_right)
x_dist = top_right_2d.x - bottom_left_2d.x
y_dist = top_right_2d.y - bottom_left_2d.y
if horizontal_alignment == 0: # left alignment
x_dist = 0
elif horizontal_alignment == 1: # center alignment
x_dist = x_dist / 2
if vertical_alignment == 2: # bottom alignment
y_dist = 0
elif vertical_alignment == 1: # center alignment
y_dist = y_dist / 2
x_vector = plane.x * -1 * x_dist
y_vector = plane.y * -1 * y_dist
move_vector = x_vector + y_vector
plane = plane.move(moving_vec=move_vector)
plane = plane.move(offset_vector)
transform_filter = _apply_transformation(
source, plane, height, offset_multiline=True
)
polydata = PolyData()
polydata.ShallowCopy(transform_filter)
return polydata
[docs]
def to_circle(center: Point3D, radius: int = 100, sides: int = 100) -> PolyData:
"""Create a VTK circle from a ladybug Point3D and radius.
Args:
center: A ladybug Point3D object.
radius: The radius of the circle. Defaults to 100 meters.
sides: The number of sides of the circle. Defaults to 100.
Returns:
A Polydata object containing a circle.
"""
polygonSource = vtk.vtkRegularPolygonSource()
polygonSource.GeneratePolygonOff()
polygonSource.SetNumberOfSides(sides)
polygonSource.SetRadius(radius)
polygonSource.SetCenter(center.x, center.y, center.z)
polygonSource.Update()
polydata = PolyData()
polydata.ShallowCopy(polygonSource.GetOutput())
return polydata