content: import numpy as np
  from ipywidgets import embed
  import pythreejs as p3s
  import uuid
  
  from .color_util import get_colors, gen_circle, gen_checkers
  
  
  EMBED_URL = "https://cdn.jsdelivr.net/npm/@jupyter-widgets/html-manager@1.0.1/dist/embed-amd.js"
  
  
  class PyThreeJSViewer(object):
  
      def __init__(self, settings, render_mode="WEBSITE"):
          self.render_mode = render_mode
          self.__update_settings(settings)
          self._light = p3s.DirectionalLight(color='white', position=[0, 0, 1], intensity=0.6)
          self._light2 = p3s.AmbientLight(intensity=0.5)
          self._cam = p3s.PerspectiveCamera(position=[0, 0, 1], lookAt=[0, 0, 0], fov=self.__s["fov"],
                                            aspect=self.__s["width"] / self.__s["height"], children=[self._light])
          self._orbit = p3s.OrbitControls(controlling=self._cam)
          self._scene = p3s.Scene(children=[self._cam, self._light2], background=self.__s["background"])  # "#4c4c80"
          self._renderer = p3s.Renderer(camera=self._cam, scene=self._scene, controls=[self._orbit],
                                        width=self.__s["width"], height=self.__s["height"],
                                        antialias=self.__s["antialias"])
  
          self.__objects = {}
          self.__cnt = 0
  
      def jupyter_mode(self):
          self.render_mode = "JUPYTER"
  
      def offline(self):
          self.render_mode = "OFFLINE"
  
      def website(self):
          self.render_mode = "WEBSITE"
  
      def __get_shading(self, shading):
          shad = {"flat": True, "wireframe": False, "wire_width": 0.03, "wire_color": "black",
                  "side": 'DoubleSide', "colormap": "viridis", "normalize": [None, None],
                  "bbox": False, "roughness": 0.5, "metalness": 0.25, "reflectivity": 1.0,
                  "line_width": 1.0, "line_color": "black",
                  "point_color": "red", "point_size": 0.01, "point_shape": "circle",
                  "text_color": "red"
                  }
          for k in shading:
              shad[k] = shading[k]
          return shad
  
      def __update_settings(self, settings={}):
          sett = {"width": 600, "height": 600, "antialias": True, "scale": 1.5, "background": "#ffffff",
                  "fov": 30}
          for k in settings:
              sett[k] = settings[k]
          self.__s = sett
  
      def __add_object(self, obj, parent=None):
          if not parent:  # Object is added to global scene and objects dict
              self.__objects[self.__cnt] = obj
              self.__cnt += 1
              self._scene.add(obj["mesh"])
          else:  # Object is added to parent object and NOT to objects dict
              parent.add(obj["mesh"])
  
          self.__update_view()
  
          if self.render_mode == "JUPYTER":
              return self.__cnt - 1
          elif self.render_mode == "WEBSITE":
              return self
  
      def __add_line_geometry(self, lines, shading, obj=None):
          lines = lines.astype("float32", copy=False)
          mi = np.min(lines, axis=0)
          ma = np.max(lines, axis=0)
  
          geometry = p3s.LineSegmentsGeometry(positions=lines.reshape((-1, 2, 3)))
          material = p3s.LineMaterial(linewidth=shading["line_width"], color=shading["line_color"])
          # , vertexColors='VertexColors'),
          lines = p3s.LineSegments2(geometry=geometry, material=material)  # type='LinePieces')
          line_obj = {"geometry": geometry, "mesh": lines, "material": material,
                      "max": ma, "min": mi, "type": "Lines", "wireframe": None}
  
          if obj:
              return self.__add_object(line_obj, obj), line_obj
          else:
              return self.__add_object(line_obj)
  
      def __update_view(self):
          if len(self.__objects) == 0:
              return
          ma = np.zeros((len(self.__objects), 3))
          mi = np.zeros((len(self.__objects), 3))
          for r, obj in enumerate(self.__objects):
              ma[r] = self.__objects[obj]["max"]
              mi[r] = self.__objects[obj]["min"]
          ma = np.max(ma, axis=0)
          mi = np.min(mi, axis=0)
          diag = np.linalg.norm(ma - mi)
          mean = ((ma - mi) / 2 + mi).tolist()
          scale = self.__s["scale"] * (diag)
          self._orbit.target = mean
          self._cam.lookAt(mean)
          self._cam.position = [mean[0], mean[1], mean[2] + scale]
          self._light.position = [mean[0], mean[1], mean[2] + scale]
  
          self._orbit.exec_three_obj_method('update')
          self._cam.exec_three_obj_method('updateProjectionMatrix')
  
      def __get_bbox(self, v):
          m = np.min(v, axis=0)
          M = np.max(v, axis=0)
  
          # Corners of the bounding box
          v_box = np.array([[m[0], m[1], m[2]], [M[0], m[1], m[2]], [M[0], M[1], m[2]], [m[0], M[1], m[2]],
                            [m[0], m[1], M[2]], [M[0], m[1], M[2]], [M[0], M[1], M[2]], [m[0], M[1], M[2]]])
  
          f_box = np.array([[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4],
                            [0, 4], [1, 5], [2, 6], [7, 3]], dtype=np.uint32)
          return v_box, f_box
  
      def __get_colors(self, v, f, c, sh):
          coloring = "VertexColors"
          if type(c) == np.ndarray and c.size == 3:  # Single color
              colors = np.ones_like(v)
              colors[:, 0] = c[0]
              colors[:, 1] = c[1]
              colors[:, 2] = c[2]
              # print("Single colors")
          elif type(c) == np.ndarray and len(c.shape) == 2 and c.shape[1] == 3:  # Color values for
              if c.shape[0] == f.shape[0]:  # faces
                  colors = np.hstack([c, c, c]).reshape((-1, 3))
                  coloring = "FaceColors"
                  # print("Face color values")
              elif c.shape[0] == v.shape[0]:  # vertices
                  colors = c
                  # print("Vertex color values")
              else:  # Wrong size, fallback
                  print("Invalid color array given! Supported are numpy arrays.", type(c))
                  colors = np.ones_like(v)
                  colors[:, 0] = 1.0
                  colors[:, 1] = 0.874
                  colors[:, 2] = 0.0
          elif type(c) == np.ndarray and c.size == f.shape[0]:  # Function values for faces
              normalize = sh["normalize"][0] != None and sh["normalize"][1] != None
              cc = get_colors(c, sh["colormap"], normalize=normalize,
                              vmin=sh["normalize"][0], vmax=sh["normalize"][1])
              # print(cc.shape)
              colors = np.hstack([cc, cc, cc]).reshape((-1, 3))
              coloring = "FaceColors"
              # print("Face function values")
          elif type(c) == np.ndarray and c.size == v.shape[0]:  # Function values for vertices
              normalize = sh["normalize"][0] != None and sh["normalize"][1] != None
              colors = get_colors(c, sh["colormap"], normalize=normalize,
                                  vmin=sh["normalize"][0], vmax=sh["normalize"][1])
              # print("Vertex function values")
  
          else:
              colors = np.ones_like(v)
              # colors[:, 0] = 1.0
              # colors[:, 1] = 0.874
              # colors[:, 2] = 0.0
              colors[:, 0] = 1
              colors[:, 1] = 1
              colors[:, 2] = 1
  
              # No color
              if c is not None:
                  print("Invalid color array given! Supported are numpy arrays.", type(c))
  
          return colors, coloring
  
      def __get_point_colors(self, v, c, sh):
          v_color = True
          if c is None:  # No color given, use global color
              # conv = mpl.colors.ColorConverter()
              colors = sh["point_color"]  # np.array(conv.to_rgb(sh["point_color"]))
              v_color = False
          elif isinstance(c, str):  # No color given, use global color
              # conv = mpl.colors.ColorConverter()
              colors = c  # np.array(conv.to_rgb(c))
              v_color = False
          elif type(c) == np.ndarray and len(c.shape) == 2 and c.shape[0] == v.shape[0] and c.shape[1] == 3:
              # Point color
              colors = c.astype("float32", copy=False)
  
          elif isinstance(c, np.ndarray) and len(c.shape) == 2 and c.shape[0] == v.shape[0] and c.shape[1] != 3:
              # Function values for vertices, but the colors are features
              c_norm = np.linalg.norm(c, ord=2, axis=-1)
              normalize = sh["normalize"][0] != None and sh["normalize"][1] != None
              colors = get_colors(c_norm, sh["colormap"], normalize=normalize,
                                  vmin=sh["normalize"][0], vmax=sh["normalize"][1])
              colors = colors.astype("float32", copy=False)
  
          elif type(c) == np.ndarray and c.size == v.shape[0]:  # Function color
              normalize = sh["normalize"][0] != None and sh["normalize"][1] != None
              colors = get_colors(c, sh["colormap"], normalize=normalize,
                                  vmin=sh["normalize"][0], vmax=sh["normalize"][1])
              colors = colors.astype("float32", copy=False)
              # print("Vertex function values")
  
          else:
              print("Invalid color array given! Supported are numpy arrays.", type(c))
              colors = sh["point_color"]
              v_color = False
  
          return colors, v_color
  
      def add_mesh(self, v, f, c=None, uv=None, n=None, shading={}, texture_data=None, **kwargs):
          shading.update(kwargs)
          sh = self.__get_shading(shading)
          mesh_obj = {}
  
          # it is a tet
          if v.shape[1] == 3 and f.shape[1] == 4:
              f_tmp = np.ndarray([f.shape[0] * 4, 3], dtype=f.dtype)
              for i in range(f.shape[0]):
                  f_tmp[i * 4 + 0] = np.array([f[i][1], f[i][0], f[i][2]])
                  f_tmp[i * 4 + 1] = np.array([f[i][0], f[i][1], f[i][3]])
                  f_tmp[i * 4 + 2] = np.array([f[i][1], f[i][2], f[i][3]])
                  f_tmp[i * 4 + 3] = np.array([f[i][2], f[i][0], f[i][3]])
              f = f_tmp
  
          if v.shape[1] == 2:
              v = np.append(v, np.zeros([v.shape[0], 1]), 1)
  
          # Type adjustment vertices
          v = v.astype("float32", copy=False)
  
          # Color setup
          colors, coloring = self.__get_colors(v, f, c, sh)
  
          # Type adjustment faces and colors
          c = colors.astype("float32", copy=False)
  
          # Material and geometry setup
          ba_dict = {"color": p3s.BufferAttribute(c)}
          if coloring == "FaceColors":
              verts = np.zeros((f.shape[0] * 3, 3), dtype="float32")
              for ii in range(f.shape[0]):
                  # print(ii*3, f[ii])
                  verts[ii * 3] = v[f[ii, 0]]
                  verts[ii * 3 + 1] = v[f[ii, 1]]
                  verts[ii * 3 + 2] = v[f[ii, 2]]
              v = verts
          else:
              f = f.astype("uint32", copy=False).ravel()
              ba_dict["index"] = p3s.BufferAttribute(f, normalized=False)
  
          ba_dict["position"] = p3s.BufferAttribute(v, normalized=False)
  
          if uv is not None:
              uv = (uv - np.min(uv)) / (np.max(uv) - np.min(uv))
              if texture_data is None:
                  texture_data = gen_checkers(20, 20)
              tex = p3s.DataTexture(data=texture_data, format="RGBFormat", type="FloatType")
              material = p3s.MeshStandardMaterial(map=tex, reflectivity=sh["reflectivity"], side=sh["side"],
                                                  roughness=sh["roughness"], metalness=sh["metalness"],
                                                  flatShading=sh["flat"],
                                                  polygonOffset=True, polygonOffsetFactor=1, polygonOffsetUnits=5)
              ba_dict["uv"] = p3s.BufferAttribute(uv.astype("float32", copy=False))
          else:
              material = p3s.MeshStandardMaterial(vertexColors=coloring, reflectivity=sh["reflectivity"],
                                                  side=sh["side"], roughness=sh["roughness"], metalness=sh["metalness"],
                                                  flatShading=sh["flat"],
                                                  polygonOffset=True, polygonOffsetFactor=1, polygonOffsetUnits=5)
  
          if type(n) != type(None) and coloring == "VertexColors":  # TODO: properly handle normals for FaceColors as well
              ba_dict["normal"] = p3s.BufferAttribute(n.astype("float32", copy=False), normalized=True)
  
          geometry = p3s.BufferGeometry(attributes=ba_dict)
  
          if coloring == "VertexColors" and type(n) == type(None):
              geometry.exec_three_obj_method('computeVertexNormals')
          elif coloring == "FaceColors" and type(n) == type(None):
              geometry.exec_three_obj_method('computeFaceNormals')
  
          # Mesh setup
          mesh = p3s.Mesh(geometry=geometry, material=material)
  
          # Wireframe setup
          mesh_obj["wireframe"] = None
          if sh["wireframe"]:
              wf_geometry = p3s.WireframeGeometry(mesh.geometry)  # WireframeGeometry
              wf_material = p3s.LineBasicMaterial(color=sh["wire_color"], linewidth=sh["wire_width"])
              wireframe = p3s.LineSegments(wf_geometry, wf_material)
              mesh.add(wireframe)
              mesh_obj["wireframe"] = wireframe
  
          # Bounding box setup
          if sh["bbox"]:
              v_box, f_box = self.__get_bbox(v)
              _, bbox = self.add_edges(v_box, f_box, sh, mesh)
              mesh_obj["bbox"] = [bbox, v_box, f_box]
  
          # Object setup
          mesh_obj["max"] = np.max(v, axis=0)
          mesh_obj["min"] = np.min(v, axis=0)
          mesh_obj["geometry"] = geometry
          mesh_obj["mesh"] = mesh
          mesh_obj["material"] = material
          mesh_obj["type"] = "Mesh"
          mesh_obj["shading"] = sh
          mesh_obj["coloring"] = coloring
          mesh_obj["arrays"] = [v, f, c]  # TODO replays with proper storage or remove if not needed
  
          return self.__add_object(mesh_obj)
  
      def add_lines(self, beginning, ending, shading={}, obj=None, **kwargs):
          shading.update(kwargs)
          if len(beginning.shape) == 1:
              if len(beginning) == 2:
                  beginning = np.array([[beginning[0], beginning[1], 0]])
          else:
              if beginning.shape[1] == 2:
                  beginning = np.append(
                      beginning, np.zeros([beginning.shape[0], 1]), 1)
          if len(ending.shape) == 1:
              if len(ending) == 2:
                  ending = np.array([[ending[0], ending[1], 0]])
          else:
              if ending.shape[1] == 2:
                  ending = np.append(
                      ending, np.zeros([ending.shape[0], 1]), 1)
  
          sh = self.__get_shading(shading)
          lines = np.hstack([beginning, ending])
          lines = lines.reshape((-1, 3))
          return self.__add_line_geometry(lines, sh, obj)
  
      def add_edges(self, vertices, edges, shading={}, obj=None, **kwargs):
          shading.update(kwargs)
          if vertices.shape[1] == 2:
              vertices = np.append(
                  vertices, np.zeros([vertices.shape[0], 1]), 1)
          sh = self.__get_shading(shading)
          lines = np.zeros((edges.size, 3))
          cnt = 0
          for e in edges:
              lines[cnt, :] = vertices[e[0]]
              lines[cnt + 1, :] = vertices[e[1]]
              cnt += 2
          return self.__add_line_geometry(lines, sh, obj)
  
      def add_points(self, points, c=None, shading={}, obj=None, **kwargs):
          shading.update(kwargs)
          if len(points.shape) == 1:
              if len(points) == 2:
                  points = np.array([[points[0], points[1], 0]])
          else:
              if points.shape[1] == 2:
                  points = np.append(
                      points, np.zeros([points.shape[0], 1]), 1)
          sh = self.__get_shading(shading)
          points = points.astype("float32", copy=False)
          mi = np.min(points, axis=0)
          ma = np.max(points, axis=0)
  
          g_attributes = {"position": p3s.BufferAttribute(points, normalized=False)}
          m_attributes = {"size": sh["point_size"]}
  
          if sh["point_shape"] == "circle":  # Plot circles
              tex = p3s.DataTexture(data=gen_circle(16, 16), format="RGBAFormat", type="FloatType")
              m_attributes["map"] = tex
              m_attributes["alphaTest"] = 0.5
              m_attributes["transparency"] = True
          else:  # Plot squares
              pass
  
          colors, v_colors = self.__get_point_colors(points, c, sh)
          if v_colors:  # Colors per point
              m_attributes["vertexColors"] = 'VertexColors'
              g_attributes["color"] = p3s.BufferAttribute(colors, normalized=False)
  
          else:  # Colors for all points
              m_attributes["color"] = colors
  
          material = p3s.PointsMaterial(**m_attributes)
          geometry = p3s.BufferGeometry(attributes=g_attributes)
          points = p3s.Points(geometry=geometry, material=material)
          point_obj = {"geometry": geometry, "mesh": points, "material": material,
                       "max": ma, "min": mi, "type": "Points", "wireframe": None}
  
          if obj:
              return self.__add_object(point_obj, obj), point_obj
          else:
              return self.__add_object(point_obj)
  
      def remove_object(self, obj_id):
          if obj_id not in self.__objects:
              print("Invalid object id. Valid ids are: ", list(self.__objects.keys()))
              return
          self._scene.remove(self.__objects[obj_id]["mesh"])
          del self.__objects[obj_id]
          self.__update_view()
  
      def reset(self):
          for obj_id in list(self.__objects.keys()).copy():
              self._scene.remove(self.__objects[obj_id]["mesh"])
              del self.__objects[obj_id]
          self.__update_view()
  
      def update_object(self, oid=0, vertices=None, colors=None, faces=None):
          obj = self.__objects[oid]
          if type(vertices) != type(None):
              if obj["coloring"] == "FaceColors":
                  f = obj["arrays"][1]
                  verts = np.zeros((f.shape[0] * 3, 3), dtype="float32")
                  for ii in range(f.shape[0]):
                      # print(ii*3, f[ii])
                      verts[ii * 3] = vertices[f[ii, 0]]
                      verts[ii * 3 + 1] = vertices[f[ii, 1]]
                      verts[ii * 3 + 2] = vertices[f[ii, 2]]
                  v = verts
  
              else:
                  v = vertices.astype("float32", copy=False)
              obj["geometry"].attributes["position"].array = v
              # self.wireframe.attributes["position"].array = v # Wireframe updates?
              obj["geometry"].attributes["position"].needsUpdate = True
          #           obj["geometry"].exec_three_obj_method('computeVertexNormals')
          if type(colors) != type(None):
              colors, coloring = self.__get_colors(obj["arrays"][0], obj["arrays"][1], colors, obj["shading"])
              colors = colors.astype("float32", copy=False)
              obj["geometry"].attributes["color"].array = colors
              obj["geometry"].attributes["color"].needsUpdate = True
          if type(faces) != type(None):
              if obj["coloring"] == "FaceColors":
                  print("Face updates are currently only possible in vertex color mode.")
                  return
              f = faces.astype("uint32", copy=False).ravel()
              print(obj["geometry"].attributes)
              obj["geometry"].attributes["index"].array = f
              # self.wireframe.attributes["position"].array = v # Wireframe updates?
              obj["geometry"].attributes["index"].needsUpdate = True
          #            obj["geometry"].exec_three_obj_method('computeVertexNormals')
          # self.mesh.geometry.verticesNeedUpdate = True
          # self.mesh.geometry.elementsNeedUpdate = True
          # self.update()
          if self.render_mode == "WEBSITE":
              return self
  
      #    def update(self):
      #        self.mesh.exec_three_obj_method('update')
      #        self.orbit.exec_three_obj_method('update')
      #        self.cam.exec_three_obj_method('updateProjectionMatrix')
      #        self.scene.exec_three_obj_method('update')
  
      def add_text(self, text, shading={}, **kwargs):
          shading.update(kwargs)
          sh = self.__get_shading(shading)
          tt = p3s.TextTexture(string=text, color=sh["text_color"])
          sm = p3s.SpriteMaterial(map=tt)
          text = p3s.Sprite(material=sm, scaleToTexture=True)
          self._scene.add(text)
  
      # def add_widget(self, widget, callback):
      #    self.widgets.append(widget)
      #    widget.observe(callback, names='value')
  
      #    def add_dropdown(self, options, default, desc, cb):
      #        widget = widgets.Dropdown(options=options, value=default, description=desc)
      #        self.__widgets.append(widget)
      #        widget.observe(cb, names="value")
      #        display(widget)
  
      #    def add_button(self, text, cb):
      #        button = widgets.Button(description=text)
      #        self.__widgets.append(button)
      #        button.on_click(cb)
      #        display(button)
  
      def to_html(self, imports=True, html_frame=True):
          # Bake positions (fixes centering bug in offline rendering)
          if len(self.__objects) == 0:
              return
          ma = np.zeros((len(self.__objects), 3))
          mi = np.zeros((len(self.__objects), 3))
          for r, obj in enumerate(self.__objects):
              ma[r] = self.__objects[obj]["max"]
              mi[r] = self.__objects[obj]["min"]
          ma = np.max(ma, axis=0)
          mi = np.min(mi, axis=0)
          diag = np.linalg.norm(ma - mi)
          mean = (ma - mi) / 2 + mi
          for r, obj in enumerate(self.__objects):
              v = self.__objects[obj]["geometry"].attributes["position"].array
              v -= mean
              v += np.array([0.0, .9, 0.0]) #! to move the obj to the center of window
  
          scale = self.__s["scale"] * (diag)
          self._orbit.target = [0.0, 0.0, 0.0]
          self._cam.lookAt([0.0, 0.0, 0.0])
          # self._cam.position = [0.0, 0.0, scale]
          self._cam.position = [0.0, 0.5, scale * 1.3] #! show four complete meshes in the window
          self._light.position = [0.0, 0.0, scale]
  
          state = embed.dependency_state(self._renderer)
  
          # Somehow these entries are missing when the state is exported in python.
          # Exporting from the GUI works, so we are inserting the missing entries.
          for k in state:
              if state[k]["model_name"] == "OrbitControlsModel":
                  state[k]["state"]["maxAzimuthAngle"] = "inf"
                  state[k]["state"]["maxDistance"] = "inf"
                  state[k]["state"]["maxZoom"] = "inf"
                  state[k]["state"]["minAzimuthAngle"] = "-inf"
  
          tpl = embed.load_requirejs_template
          if not imports:
              embed.load_requirejs_template = ""
  
          s = embed.embed_snippet(self._renderer, state=state, embed_url=EMBED_URL)
          # s = embed.embed_snippet(self.__w, state=state)
          embed.load_requirejs_template = tpl
  
          if html_frame:
              s = "<html>\n<body>\n" + s + "\n</body>\n</html>"
  
          # Revert changes
          for r, obj in enumerate(self.__objects):
              v = self.__objects[obj]["geometry"].attributes["position"].array
              v += mean
          self.__update_view()
  
          return s
  
      def save(self, filename=""):
          if filename == "":
              uid = str(uuid.uuid4()) + ".html"
          else:
              filename = filename.replace(".html", "")
              uid = filename + '.html'
          with open(uid, "w") as f:
              f.write(self.to_html())
          print("Plot saved to file %s." % uid)
  