"""
In our FEA model viewer, faces, edges and vertices are shown by separate actors
because face edges are a subset of face-cell edges, and because we want fully
independent control over their visibility and appearance. Moreover we want to
be able to overlay regular actors by actors for highlighted and selected parts
to avoid having to extract the selected/highlighted parts from the data set
shown by the corresponding regular actor.

For example, a selected face is shown by overlaying it with an actor showing
semitransparent white face cells of the selected faces, and selected edges and
vertices are shown by overlaying the regular edges and vertices by thicker
opaque red edges and yellow vertices.

We use coincident topology resolution (CTR) parameters to control the drawing
order of vertices/edges/faces relative to each other, and of
highlighted/selected vs regular parts. To test and demonstrate the coincident
topology behavior this script shows a large semitransparent working plane in
the X-Y plane and a cube with rib length 1. With respect to the chosen
viewpoint, the top face and the left edge are shown as selected, and the
top-right or both right-edge nodes are shown as selected.

Several scenarios of CTR can be demonstrated:
1. CTR off
2. CRT on with default common offset parameters
3. CRT on with no common offset parameters but only relative parameters
4. CRT with default+relative parameters combined
All of these are also shown with the camera using parallel projection.

Repeatedly pressing the space-bar in the viewer window executes the defined
scenarios.

Observe how the overlay of the selected face is not shown unless relative
parameters are used to bring the selected face forward.

Observe how the default parameters cause the edges to be lifted 'upwards' off
the corresponding surfaces.

Note: this script was developed to test the coincident topology behavior which
changed from 8.1.1 onwards, esp. to address/revert the scaling of offsets by
actor diagonal length.

Sometime past 8.1.1
(https://gitlab.kitware.com/vtk/vtk/-/commit/3052137e6621684f9184b293b82c677e976c2f89)
the processing of CTR parameters changed: Of the two parameters factor and unit
now only the unit value is used, and it is multiplied by the diagonal length of
the actor. For our purposes this meant that related actors (e.g.
selected/unselected edges) with different bounding boxes would have to
recompute their offsets. Also a single vertex could not be offset at all
because it has a zero diagonal length.

For 9.1.0, the scaling of the offset with the actor's diagonal has been reverted
(https://gitlab.kitware.com/vtk/vtk/-/commit/747e7f9056dfc4a1e2dfcc566dd0481033552cbc)

"""

import vtk

# The working plane : size 40x40 in the X-Y plane
wp_size=40
nx=10
ny=20

# working plane
wp_plane=vtk.vtkPlaneSource()
wp_plane.SetOrigin(-wp_size/2,-wp_size/2,0)
wp_plane.SetPoint1(wp_size/2,-wp_size/2,0)
wp_plane.SetPoint2(-wp_size/2,+wp_size/2,0)
wp_plane.SetXResolution(2*nx)
wp_plane.SetYResolution(2*ny)

wp_plane_mapper=vtk.vtkPolyDataMapper()
wp_plane_mapper.SetInputConnection(wp_plane.GetOutputPort())
wp_plane_actor=vtk.vtkActor()
wp_plane_actor.SetMapper(wp_plane_mapper)
wp_plane_actor.GetProperty().SetColor( 0,1,0 )
wp_plane_actor.GetProperty().SetOpacity( 0.6 )

# working plane grid lines
wp_grid_lines=vtk.vtkExtractEdges()
wp_grid_lines.SetInputConnection(wp_plane.GetOutputPort())

wp_grid_mapper=vtk.vtkPolyDataMapper()
wp_grid_mapper.SetInputConnection(wp_grid_lines.GetOutputPort())
wp_grid_actor=vtk.vtkActor()
wp_grid_actor.SetMapper(wp_grid_mapper)
wp_grid_actor.GetProperty().SetColor( .6,.6,.6 )
wp_grid_actor.GetProperty().SetOpacity( 1.0 )

# working plane grid vertices
wp_points = vtk.vtkVertexGlyphFilter()
wp_points.SetInputConnection(wp_plane.GetOutputPort())
wp_points_mapper=vtk.vtkPolyDataMapper()
wp_points_mapper.SetInputConnection(wp_points.GetOutputPort())
wp_points_actor=vtk.vtkActor()
wp_points_actor.SetMapper(wp_points_mapper)
wp_points_actor.GetProperty().SetColor( 1,1,1 )
wp_points_actor.GetProperty().SetPointSize( 3 )

