Source code for meshmagick.MMviewer

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
This module implements a viewer based on VTK.
"""

import vtk
from os import getcwd
from datetime import datetime

__year__ = datetime.now().year

# TODO: See links below for interactive update of actors
# https://stackoverflow.com/questions/31075569/vtk-rotate-actor-programmatically-while-vtkrenderwindowinteractor-is-active
# https://stackoverflow.com/questions/32417197/vtk-update-vtkpolydata-in-renderwindow-without-blocking-interaction


[docs]class MMViewer: """This class implements a viewer based on VTK""" def __init__(self): # Building renderer self.renderer = vtk.vtkRenderer() self.renderer.SetBackground(0.7706, 0.8165, 1.0) # Building render window self.render_window = vtk.vtkRenderWindow() # self.render_window.FullScreenOn() # BUGFIX: It causes the window to fail to open... self.render_window.SetSize(1024, 768) self.render_window.SetWindowName("Meshmagick viewer") self.render_window.AddRenderer(self.renderer) # Building interactor self.render_window_interactor = vtk.vtkRenderWindowInteractor() self.render_window_interactor.SetRenderWindow(self.render_window) self.render_window_interactor.GetInteractorStyle().SetCurrentStyleToTrackballCamera() self.render_window_interactor.AddObserver('KeyPressEvent', self.on_key_press, 0.0) # Building axes view axes = vtk.vtkAxesActor() widget = vtk.vtkOrientationMarkerWidget() widget.SetOrientationMarker(axes) self.widget = widget self.widget.SetInteractor(self.render_window_interactor) self.widget.SetEnabled(1) self.widget.InteractiveOn() # Building command annotations command_text = "left mouse : rotate\n" + \ "right mouse : zoom\n" + \ "middle mouse : pan\n" + \ "ctrl+left mouse : spin\n" + \ "n : (un)show normals\n" + \ "b : (un)show axes box\n" + \ "f : focus on the mouse cursor\n" + \ "r : reset view\n" + \ "s : surface representation\n" + \ "w : wire representation\n" + \ "h : (un)show Oxy plane\n" + \ "g : (un)show Axes planes\n" + \ "x : save\n" + \ "c : screenshot\n" + \ "q : quit" corner_annotation = vtk.vtkCornerAnnotation() corner_annotation.SetLinearFontScaleFactor(2) corner_annotation.SetNonlinearFontScaleFactor(1) corner_annotation.SetMaximumFontSize(20) corner_annotation.SetText(3, command_text) corner_annotation.GetTextProperty().SetColor(0., 0., 0.) self.renderer.AddViewProp(corner_annotation) copyright_text = "Meshmagick Viewer\nCopyright 2014-%u, Ecole Centrale de Nantes" % __year__ copyright_annotation = vtk.vtkCornerAnnotation() copyright_annotation.SetLinearFontScaleFactor(1) copyright_annotation.SetNonlinearFontScaleFactor(1) copyright_annotation.SetMaximumFontSize(12) copyright_annotation.SetText(1, copyright_text) copyright_annotation.GetTextProperty().SetColor(0., 0., 0.) self.renderer.AddViewProp(copyright_annotation) self.normals = [] self.axes = [] self.oxy_plane = None self.axes_planes = None self.polydatas = list() self.hiden = dict() # TODO: A terminer -> cf methode self.hide()
[docs] def normals_on(self): """Displays the normals""" self.normals = True
[docs] def normals_off(self): """Hide the normals""" self.normals = False
[docs] def plane_on(self): """Displays the Oxy plane""" pd = self.polydatas[0] (xmin, xmax, ymin, ymax, _, _) = pd.GetBounds() dx = 0.1 * (xmax - xmin) dy = 0.1 * (ymax - ymin) plane = vtk.vtkPlaneSource() plane.SetOrigin(xmin - dx, ymax + dy, 0) plane.SetPoint1(xmin - dx, ymin - dy, 0) plane.SetPoint2(xmax + dx, ymax + dy, 0) plane.Update() polydata = plane.GetOutput() mapper = vtk.vtkPolyDataMapper() if vtk.VTK_MAJOR_VERSION <= 5: mapper.SetInput(polydata) else: mapper.SetInputData(polydata) actor = vtk.vtkActor() actor.SetMapper(mapper) color = [0., 102. / 255, 204. / 255] actor.GetProperty().SetColor(color) actor.GetProperty().SetEdgeColor(0, 0, 0) actor.GetProperty().SetLineWidth(1) self.renderer.AddActor(actor) self.renderer.Modified() self.oxy_plane = actor
[docs] def axes_planes_on(self): """Displays the Oxy plane""" pd = self.polydatas[0] (xmin, xmax, ymin, ymax, zmin, zmax) = pd.GetBounds() dx = 0.1 * (xmax - xmin) dy = 0.1 * (ymax - ymin) dz = 0.1 * (zmax - zmin) plane_oxy = vtk.vtkPlaneSource() plane_oxz = vtk.vtkPlaneSource() plane_oyz = vtk.vtkPlaneSource() plane_oxy.SetOrigin(xmin - dx, ymax + dy, 0) plane_oxy.SetPoint1(xmin - dx, ymin - dy, 0) plane_oxy.SetPoint2(xmax + dx, ymax + dy, 0) plane_oxy.Update() polydata_oxy = plane_oxy.GetOutput() plane_oxz.SetOrigin(xmin - dx, 0, zmax + dz) plane_oxz.SetPoint1(xmin - dx, 0, zmin - dz) plane_oxz.SetPoint2(xmax + dx, 0, zmax + dz) plane_oxz.Update() polydata_oxz = plane_oxz.GetOutput() plane_oyz.SetOrigin(0, ymin - dy, zmax + dz) plane_oyz.SetPoint1(0, ymin - dy, zmin - dz) plane_oyz.SetPoint2(0, ymax + dy, zmax + dz) plane_oyz.Update() polydata_oyz = plane_oyz.GetOutput() mapper_oxy = vtk.vtkPolyDataMapper() mapper_oxz = vtk.vtkPolyDataMapper() mapper_oyz = vtk.vtkPolyDataMapper() if vtk.VTK_MAJOR_VERSION <= 5: mapper_oxy.SetInput(polydata_oxy) mapper_oxz.SetInput(polydata_oxz) mapper_oyz.SetInput(polydata_oyz) else: mapper_oxy.SetInputData(polydata_oxy) mapper_oxz.SetInputData(polydata_oxz) mapper_oyz.SetInputData(polydata_oyz) actor_oxy = vtk.vtkActor() actor_oxy.SetMapper(mapper_oxy) actor_oxy.GetProperty().SetColor(0, 0, 255) actor_oxy.GetProperty().SetEdgeColor(0, 0, 0) actor_oxy.GetProperty().SetLineWidth(1) actor_oxz = vtk.vtkActor() actor_oxz.SetMapper(mapper_oxz) actor_oxz.GetProperty().SetColor(0, 255, 0) actor_oxz.GetProperty().SetEdgeColor(0, 0, 0) actor_oxz.GetProperty().SetLineWidth(1) actor_oyz = vtk.vtkActor() actor_oyz.SetMapper(mapper_oyz) actor_oxz.GetProperty().SetColor(255, 0, 0) actor_oyz.GetProperty().SetEdgeColor(0, 0, 0) actor_oyz.GetProperty().SetLineWidth(1) self.renderer.AddActor(actor_oxy) self.renderer.Modified() self.renderer.AddActor(actor_oxz) self.renderer.Modified() self.renderer.AddActor(actor_oyz) self.renderer.Modified() self.axes_planes = [actor_oxy, actor_oxz, actor_oyz]
[docs] def add_point(self, pos, color=(0, 0, 0)): """Add a point to the viewer Parameters ---------- pos : array_like The point's position color : array_like, optional The RGB color required for the point. Default is (0, 0, 0) corresponding to black. Returns ------- vtkPolyData """ assert len(pos) == 3 p = vtk.vtkPoints() v = vtk.vtkCellArray() i = p.InsertNextPoint(pos) v.InsertNextCell(1) v.InsertCellPoint(i) pd = vtk.vtkPolyData() pd.SetPoints(p) pd.SetVerts(v) self.add_polydata(pd, color=color) return pd
[docs] def add_line(self, p0, p1, color=(0, 0, 0)): """Add a line to the viewer Parameters ---------- p0 : array_like position of one end point of the line p1 : array_like position of a second end point of the line color : array_like, optional RGB color of the line. Default is black (0, 0, 0) Returns ------- vtkPolyData """ assert len(p0) == 3 and len(p1) == 3 points = vtk.vtkPoints() points.InsertNextPoint(p0) points.InsertNextPoint(p1) line = vtk.vtkLine() line.GetPointIds().SetId(0, 0) line.GetPointIds().SetId(1, 1) lines = vtk.vtkCellArray() lines.InsertNextCell(line) lines_pd = vtk.vtkPolyData() lines_pd.SetPoints(points) lines_pd.SetLines(lines) self.add_polydata(lines_pd, color=color) return lines_pd
[docs] def add_vector(self, point, value, scale=1, color=(0, 0, 0)): """Add a vector to the viewer Parameters ---------- point : array_like starting point position of the vector value : float the magnitude of the vector scale : float, optional the scaling to apply to the vector for better visualization. Default is 1. color : array_like The color of the vector. Default is black (0, 0, 0) """ points = vtk.vtkPoints() idx = points.InsertNextPoint(point) vert = vtk.vtkCellArray() vert.InsertNextCell(1) vert.InsertCellPoint(idx) pd_point = vtk.vtkPolyData() pd_point.SetPoints(points) pd_point.SetVerts(vert) arrow = vtk.vtkArrowSource() arrow.SetTipResolution(16) arrow.SetTipLength(0.1) arrow.SetTipRadius(0.02) arrow.SetShaftRadius(0.005) vec = vtk.vtkFloatArray() vec.SetNumberOfComponents(3) v0, v1, v2 = value / scale vec.InsertTuple3(idx, v0, v1, v2) pd_point.GetPointData().SetVectors(vec) g_glyph = vtk.vtkGlyph3D() # g_glyph.SetScaleModeToDataScalingOff() g_glyph.SetVectorModeToUseVector() g_glyph.SetInputData(pd_point) g_glyph.SetSourceConnection(arrow.GetOutputPort()) g_glyph.SetScaleModeToScaleByVector() # g_glyph.SetScaleFactor(10) g_glyph.ScalingOn() g_glyph.Update() g_glyph_mapper = vtk.vtkPolyDataMapper() g_glyph_mapper.SetInputConnection(g_glyph.GetOutputPort()) g_glyph_actor = vtk.vtkActor() g_glyph_actor.SetMapper(g_glyph_mapper) g_glyph_actor.GetProperty().SetColor(color) self.renderer.AddActor(g_glyph_actor)
[docs] def add_plane(self, center, normal): """Add a plane to the viewer Parameters ---------- center : array_like The origin of the plane normal : array_like The normal of the plane """ plane = vtk.vtkPlaneSource() plane.SetCenter(center) plane.SetNormal(normal) mapper = vtk.vtkPolyDataMapper() if vtk.VTK_MAJOR_VERSION <= 5: mapper.SetInput(plane.GetOutput()) else: mapper.SetInputData(plane.GetOutput())
# FIXME: terminer l'implementation et l'utiliser pour le plan de la surface libre
[docs] def add_polydata(self, polydata, color=(1, 1, 0), representation='surface'): """Add a polydata object to the viewer Parameters ---------- polydata : vtkPolyData the object to be added color : array_like, optional the color of the object. Default is yellow (1, 1, 0) representation : str the representation mode of the object ('surface' or 'wireframe'). Default is 'surface'. """ assert isinstance(polydata, vtk.vtkPolyData) assert representation in ('surface', 'wireframe') self.polydatas.append(polydata) # Building mapper mapper = vtk.vtkPolyDataMapper() if vtk.VTK_MAJOR_VERSION <= 5: mapper.SetInput(polydata) else: mapper.SetInputData(polydata) # Building actor actor = vtk.vtkActor() actor.SetMapper(mapper) # Properties setting actor.GetProperty().SetColor(color) actor.GetProperty().EdgeVisibilityOn() actor.GetProperty().SetEdgeColor(0, 0, 0) actor.GetProperty().SetLineWidth(1) actor.GetProperty().SetPointSize(10) if representation == 'wireframe': actor.GetProperty().SetRepresentationToWireframe() self.renderer.AddActor(actor) self.renderer.Modified()
[docs] def show_normals(self): """Shows the normals of the current objects""" for polydata in self.polydatas: normals = vtk.vtkPolyDataNormals() normals.ConsistencyOff() # normals.ComputePointNormalsOn() normals.ComputeCellNormalsOn() if vtk.VTK_MAJOR_VERSION <= 5: normals.SetInput(polydata) else: normals.SetInputData(polydata) normals.Update() normals_at_centers = vtk.vtkCellCenters() normals_at_centers.SetInputConnection(normals.GetOutputPort()) normals_mapper = vtk.vtkPolyDataMapper() if vtk.VTK_MAJOR_VERSION <= 5: normals_output = normals.GetOutput() normals_mapper.SetInput(normals_output) else: normals_mapper.SetInputData(normals.GetOutput()) normals_actor = vtk.vtkActor() normals_actor.SetMapper(normals_mapper) arrows = vtk.vtkArrowSource() arrows.SetTipResolution(16) arrows.SetTipLength(0.5) arrows.SetTipRadius(0.1) glyph = vtk.vtkGlyph3D() glyph.SetSourceConnection(arrows.GetOutputPort()) glyph.SetInputConnection(normals_at_centers.GetOutputPort()) glyph.SetVectorModeToUseNormal() glyph.SetScaleFactor(1) # FIXME: may be too big ... # glyph.SetVectorModeToUseNormal() # glyph.SetVectorModeToUseVector() # glyph.SetScaleModeToDataScalingOff() glyph.Update() glyph_mapper = vtk.vtkPolyDataMapper() glyph_mapper.SetInputConnection(glyph.GetOutputPort()) glyph_actor = vtk.vtkActor() glyph_actor.SetMapper(glyph_mapper) self.renderer.AddActor(glyph_actor) self.normals.append(glyph_actor)
[docs] def show_axes(self): """Shows the axes around the main object""" tprop = vtk.vtkTextProperty() tprop.SetColor(0., 0., 0.) tprop.ShadowOn() axes = vtk.vtkCubeAxesActor2D() if vtk.VTK_MAJOR_VERSION <= 5: axes.SetInput(self.polydatas[0]) else: axes.SetInputData(self.polydatas[0]) axes.SetCamera(self.renderer.GetActiveCamera()) axes.SetLabelFormat("%6.4g") axes.SetFlyModeToOuterEdges() axes.SetFontFactor(0.8) axes.SetAxisTitleTextProperty(tprop) axes.SetAxisLabelTextProperty(tprop) # axes.DrawGridLinesOn() self.renderer.AddViewProp(axes) self.axes.append(axes)
[docs] def show(self): """Show the viewer""" self.renderer.ResetCamera() self.render_window.Render() self.render_window_interactor.Start()
# self.render_window_interactor.Initialize()
[docs] def hide(self, index): if index > len(self.polydatas): print(("No mesh with index %u" % index)) return self.hiden
[docs] def save(self): """Saves the main object in a 'mmviewer_save.vtp' vtp file is the current folder""" from vtk import vtkXMLPolyDataWriter writer = vtkXMLPolyDataWriter() writer.SetDataModeToAscii() writer.SetFileName('mmviewer_save.vtp') for polydata in self.polydatas: if vtk.VTK_MAJOR_VERSION <= 5: writer.SetInput(polydata) else: writer.SetInputData(polydata) writer.Write() print(("File 'mmviewer_save.vtp' written in %s" % getcwd())) return
[docs] def screenshot(self): """Saves a screeshot of the current window in a file screenshot.png""" w2if = vtk.vtkWindowToImageFilter() w2if.SetInput(self.render_window) w2if.Update() writer = vtk.vtkPNGWriter() writer.SetFileName("screenshot.png") if vtk.VTK_MAJOR_VERSION <= 5: writer.SetInput(w2if.GetOutput()) else: writer.SetInputData(w2if.GetOutput()) writer.Write() print(("File 'screenshot.png' written in %s" % getcwd())) return
[docs] def finalize(self): """Cleanly close the viewer""" del self.render_window del self.render_window_interactor
[docs] def on_key_press(self, obj, event): """Event trig at keystroke""" key = obj.GetKeySym() if key == 'n': if self.normals: # self.normals = False for actor in self.normals: self.renderer.RemoveActor(actor) self.renderer.Render() self.normals = [] else: self.show_normals() self.renderer.Render() elif key == 'b': if self.axes: for axis in self.axes: self.renderer.RemoveActor(axis) self.axes = [] else: self.show_axes() elif key == 'e' or key == 'q': self.render_window_interactor.GetRenderWindow().Finalize() self.render_window_interactor.TerminateApp() elif key == 'x': self.save() elif key == 'c': # pass self.screenshot() elif key == 'h': if self.oxy_plane: self.renderer.RemoveActor(self.oxy_plane) self.oxy_plane = None else: self.plane_on() elif key == 'g': if self.axes_planes: for actor in self.axes_planes: self.renderer.RemoveActor(actor) self.axes_planes = None else: self.axes_planes_on()