text: from enum import Enum
  import weakref
  
  import matplotlib.pyplot as plt
  import numpy as np
  
  from mm2d.core.spatial import Transform
  from mm2d.core.shapes import Circle, Polygon, Segment
  
  # TODO rendering is based on matplotlib, but that dependency should be
  # contained to the rendering code only (i.e., writing other renderers should be
  # straightforward)
  
  
  class MarkerType(Enum):
      POINT = 0
      PATH = 1
  
  
  class MarkerRenderer:
      def __init__(self, axes, body, transform, **render_params):
          self.body = body
          self.transform = transform
          (self.plot,) = axes.plot([], [], **render_params)
  
      def position(self):
          return self.transform @ self.body.state().pose.position
  
  
  class MarkerPointRenderer(MarkerRenderer):
      def __init__(self, axes, body, transform, **render_params):
          super().__init__(axes, body, transform, **render_params)
  
      def render(self):
          """Initial render."""
          p = self.position()
          self.plot.set_xdata(p[0])
          self.plot.set_ydata(p[1])
  
  
  class MarkerPathRenderer(MarkerRenderer):
      """A patch that also records the path of a `Marker` over time."""
  
      def __init__(self, axes, body, transform, **render_args):
          super().__init__(axes, body, transform, **render_args)
          self.xs = []
          self.ys = []
  
      def render(self):
          """Update the render."""
          p = self.pose().position
  
          self.xs.append(p[0])
          self.ys.append(p[1])
  
          self.plot.set_xdata(self.xs)
          self.plot.set_ydata(self.ys)
  
  
  class ShapeRenderer:
      def __init__(self, body, shape):
          self.body = body
          self.shape = shape
  
          # Shapes can have a dict of render_params added to control their
          # appearance.
          try:
              self.render_params = shape.render_params
          except AttributeError:
              # TODO default values
              self.render_params = {}
  
      def pose(self):
          return self.body.state().pose
  
  
  # is it better to have one renderer per shape? I guess this is nice for OOP
  class CircleRenderer(ShapeRenderer):
      def __init__(self, axes, body, circle):
          super().__init__(body, circle)
  
          # TODO: these should be attached to a body since they are part of the
          # environment, so there isn't an issue getting the position. However,
          # may need to think about this for other shapes or bodies - is it bad
          # that bodies need to be added to an Environment before they can be
          # added to the renderer? Worst-case we fallback to the body's initial
          # pose.
          p = self.position()
          self.patch = plt.Circle(p, circle.radius, **self.render_params)
          axes.add_patch(self.patch)
  
      def position(self):
          return super().pose() @ self.shape.position
  
      def render(self, ax):
          """Update the initial render."""
          self.patch.center = self.position()
  
  
  class PolygonRenderer:
      def __init__(self, axes, body, polygon):
          super().__init__(body, polygon)
  
          vertices = polygon.transformed_vertices(self.pose())
          self.patch = plt.Polygon(vertices, **self.render_params)
          axes.add_patch(self.patch)
  
      def pose(self):
          return super().pose() @ self.shape.pose
  
      def render(self, ax):
          """Update the initial render."""
          vertices = self.shape.transformed_vertices(self.pose())
          self.patch.set_xy(vertices)
  
  
  class SegmentRenderer:
      def __init__(self, axes, body, segment):
          super().__init__(body, segment)
  
          self.line = plt.Line2D([], [], **self.render_params)
          axes.add_line(self.line)
  
      def pose(self):
          return super().pose() @ self.shape.pose
  
      def render(self, ax):
          """Update the initial render."""
          T = self.pose()
          aw = T @ self.shape.a
          bw = T @ self.shape.b
          self.line.set_xdata([aw[0], bw[0]])
          self.line.set_ydata([aw[1], bw[1]])
  
  
  # TODO want to make this totally independent of the RealtimePlotter: not sure
  # what to call the latter
  class EnvironmentRenderer:
      def __init__(self, axes, environment):
          self.axes = axes
          self.environment = environment
  
          # using a weakref allows the patch to be removed when the shape (key)
          # is deleted
          self.shape_renderers = weakref.WeakKeyDictionary()
          self.marker_renderers = weakref.WeakKeyDictionary()
  
      def mark_body(
          self, body, transform=None, marker_type=MarkerType.POINT, **render_params
      ):
          """Place a marker on a body."""
          if transform is None:
              transform = Transform.eye()
  
          if marker_type == MarkerType.PATH:
              renderer = MarkerPathRenderer(self.axes, body, transform, **render_params)
          else:
              renderer = MarkerPointRenderer(self.axes, body, transform, **render_params)
          self.marker_renderers[body] = renderer
  
      def mark_robot(
          self,
          robot,
          frame_name,
          transform=None,
          marker_type=MarkerType.POINT,
          **render_params
      ):
          """Place a marker on a robot."""
          if transform is None:
              transform = Transform.eye()
  
          # look up the body on the robot
          frame = robot.frames[frame_name]
          body = robot.bodies[frame.link]
  
          # compose the frame's pose and any supplied transform to get transform
          # relative to body
          transform = frame.pose @ transform
  
          self.mark_body(body, transform, marker_type, **render_params)
  
      def render(self):
          """Render all shapes and markers."""
          # TODO we need the environment to provide all of the bodies:
          for body in self.environment.bodies:
              for fixture in body.fixtures:
                  shape = fixture.shape
  
                  # if we have already rendered this shape, we just need to
                  # update it
                  if shape in self.renderers:
                      renderer = self.renderers[shape]
  
                  # otherwise, we need to create a new renderer for it and render
                  # it
                  else:
                      if type(shape) == Circle:
                          renderer = CircleRenderer(self.axes, body, shape)
                      elif type(shape) == Polygon:
                          renderer = PolygonRenderer(self.axes, body, shape)
                      elif type(shape) == Segment:
                          renderer = SegmentRenderer(self.axes, body, shape)
                      self.renderers[shape] = renderer
  
                  renderer.render()
  
          for marker_renderer in self.marker_renderers.values():
              marker_renderer.render()
  