# The model: a blue 1x1x1 cube with its center at the origin
cube=vtk.vtkCubeSource()
cube_mapper=vtk.vtkPolyDataMapper()
cube_mapper.SetInputConnection(cube.GetOutputPort())
cube_actor=vtk.vtkActor()
cube_actor.SetMapper(cube_mapper)
cube_actor.GetProperty().SetColor(0,0,1)

# The cube has unconnected faces. Reconnect for
# extraction of edges and vertices
cube_merged = vtk.vtkGeometryFilter()
cube_merged.MergingOn()
cube_merged.SetInputConnection(cube.GetOutputPort())

# Edges of the cube
cube_edges = vtk.vtkFeatureEdges()
cube_edges.ColoringOff()
cube_edges.SetInputConnection(cube_merged.GetOutputPort())
cube_edges_mapper=vtk.vtkPolyDataMapper()
cube_edges_mapper.SetInputConnection(cube_edges.GetOutputPort())
cube_edges_actor=vtk.vtkActor()
cube_edges_actor.SetMapper(cube_edges_mapper)
cube_edges_actor.GetProperty().SetColor(.5,1,.5)
cube_edges_actor.GetProperty().SetLineWidth(2)
cube_edges_actor.GetProperty().SetAmbient(1)
cube_edges_actor.GetProperty().SetDiffuse(0)
cube_edges_actor.GetProperty().SetSpecular(0)

# Vertices of the cube
cube_vertices=vtk.vtkVertexGlyphFilter()
cube_vertices.SetInputConnection(cube_merged.GetOutputPort())
cube_vertices_mapper=vtk.vtkPolyDataMapper()
cube_vertices_mapper.SetInputConnection(cube_vertices.GetOutputPort())
cube_vertices_actor=vtk.vtkActor()
cube_vertices_actor.SetMapper(cube_vertices_mapper)
cube_vertices_actor.GetProperty().SetColor(0,0,0)
cube_vertices_actor.GetProperty().SetPointSize(4)
cube_vertices_actor.GetProperty().SetAmbient(1)
cube_vertices_actor.GetProperty().SetDiffuse(0)
cube_vertices_actor.GetProperty().SetSpecular(0)

# Selected faces shown by overlaying with transparent white face cells
selected_face = vtk.vtkGeometryFilter()
selected_face.SetInputConnection(cube_merged.GetOutputPort())
selected_face.CellClippingOn()
selected_face.SetCellMinimum(-1) # nothing selected initially
selected_face.SetCellMaximum(-1)
selected_face_mapper=vtk.vtkPolyDataMapper()
selected_face_mapper.SetInputConnection(selected_face.GetOutputPort())
selected_face_actor=vtk.vtkActor()
selected_face_actor.SetMapper(selected_face_mapper)
selected_face_actor.GetProperty().SetColor(1,1,1)
selected_face_actor.GetProperty().SetOpacity(0.7)

# Selected edges shown by overlaying with thicker solid red edge cells
selected_edge = vtk.vtkGeometryFilter()
selected_edge.SetInputConnection(cube_edges.GetOutputPort())
selected_edge.CellClippingOn()
selected_edge.SetCellMinimum(-1) # nothing selected initially
selected_edge.SetCellMaximum(-1)
selected_edge_mapper=vtk.vtkPolyDataMapper()
selected_edge_mapper.SetInputConnection(selected_edge.GetOutputPort())
selected_edge_actor=vtk.vtkActor()
selected_edge_actor.SetMapper(selected_edge_mapper)
selected_edge_actor.GetProperty().SetColor(1,0,0)
selected_edge_actor.GetProperty().SetLineWidth(4)
selected_edge_actor.GetProperty().SetAmbient(1)
selected_edge_actor.GetProperty().SetDiffuse(0)
selected_edge_actor.GetProperty().SetSpecular(0)

