"""DisplayPolyData object to control the representation of a Polydata object."""
import pathlib
import uuid
from typing import List, Union
from ladybug.color import Color
from ladybug_display.visualization import AnalysisGeometry, ContextGeometry, \
DisplayMesh3D, Mesh3D, Point3D, DisplayPoint3D, Mesh2D, DisplayMesh2D
from .from_geometry import from_points3d
from .polydata import PolyData
from .joined_polydata import JoinedPolyData
from .vtkjs.schema import DataSetProperty, DataSet, DisplayMode, DataSetMapper
display_mode_mapper = {
'Surface': DisplayMode.Surface,
'SurfaceWithEdges': DisplayMode.SurfaceWithEdges,
'Wireframe': DisplayMode.Wireframe,
'Points': DisplayMode.Points
}
[docs]
class DisplayPolyData:
"""A collection of PolyData with display attributes.
All the PolyData must have data with the same names attached to them.
This object is similar to an AnalysisGeometry object in ladybug-display.
Args:
name: A display name.
identifier: A unique identifier for this DisplayPolyData. This identifier should
be unique among all the DisplayPolyData in a VisualizationSet.
polydata: A list of PolyData objects.
color: A Ladybug color to set the diffuse color for DisplayPolyData to use when
there is no data available.
display_mode: Display model. It can be set to Surface, SurfaceWithEdges,
Wireframe and Points.
hidden: A boolean to note whether the geometry is hidden by default and must be
un-hidden to be visible in the 3D scene. Default is False.
"""
def __init__(
self, name: str, identifier: str, *, polydata: List[PolyData] = None,
color: Color = None, display_mode: DisplayMode = DisplayMode.Surface,
hidden: bool = False
) -> None:
self.name = name
self.identifier = identifier or str(uuid.uuid4())
polydata = polydata or []
self.polydata = [pd for pd in polydata if pd] # remove None values
if len(self.polydata) != len(polydata):
print(f'{self.name} includes invalid Polydata.')
self.color = color or Color(204, 204, 204, 255)
self.display_mode = display_mode
self.hidden = hidden
[docs]
@classmethod
def from_visualization_geometry(
cls, geometry: Union[AnalysisGeometry, ContextGeometry]
) -> 'DisplayPolyData':
if isinstance(geometry, AnalysisGeometry):
return cls.from_analysis_geometry(geometry)
else:
return cls.from_context_geometry(geometry)
[docs]
@classmethod
def from_analysis_geometry(cls, geometry: AnalysisGeometry) -> 'DisplayPolyData':
"""Create DisplayPolyData from an AnalysisGeometry."""
geometries = geometry.geometry
poly_datas: List[PolyData] = [geo.to_polydata() for geo in geometries]
matching_method = geometry.matching_method
all_mesh = all(
isinstance(geo, (DisplayMesh3D, Mesh3D, Mesh2D, DisplayMesh2D))
for geo in geometries
)
all_points = all(
isinstance(geo, (DisplayPoint3D, Point3D)) for geo in geometries
)
if all_points:
# all the geometries are Points - translate them to a single Polydata
poly_datas = [from_points3d(geometries)]
matching_method = 'vertices'
for ds_count, data_set in enumerate(geometry.data_sets):
ds_name = PolyData._get_dataset_name(data_set)
if matching_method == 'geometries' and not all_points:
data_set_values = []
# generate the data per face of based on the number of faces of geometry
for value, poly_data in zip(data_set.values, poly_datas):
face_count = poly_data.GetNumberOfCells()
values = [value] * face_count
data_set_values.append(values)
matching_method = 'faces'
elif all_points:
data_set_values = [data_set.values]
elif all_mesh:
# break down the values for each mesh based on the number of faces
# or the number of vertices
if len(geometries) == 1:
data_set_values = [data_set.values]
else:
data_set_values = []
starting_index = 0
for geo in geometries:
if matching_method == 'faces':
count = len(geo.faces)
else:
count = len(geo.vertices)
values = data_set.values[starting_index: starting_index + count]
data_set_values.append(values)
starting_index += count
else:
print(data_set)
for geometry in geometries:
print(type(geometry))
assert False, '^ Unknown data mapping combination.'
# add this dataset to polydatas
for count, poly_data in enumerate(poly_datas):
values = data_set_values[count]
poly_data.add_data(
data=values, name=ds_name,
per_face=not matching_method == 'vertices',
legend_parameters=data_set.legend_parameters,
unit=data_set.unit, data_type=data_set.data_type
)
if ds_count == geometry.active_data:
color_by = ds_name
vtk_data_set = cls(
name=geometry.display_name, identifier=geometry.identifier,
polydata=poly_datas,
display_mode=display_mode_mapper[geometry.display_mode],
hidden=geometry.hidden
)
vtk_data_set.color_by = color_by
return vtk_data_set
[docs]
@classmethod
def from_context_geometry(cls, geometry: ContextGeometry) -> 'DisplayPolyData':
"""Create DisplayPolyData from a ContextGeometry."""
# context geometry
# the assumption is that all the geometries under the same context geometry
# have the same display mode and color. We pick the first item.
try:
first_geometry = geometry.geometry[0]
except IndexError:
# no context geometry
return
all_points = all(
isinstance(geo, (DisplayPoint3D, Point3D)) for geo in geometry.geometry
)
if all_points:
poly_datas = [from_points3d(geometry.geometry)]
else:
poly_datas = [geometry.to_polydata() for geometry in geometry.geometry]
try:
display_mode = first_geometry.display_mode
except AttributeError:
# not a display geometry
display_mode = DisplayMode.Surface
else:
display_mode = display_mode_mapper[display_mode]
try:
color = first_geometry.color
except AttributeError:
color = None
vtk_data_set = cls(
name=geometry.display_name,
identifier=geometry.identifier,
polydata=poly_datas,
display_mode=display_mode,
color=color,
hidden=geometry.hidden
)
return vtk_data_set
@property
def is_empty(self):
return len(self.polydata) == 0
@property
def color(self) -> Color:
"""Diffuse color.
By default the dataset will be colored by this color unless color_by property
is set to a dataset value.
"""
return self._color
@color.setter
def color(self, value: Color):
self._color = value if value else Color(204, 204, 204, 255)
@property
def color_by(self) -> str:
"""Get and set the field that the DataSet should colored-by when exported to vtkjs.
By default the dataset will be colored by surface color and not data.
"""
return self.polydata[0].color_by
@color_by.setter
def color_by(self, value: str):
for data in self.polydata:
data.color_by = value
@property
def opacity(self) -> float:
"""Get and set the visualization opacity."""
return self.color.a
@opacity.setter
def opacity(self, value):
color = self.color.duplicate()
color.a = value
@property
def display_mode(self) -> DisplayMode:
"""Display model (AKA Representation) mode in VTK Glance viewer.
Valid values are:
* Surface
* SurfaceWithEdges
* Wireframe
* Points
Default is 0 for Surface mode.
"""
return self._display_mode
@display_mode.setter
def display_mode(self, mode: DisplayMode = DisplayMode.Surface):
self._display_mode = mode
@property
def hidden(self) -> bool:
"""Visualization default state on load.
A boolean to note whether the geometry is hidden by default and must be
un-hidden to be visible in the 3D scene.
"""
return self._hidden
@hidden.setter
def hidden(self, value: bool):
self._hidden = bool(value)
@property
def edge_visibility(self) -> bool:
"""Edge visibility.
The edges will be visible in Wireframe or SurfaceWithEdges modes.
"""
if self.display_mode.value in (0, 2):
return False
else:
return True
[docs]
def to_folder(self, folder, sub_folder=None) -> str:
"""Write data information to a folder.
Args:
folder: Target folder to write the dataset.
sub_folder: Subfolder name for this dataset. By default it will be set to
the name of the dataset.
"""
sub_folder = sub_folder or self.identifier
target_folder = pathlib.Path(folder, sub_folder)
if len(self.polydata) == 0:
return
elif len(self.polydata) == 1:
data = self.polydata[0]
else:
data = JoinedPolyData.from_polydata(self.polydata)
return data.to_folder(target_folder.as_posix())
[docs]
def to_vtk_dataset(self) -> DataSet:
"""Convert to a vtkjs DataSet object."""
prop = {
'representation': min(self.display_mode.value, 2),
'edgeVisibility': int(self.edge_visibility),
'diffuseColor': [self.color.r / 255, self.color.g / 255, self.color.b / 255],
'opacity': self.opacity / 255
}
ds_prop = DataSetProperty.parse_obj(prop)
mapper = DataSetMapper()
if self.color_by:
mapper.colorByArrayName = self.color_by
# Collect meta data for each data attached to polydata. Since all the polydata
# in a DisplayPolyData most have the same metadata we pick the first one
metadata = [
metadata.to_vtk_metadata().dict()
for metadata in self.polydata[0].data.values()
]
data = {
'name': self.name,
'httpDataSetReader': {'url': self.identifier},
'property': ds_prop.dict(),
'mapper': mapper.dict(),
'metadata': metadata,
'hidden': self.hidden
}
return DataSet.parse_obj(data)
def __repr__(self) -> str:
return f'DisplayPolyData: {self.name}' \
f'\n DataSets: #{len(self.polydata)}\n Color: {self.color}'