Source code for honeybee_vtk.assistant

"""An assistant class to the Scene object."""

import vtk
import pathlib
from typing import Tuple
from .camera import Camera
from .types import ImageTypes
from .actor import Actor
from .text_actor import TextActor

from typing import List


[docs]class Assistant: """Initialize an Assistant object. This is more of a helper class and the interface for this class is the Scene object. This class takes a camera and a list of actors to assemble a vtk interactor, a vtk renderWindow, and a vtk renderer objects. Args: background_color: A tuple of three integers that represent RGB values of the color that you'd like to set as the background color. actors: A list of actors. camera: A Camera object. legend_parameters: A list of legend parameter objects to be added to the scene text_actor: A TextActor object. Defaults to None. """ def __init__(self, background_color: Tuple[int, int, int], camera: Camera, actors: List[Actor], legend_parameters: list, text_actor: TextActor = None) -> None: self._background_color = background_color self._actors = actors self._camera = camera self._legend_params = legend_parameters self._text_actor = text_actor self._vtk_camera = None @property def vtk_camera(self) -> vtk.vtkCamera: return self._vtk_camera @vtk_camera.setter def vtk_camera(self, vtk_camera): assert isinstance(vtk_camera, vtk.vtkCamera), \ 'vtk_camera must be a vtkCamera object.' self._vtk_camera = vtk_camera def _create_window(self) -> \ Tuple[vtk.vtkRenderWindowInteractor, vtk.vtkRenderWindow, vtk.vtkRenderer]: """Create a rendering window with a single renderer and an interactor. The objects are embedded inside each other. interactor is the outmost layer. A render is added to a window and then the window is set inside the interactor. This method returns a tuple of a window_interactor, a render_window, and a renderer. Returns: A tuple with three elements - interactor: A vtkRenderWindowInteractor object. - window: A vtkRenderWindow object. - renderer: A vtkRenderer object. """ if not self._camera: raise ValueError( 'A camera is needed to create a render window.' ) # Setting renderer, render window, and interactor renderer = vtk.vtkRenderer() # Add actors to the window for actor in self._actors: renderer.AddActor(actor.to_vtk()) # add text actor if provided if self._text_actor: renderer.AddActor(self._text_actor.to_vtk()) # Add legends to the window if self._legend_params: for legend_param in self._legend_params: if not legend_param.hide_legend: # Add legends to renderer renderer.AddActor(legend_param.get_scalarbar()) # add renderer to rendering window window = vtk.vtkRenderWindow() window.AddRenderer(renderer) # set rendering window in window interactor interactor = vtk.vtkRenderWindowInteractor() interactor.SetRenderWindow(window) # Set background color renderer.SetBackground(self._background_color) renderer.TwoSidedLightingOn() if self.vtk_camera: renderer.SetActiveCamera(self.vtk_camera) else: renderer.SetActiveCamera(self._camera.to_vtk()) if self._camera.reset_camera: renderer.ResetCamera() # the order is from outside to inside return interactor, window, renderer def _export_gltf(self, folder: str, name: str) -> str: """Export a scene to a glTF file. Args: folder: A valid path to where you'd like to write the gltf file. name: Name of the gltf file as a text string. Returns: A text string representing the path to the gltf file. """ # TODO: Find out why model's displaymode is not applied # TODO: Find out if the axis should be rotated to work with gltf viewers # TODO: Find a viewer for gltf files. The up axis of f3d viewer is Y axis. window, renderer = self._create_window()[1:] gltf_file_path = pathlib.Path(folder, f'{name}.gltf') exporter = vtk.vtkGLTFExporter() exporter.SaveNormalOn() exporter.InlineDataOn() exporter.SetFileName(gltf_file_path.as_posix()) exporter.SetActiveRenderer(renderer) exporter.SetRenderWindow(window) exporter.Modified() exporter.Write() return gltf_file_path.as_posix() @staticmethod def _get_image_writer(image_type: ImageTypes) -> None: """Get vtk image writer for each image type.""" if image_type == ImageTypes.png: writer = vtk.vtkPNGWriter() elif image_type == ImageTypes.bmp: writer = vtk.vtkBMPWriter() elif image_type == ImageTypes.jpg: writer = vtk.vtkJPEGWriter() elif image_type == ImageTypes.pnm: writer = vtk.vtkPNMWriter() elif image_type == ImageTypes.ps: writer = vtk.vtkPostScriptWriter() elif image_type == ImageTypes.tiff: writer = vtk.vtkTIFFWriter() else: raise ValueError(f'Invalid image type: {image_type}') return writer
[docs] def auto_image_dimension(self, image_width: int = 0, image_height: int = 0) -> Tuple[int]: """Calculate image dimension. If image width and image height are not specified by the user, Camera's x and y dimension are used instead. Note that a Camera object gets x and y dimension from its parent Radiance View object. Args: image_width: Image width in pixels set by the user. Defaults to 0, which will use view's x dimension. image_height: Image height in pixels set by the user. Defaults to 0, which will use view's y dimension. Returns: A tuple with two elements - image_width: Image width in pixels. - image_height: Image height in pixels. """ dim_x, dim_y = self._camera.dimension_x_y() if not image_width: image_width = dim_x if not image_height: image_height = dim_y return image_width, image_height
[docs] def auto_text_height(self, image_width, image_height) -> None: """Calculate text height based on image dimension. If a user has not specified the text height in legend parameters, it will be calculated based on the average of image width and image height. 2.5% of this average is used as the text height. Args: image_width: Image width in pixels set by the user. image_height: Image height in pixels set by the user. """ text_size = round(((image_width + image_height) / 2) * 0.025) for legend_param in self._legend_params: if legend_param.label_parameters.size == 0: legend_param.label_parameters.size = text_size if legend_param.title_parameters.size == 0: legend_param.title_parameters.size = text_size
def _export_image( self, folder: str, image_type: ImageTypes = ImageTypes.png, *, image_scale: int = 1, image_width: int = 0, image_height: int = 0, image_name: str = None, color_range: vtk.vtkLookupTable = None, rgba: bool = False, show: bool = False) -> str: """Export the window as an image. Reference: https://kitware.github.io/vtk-examples/site/Python/IO/ImageWriter/ This method is able to export an image in '.png', '.jpg', '.ps', '.tiff', '.bmp', and '.pnm' formats. Args: folder: A valid path to where you'd like to write the image. image_type: An ImageType object. image_scale: An integer value as a scale factor. Defaults to 1. image_width: An integer value that sets the width of image in pixels. Defaults to 0, which will use view's x dimension. image_height: An integer value that sets the height of image in pixels. Defaults to 0, which will use view's y dimension. image_name: A text string that sets the name of the image. Defaults to None. color_range: A vtk lookup table object which can be obtained from the color_range method of the DataFieldInfo object. Defaults to None. rgba: A boolean value to set the type of buffer. A True value sets an the background color to black. A False value uses the Scene object's background color. Defaults to False. show: A boolean value to decide if the the render window should pop up. Defaults to False. legends: Legends to add to the image. Returns: A text string representing the path to the image. """ # Set image width and height image_width, image_height = self.auto_image_dimension(image_width, image_height) # Set text height for the labels and the title of the legend self.auto_text_height(image_width, image_height) # Create a render window interactor, window = self._create_window()[:2] # render window if not show: window.OffScreenRenderingOn() window.SetSize(image_width, image_height) window.Render() else: window.SetSize(image_width, image_height) window.Render() interactor.Initialize() interactor.Start() if not image_name: image_path = pathlib.Path( folder, f'{self._camera.identifier}.{image_type.value}') else: image_path = pathlib.Path( folder, f'{image_name}_{self._camera.identifier}.{image_type.value}') writer = self._get_image_writer(image_type) if image_type == ImageTypes.jpg: writer.SetQuality(100) # image quality window_to_image_filter = vtk.vtkWindowToImageFilter() window_to_image_filter.SetInput(window) window_to_image_filter.SetScale(image_scale) # rgba is not supported for postscript image type if rgba and image_type != ImageTypes.ps: window_to_image_filter.SetInputBufferTypeToRGBA() else: window_to_image_filter.SetInputBufferTypeToRGB() # Read from the front buffer. window_to_image_filter.ReadFrontBufferOff() window_to_image_filter.Update() writer.SetFileName(image_path.as_posix()) writer.SetInputConnection(window_to_image_filter.GetOutputPort()) writer.Write() return image_path.as_posix()