# Selected vertices shown by overlaying with thicker solid yellow vertex cells
selected_vertex = vtk.vtkGeometryFilter()
selected_vertex.SetInputConnection(cube_vertices.GetOutputPort())
selected_vertex.CellClippingOn()
selected_vertex.SetCellMinimum(-1) # nothing selected initially
selected_vertex.SetCellMaximum(-1)
selected_vertex_mapper=vtk.vtkPolyDataMapper()
selected_vertex_mapper.SetInputConnection(selected_vertex.GetOutputPort())
selected_vertex_actor=vtk.vtkActor()
selected_vertex_actor.SetMapper(selected_vertex_mapper)
selected_vertex_actor.GetProperty().SetColor(1,1,0)
selected_vertex_actor.GetProperty().SetPointSize(8)
selected_vertex_actor.GetProperty().SetAmbient(1)
selected_vertex_actor.GetProperty().SetDiffuse(0)
selected_vertex_actor.GetProperty().SetSpecular(0)

wp_actors=[ wp_plane_actor, wp_grid_actor, wp_points_actor ]
cube_actors= [cube_actor, cube_edges_actor, cube_vertices_actor ]
selected_actors = [ selected_face_actor, selected_edge_actor, selected_vertex_actor ]

r = vtk.vtkRenderer()
for actor in wp_actors + cube_actors + selected_actors:
    r.AddViewProp(actor)

# An actor for displaying the case label
label=vtk.vtkTextActor()
label.SetPosition(20,20)
label.SetPosition2(1,1)
r.AddViewProp(label)

rw=vtk.vtkRenderWindow()
rw.SetSize(800,800)
rw.AddRenderer(r)

# A zoomed-in position
r.GetActiveCamera().SetViewUp(0,0,1)
r.GetActiveCamera().SetFocalPoint(0,0,0)
r.GetActiveCamera().SetPosition(3,3,3)

#
# Functions that are used to control the execution
# of the scenarios
#

# Functions to select top-right vertex or both right-edge vertices

def selectSingleVertex():
    """select cell #3 : top right vertex"""
    # to demonstrate scaling-by-actor-length issue
    selected_vertex.SetCellMinimum(3)
    selected_vertex.SetCellMaximum(3)

def selectEdgeVertices():
    """select cells #2 and #3 : right edge vertices"""
    selected_vertex.SetCellMinimum(2)
    selected_vertex.SetCellMaximum(3)

def selectBoundingBoxVertices():
    """select #3 and #4 : top-right and bottom-left vertices"""
    selected_vertex.SetCellMinimum(3)
    selected_vertex.SetCellMaximum(4)

def parallelProjection():
    r.GetActiveCamera().SetParallelProjection(1)

def perspectiveProjection():
    r.GetActiveCamera().SetParallelProjection(0)

def setRelativeParameters(multiplier):
    """
    Set relative parameters such that:
    1. working plane is further back than anything else
    2. selected parts are brought closer than unselected parts
    Common parameters are used to move polygons/lines/points relative to each other

    The argument is a function that computes a
    possibly actor-dependent multiplication factor
    """
    wp_plane_mapper.SetRelativeCoincidentTopologyPolygonOffsetParameters( 0., 2.*multiplier(wp_plane_actor) )
    wp_grid_mapper.SetRelativeCoincidentTopologyLineOffsetParameters    ( 0., 2.*multiplier(wp_grid_actor) )
    wp_points_mapper.SetRelativeCoincidentTopologyPointOffsetParameter  (     2.*multiplier(wp_points_actor) )

    cube_mapper.SetRelativeCoincidentTopologyPolygonOffsetParameters      ( 0.,  0.*multiplier(cube_actor) )
    cube_edges_mapper.SetRelativeCoincidentTopologyLineOffsetParameters   ( 0.,  0.*multiplier(cube_edges_actor) )
    cube_vertices_mapper.SetRelativeCoincidentTopologyPointOffsetParameter(      0.*multiplier(cube_vertices_actor) )

    selected_face_mapper.SetRelativeCoincidentTopologyPolygonOffsetParameters( 0., -2.*multiplier(selected_face_actor) )
    selected_edge_mapper.SetRelativeCoincidentTopologyLineOffsetParameters   ( 0., -2.*multiplier(selected_edge_actor) )
    selected_vertex_mapper.SetRelativeCoincidentTopologyPointOffsetParameter (     -2.*multiplier(selected_vertex_actor) )

def scaleToCube(actor):
    """
    multiplier for setCoincidentTopologyRelativeParameters :
    use cube actor as length reference instead of
    actor length
    """
    # to address scaling-by-actor-length issue
    cube_length=cube_actor.GetLength()
    if actor.GetLength() == 0.:
        return 1
    return cube_length/actor.GetLength()

def constant(actor):
    """
    multiplier for setCoincidentTopologyRelativeParameters :
    actor-independent multiplier
    """
    return 1

def zero(actor):
    """
    multiplier for setCoincidentTopologyRelativeParameters :
    set to zero : no relative parameters are used
    """
    return 0


def setCoincidentTopology(polygon,line,point):
    vtk.vtkMapper.SetResolveCoincidentTopologyToPolygonOffset()
    vtk.vtkMapper.SetResolveCoincidentTopologyPolygonOffsetParameters(0.,polygon)
    vtk.vtkMapper.SetResolveCoincidentTopologyLineOffsetParameters(0., line)
    vtk.vtkMapper.SetResolveCoincidentTopologyPointOffsetParameter(point)

def coincidentTopologyOff():
    vtk.vtkMapper.SetResolveCoincidentTopologyToOff()

def commonParametersDefault():
    """
    By default lines are moved forward 4 units and points 8 units
    And no relative parameters are used
    """
    setCoincidentTopology(0,-4.,-8.)


def commonParametersOff():
    setCoincidentTopology(0,0,0)


def relativeParametersOff():
    setRelativeParameters(zero)


def relativeParametersConstant():
    setRelativeParameters(constant)


def relativeParametersScaled():
    """
    To compensate for actors with different bounds.
    Note: it is also necessary to reset the common parameters to zero because
    they have a stronger effect on larger actors
    Using relative parameters scaled by actor size relative to 'model' (cube) size
    allows selections to be brought forward properly and avoids excessive offsetting
    of working plane actors.
    """
    # to address scaling-by-actor-length issue
    setRelativeParameters(scaleToCube)


class IterateThroughCases:
    """
    An event observer that responds to pressing the space bar by
    showing the next case from the list of cases

    a case is a label to show in the text actor and a list of
    functions to execute before rendering, to configure the view
    """
    def __init__(self):
        self.cases=list()
        self.index=0

    def __call__(self,obj,ev):
        if obj.GetKeySym() != 'space':
            return
        case=self.cases[self.index]
        print(f'Case[{self.index}]: {case[0]}')
        label.SetInput(case[0])
        for func in case[1]:
          func()

        selected_edge.SetCellMinimum(7) # select left edge
        selected_edge.SetCellMaximum(7)
        selected_face.SetCellMinimum(4) # select top face
        selected_face.SetCellMaximum(5)

        self.index = (self.index+1) % len(self.cases)
        obj.Render()


iterate=IterateThroughCases()
iterate.cases = [
                 ('off',                     [perspectiveProjection,selectEdgeVertices,
                                              coincidentTopologyOff]),
                 ('default',                 [commonParametersDefault,relativeParametersOff]),
                 ('relative',                [commonParametersOff,relativeParametersConstant]),
                 ('default+relative',        [commonParametersDefault,relativeParametersConstant]),
                 #('scaled',                  [commonParametersOff,relativeParametersScaled]),
                 ('off (parallel)',          [parallelProjection,selectEdgeVertices,
                                              coincidentTopologyOff]),
                 ('default (parallel)',                 [commonParametersDefault,relativeParametersOff]),
                 ('relative (parallel)',                [commonParametersOff,relativeParametersConstant]),
                 ('default+relative (parallel)',        [commonParametersDefault,relativeParametersConstant]),
                 #('scaled (parallel)',                  [commonParametersOff,relativeParametersScaled]),
                 ]

rwi = vtk.vtkRenderWindowInteractor()
rwi.SetRenderWindow(rw)
iterate(rwi,'KeyPressEvent')
rwi.AddObserver('KeyPressEvent',iterate)
rwi.Start()
