Beispiel #1
0
    def add_view_frustum():
        position = np.array(lm_camera_params['eye'])
        center = np.array(lm_camera_params['center'])
        up = np.array(lm_camera_params['up'])
        aspect = lm_camera_params['aspect']
        fov = math.radians(lm_camera_params['vfov'])

        M = lookat_matrix(position, center, up)
        z = 5
        half_fov = fov * .5
        y = math.tan(half_fov) * z
        x = aspect * y

        p = list(position)
        p1 = list(position + np.dot(M, [-x, -y, -z]))
        p2 = list(position + np.dot(M, [x, -y, -z]))
        p3 = list(position + np.dot(M, [x, y, -z]))
        p4 = list(position + np.dot(M, [-x, y, -z]))

        # Add mesh
        geom = three.Geometry(
            vertices=[p, p1, p2, p, p2, p3, p, p3, p4, p, p4, p1])
        mat = three.MeshBasicMaterial(color='#00ff00',
                                      wireframe=True,
                                      side='DoubleSide')
        mesh = three.Line(geometry=geom, material=mat)
        scene.add(mesh)
Beispiel #2
0
def draw_mesh(mesh, color=None):
    vertices, faces = mesh.to_vertices_and_faces()
    hexcolor = rgb_to_hex(color[:3]) if color else '#cccccc'
    vertexcolors = [hexcolor] * len(vertices)
    faces = [f + [None, [vertexcolors[i] for i in f], None] for f in faces]
    geo = p3js.Geometry(vertices=vertices, faces=faces)
    geo.exec_three_obj_method('computeFaceNormals')
    return p3js.Mesh(geometry=geo, material=p3js.MeshLambertMaterial(vertexColors='VertexColors'), position=[0, 0, 0])
Beispiel #3
0
def display_path(th_scene, vs, **kwargs):
    """Display path."""
    geom = three.Geometry(vertices=vs)
    mat_line = three.LineBasicMaterial(**kwargs)
    line = three.Line(geometry=geom, material=mat_line)
    th_scene.add(line)
    mat_points = three.PointsMaterial(**kwargs)
    points = three.Points(geometry=geom, material=mat_points)
    th_scene.add(points)
Beispiel #4
0
def lines_children(origins, targets, color="blue"):
    material = p3js.LineBasicMaterial(color=color, linewidth=4)

    scene_children = []
    # For each 24 joint
    for origin, target in zip(origins, targets):
        geometry = p3js.Geometry(vertices=np.array([origin, target]).tolist())
        line = p3js.Line(geometry, material)
        scene_children.append(line)
    return scene_children
Beispiel #5
0
def get_polylines_pythreejs(polylines):
    lines = []
    for x in polylines:
        line_geometry = pythreejs.Geometry(vertices=x["vertices"])
        line = pythreejs.Line(
            geometry=line_geometry,
            material=pythreejs.LineBasicMaterial(color=x["color"]),
            type='LinePieces')
        lines.append(line)

    return lines
def draw_mesh(mesh, hexcolor):
    mesh_quads_to_triangles(mesh)
    vertices, faces = mesh.to_vertices_and_faces()
    vertexcolors = [hexcolor] * len(vertices)
    faces = [f + [None, [vertexcolors[i] for i in f], None] for f in faces]
    geo = p3js.Geometry(vertices=vertices, faces=faces)
    geo.exec_three_obj_method('computeFaceNormals')
    return p3js.Mesh(
        geometry=geo,
        material=p3js.MeshLambertMaterial(vertexColors='VertexColors'),
        position=[0, 0, 0])
Beispiel #7
0
def joint_children(joints3D, color="blue", links=None):
    material = p3js.LineBasicMaterial(color=color, linewidth=4)

    scene_children = []
    # For each 24 joint
    if links is None:
        links = [
            (0, 1, 2, 3, 4),
            (0, 5, 6, 7, 8),
            (0, 9, 10, 11, 12),
            (0, 13, 14, 15, 16),
            (0, 17, 18, 19, 20),
        ]
    for link in links:
        for j1, j2 in zip(link[0:-1], link[1:]):
            geometry = p3js.Geometry(vertices=joints3D[(j1, j2), :].tolist())
            line = p3js.Line(geometry, material)
            scene_children.append(line)
    return scene_children
Beispiel #8
0
def ray2mesh(ray):
    rays = py3js.Group()

    w = ray.wavelength
    rc, gc, bc = wavelength2RGB(w)
    rc = int(255 * rc)
    gc = int(255 * gc)
    bc = int(255 * bc)
    material = py3js.LineBasicMaterial(
        color="#{:02X}{:02X}{:02X}".format(rc, gc, bc))

    rl = ray2list(ray)

    for r in rl:
        geometry = py3js.Geometry()
        geometry.vertices = r
        line = py3js.Line(geometry, material)
        rays.add(line)

    return rays
Beispiel #9
0
def ray2mesh(ray):
    rays = py3js.Group()

    if ray.draw_color is None:
        color = wavelength2RGB(ray.wavelength)
    else:
        color = colors.to_rgb(ray.draw_color)

    int_colors = [int(255 * c) for c in color]
    material = py3js.LineBasicMaterial(color="#{:02X}{:02X}{:02X}".format(
        *int_colors))

    rl = ray2list(ray)

    for r in rl:
        geometry = py3js.Geometry()
        geometry.vertices = r
        line = py3js.Line(geometry, material)
        rays.add(line)

    return rays
Beispiel #10
0
def axes(max_dist):
    """
    Generate X, Y, Z axes of length max_width in the form of a pythreejs
    Line object.

    Parameters
    ----------
    max_dist : float
                maximum extent of grid from origin in each dimension

    Returns
    -------
    axes : pythreejs.Line
            a pythreejs Line object representing the xyz axes.
    """
    axes_geom = p3j.Geometry(
        vertices=[[0, 0, 0], [max_dist, 0, 0], [0, 0, 0], [0, max_dist, 0],
                  [0, 0, 0], [0, 0, max_dist]],
        colors=['white', 'white', 'white', 'white', 'white', 'white'])

    return p3j.Line(geometry=axes_geom,
                    material=p3j.LineBasicMaterial(
                        linewidth=1, vertexColors='VertexColors'))
Beispiel #11
0
def create_js_scene_view(gcollect,
                         add_objects=True,
                         add_labels=False,
                         gobject_jsmap=None,
                         jslink=False):
    """create PyThreeJS Scene for GeometricCollection
    and one-way link all GeometricObject attributes and creation/deletion

    Properties
    ----------
    gcollect : GeometricCollection
    add_objects: bool 
        add objects to scene
    add_labels : bool
        add object labels to scene
    gobject_jsmap : None or dict
        if None use default gobject->jsobject mapping
    jslink : bool
        if True, where possible, create client side links
        http://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Events.html#The-difference-between-linking-in-the-kernel-and-linking-in-the-client     
    
    Returns
    -------
    scene : pythreejs.Scene
        scene.children = [gobjcontainer,light]
                        
    Examples
    --------
    
    >>> from pandas3js.models import GeometricCollection, Sphere
    >>> collection = GeometricCollection()
    >>> scene = create_js_scene_view(collection,add_objects=True,add_labels=True)
    >>> container = scene.children[0]
    >>> [type(child) for child in container.children]
    []
                        
    >>> sphere = Sphere(id=1)
    >>> collection.add_object(sphere)
    >>> [type(child) for child in container.children]    
    [<class 'traitlets.traitlets.Sprite'>, <class 'traitlets.traitlets.Mesh'>]
                        
    >>> mesh = container.children[1]
    >>> mesh.position
    [0.0, 0.0, 0.0]
                        
    >>> sphere.position = (1,0,0)
    >>> mesh.position
    [1.0, 0.0, 0.0]

    >>> sphere = collection.pop(1)
    >>> [type(child) for child in container.children]
    []
        
    """
    assert isinstance(
        gcollect,
        GeometricCollection), 'gcollect must be a GeometricCollection'

    meshes = []
    for gobject in gcollect.idobjects:
        if add_labels:
            lmesh = create_jslabelmesh_view(gobject,
                                            gobject_jsmap,
                                            jslink=jslink)
            meshes.append(lmesh)
        if add_objects:
            gmesh = create_jsmesh_view(gobject, gobject_jsmap, jslink=jslink)
            meshes.append(gmesh)

    # create dummy parent mesh to house all meshes, so we can use single mouse picker
    # NB: it would be better to use groups https://threejs.org/docs/#api/objects/Group
    # but this is not implemented in pythreejs
    gcontainer = js.Mesh(geometry=js.Geometry(),
                         material=js.BasicMaterial(),
                         position=[0, 0, 0],
                         children=meshes)

    scenelight = js.AmbientLight(color='#777777')
    scene = js.Scene(children=[gcontainer, scenelight])

    def gobjects_changed(change):

        old = set(change.old)
        new = set(change.new)
        removed_objects = old.difference(new)
        added_objects = new.difference(old)

        if removed_objects:
            removed_ids = [o.id for o in removed_objects]
            original_children = []
            for child in gcontainer.children:
                if child.gobject_id not in removed_ids:
                    original_children.append(child)
        else:
            original_children = gcontainer.children

        new_meshes = []
        for gobject in added_objects:
            if add_labels:
                new_meshes.append(
                    create_jslabelmesh_view(gobject,
                                            gobject_jsmap,
                                            jslink=jslink))
            if add_objects:
                new_meshes.append(
                    create_jsmesh_view(gobject, gobject_jsmap, jslink=jslink))

        gcontainer.children = new_meshes + original_children

    gcollect.observe(gobjects_changed, names='idobjects')

    return scene
Beispiel #12
0
    def _get_grids(self, obj_vertices):
        extents = self._get_extents(obj_vertices)
        grid_verts = []
        deltas = [extent[1] - extent[0] for extent in extents]
        max_extent = max(deltas)
        space1 = 10.0**pymath.floor(
            pymath.log(max_extent) / pymath.log(10.0) - 0.5)
        space2 = 2 * 10.0**pymath.floor(
            pymath.log(max_extent / 2.0) / pymath.log(10.0) - 0.5)
        space = space2
        if max_extent / space2 < 5: space = space1
        N = int(pymath.floor(max_extent / space + 2.0))
        grid_cols = []
        axis_cols = ['#ff3333', '#33ff33', '#3333ff']
        ends = []
        for axis1 in range(3):
            start = pymath.floor(extents[axis1][0] / space) * space
            ends.append(start + space * N)
            for axis2 in range(3):
                axis3 = [x for x in [0, 1, 2] if x not in [axis1, axis2]][0]
                if axis1 == axis2: continue
                delta = extents[axis1][1] - extents[axis1][0]

                start2 = pymath.floor(extents[axis2][0] / space) * space
                end2 = start2 + (N - 1) * space
                verts = self._get_grid_lines(axis1, start, space, N, axis2,
                                             start2, end2)
                grid_verts.extend(verts)
                grid_cols.extend([axis_cols[axis3] for vert in verts])

        # now draw the X,Y,Z labels:
        char_width = max_extent * 0.05
        char_lines = []
        # X:
        char_lines_x = []
        char_lines_x.append([[0.0, 0.0], [1.0, 1.0]])
        char_lines_x.append([[0.0, 1.0], [1.0, 0.0]])
        char_lines.append(char_lines_x)
        # Y:
        char_lines_y = []
        char_lines_y.append([[0.5, 0.0], [0.5, 0.5]])
        char_lines_y.append([[0.5, 0.5], [0.0, 1.0]])
        char_lines_y.append([[0.5, 0.5], [1.0, 1.0]])
        char_lines.append(char_lines_y)
        # Z:
        char_lines_z = []
        char_lines_z.append([[1.0, 1.0], [0.0, 1.0]])
        char_lines_z.append([[0.0, 1.0], [1.0, 0.0]])
        char_lines_z.append([[1.0, 0.0], [0.0, 0.0]])
        char_lines.append(char_lines_z)

        for iaxis in range(3):
            ax1 = [0, 1, 2][iaxis]
            ax2 = [2, 2, 1][iaxis]
            char_lns = char_lines[iaxis]
            segs = [[[0, 0], [ends[iaxis] + char_width, 0]],
                    [[ends[iaxis] + char_width, 0],
                     [ends[iaxis] + 0.5 * char_width, 0.5 * char_width]],
                    [[ends[iaxis] + char_width, 0],
                     [ends[iaxis] + 0.5 * char_width, -0.5 * char_width]]]
            for seg in segs:
                for pt in seg:
                    pt3 = [0, 0, 0]
                    pt3[ax1] += pt[0]
                    pt3[ax2] += pt[1]
                    grid_verts.append(pt3)
                    grid_cols.append('#000000')

            for seg in char_lns:
                for pt in seg:
                    pt3 = [0, 0, 0]
                    pt3[iaxis] += ends[iaxis] + 2 * char_width
                    pt3[ax1] += pt[0] * char_width
                    pt3[ax2] += 1.2 * (pt[1] - 0.5) * char_width
                    grid_verts.append(pt3)
                    grid_cols.append('#000000')

        lines_geom = pjs.Geometry(vertices=grid_verts, colors=grid_cols)
        lines = pjs.LineSegments(geometry=lines_geom,
                                 material=pjs.LineBasicMaterial(
                                     linewidth=self.grid_lines_width,
                                     transparent=True,
                                     opacity=0.5,
                                     dashSize=10,
                                     gapSize=10,
                                     vertexColors='VertexColors'),
                                 type='LinePieces')

        return lines, space
Beispiel #13
0
    def _render_stl(self, stl_file):
        vertices, faces = self._conv_stl(stl_file)

        # Map the vertex colors into the 'color' slot of the faces
        faces = [f + [None, [OBJ_COLOR for i in f], None] for f in faces]

        # Create the geometry:
        obj_geometry = pjs.Geometry(vertices=vertices,
                                    faces=faces,
                                    colors=[OBJ_COLOR] * len(vertices))
        # Calculate normals per face, for nice crisp edges:
        obj_geometry.exec_three_obj_method('computeFaceNormals')

        # Create a mesh. Note that the material need to be told to use the vertex colors.
        my_object_mesh = pjs.Mesh(
            geometry=obj_geometry,
            material=pjs.MeshLambertMaterial(vertexColors='VertexColors'),
            position=[0, 0, 0],  # Center the cube
        )

        n_vert = len(vertices)
        center = [
            sum([vertex[i] for vertex in vertices]) / float(n_vert)
            for i in range(3)
        ]
        extents = self._get_extents(vertices)
        max_delta = max([extent[1] - extent[0] for extent in extents])
        camPos = [center[i] + 4 * max_delta for i in range(3)]
        light_pos = [center[i] + (i + 3) * max_delta for i in range(3)]

        # Set up a scene and render it:
        camera = pjs.PerspectiveCamera(position=camPos,
                                       fov=20,
                                       children=[
                                           pjs.DirectionalLight(
                                               color='#ffffff',
                                               position=light_pos,
                                               intensity=0.5)
                                       ])
        camera.up = (0, 0, 1)

        scene_things = [
            my_object_mesh, camera,
            pjs.AmbientLight(color='#888888')
        ]
        if self.draw_grids:
            grids, space = self._get_grids(vertices)
            scene_things.append(grids)

        scene = pjs.Scene(children=scene_things, background=BACKGROUND_COLOR)

        renderer_obj = pjs.Renderer(
            camera=camera,
            background='#cccc88',
            background_opacity=0,
            scene=scene,
            controls=[pjs.OrbitControls(controlling=camera)],
            width=self.width,
            height=self.height)

        display_things = [renderer_obj]
        if self.draw_grids:
            s = """
<svg width="{}" height="30">
<rect width="20" height="20" x="{}" y="0" style="fill:none;stroke-width:1;stroke:rgb(0,255,0)" />
    <text x="{}" y="15">={:.1f}</text>
  Sorry, your browser does not support inline SVG.
</svg>""".format(self.width, self.width // 2, self.width // 2 + 25, space)
            display_things.append(HTML(s))

        display(*display_things)
Beispiel #14
0
 def draw_mesh(self, mesh, color=None, id=None):
     v, f = mesh.to_vertices_and_faces()
     return p3js.Geometry(vertices=v, faces=f)
Beispiel #15
0
def hand_obj_children(
    obj_verts=None,
    obj_faces=None,
    gt_obj_verts=None,
    gt_obj_faces=None,
    hand_verts=None,
    mano_faces_left=None,
    display_wireframe=False,
    inside_face_colors=True,
    hand_opacity=1,
    obj_opacity=0.2,
):
    """Args:
        obj_verts(numpy.ndarray): vertices of object
        hand_verts(numpy.ndarray): vertices of handect
        *_faces(numpy.ndarray): faces
    """

    scene_children = []
    if obj_verts is not None:
        geo_obj = p3js.Geometry(vertices=obj_verts.tolist(),
                                faces=obj_faces.tolist())
        geo_obj.exec_three_obj_method("computeFaceNormals")
        mat = p3js.MeshLambertMaterial(color="red",
                                       side="FrontSide",
                                       transparent=True)
        mat.opacity = obj_opacity  # obj_opacity
        surf_obj = p3js.Mesh(geometry=geo_obj, material=mat)
        if inside_face_colors:
            back_color = "#a91818"
        else:
            back_color = "red"
        mat_bak = p3js.MeshLambertMaterial(color=back_color,
                                           side="BackSide",
                                           transparent=True)
        mat_bak.opacity = obj_opacity
        surf_obj_back = p3js.Mesh(geometry=geo_obj, material=mat_bak)
        scene_children.append(surf_obj)
        scene_children.append(surf_obj_back)
        if display_wireframe:
            obj_edges = p3js.Mesh(
                geometry=geo_obj,
                material=p3js.MeshBasicMaterial(color="black", wireframe=True),
            )
            scene_children.append(obj_edges)
    if gt_obj_verts is not None:
        geo_obj = p3js.Geometry(vertices=gt_obj_verts.tolist(),
                                faces=gt_obj_faces.tolist())
        geo_obj.exec_three_obj_method("computeFaceNormals")
        mat = p3js.MeshLambertMaterial(color="orange",
                                       side="FrontSide",
                                       transparent=True)
        mat.opacity = obj_opacity
        surf_obj = p3js.Mesh(geometry=geo_obj, material=mat)
        mat_back = p3js.MeshLambertMaterial(color="#a91818",
                                            side="BackSide",
                                            transparent=True)
        mat_back.opacity = obj_opacity
        surf_obj_back = p3js.Mesh(geometry=geo_obj, material=mat_bak)
        scene_children.append(surf_obj)
        scene_children.append(surf_obj_back)
        if display_wireframe:
            obj_edges = p3js.Mesh(
                geometry=geo_obj,
                material=p3js.MeshBasicMaterial(color="black", wireframe=True),
            )
            scene_children.append(obj_edges)
    if hand_verts is not None:
        geo_hand = p3js.Geometry(vertices=hand_verts.tolist(),
                                 faces=mano_faces_left.tolist())
        geo_hand.exec_three_obj_method("computeFaceNormals")
        mat = p3js.MeshLambertMaterial(color="blue",
                                       side="FrontSide",
                                       transparent=True)
        mat.opacity = hand_opacity
        surf_hand = p3js.Mesh(geometry=geo_hand, material=mat)
        bak_mat = p3js.MeshLambertMaterial(color="blue",
                                           side="BackSide",
                                           transparent=True)
        bak_mat.opacity = hand_opacity
        surf_hand_bak = p3js.Mesh(geometry=geo_hand, material=bak_mat)

        scene_children.append(surf_hand)
        scene_children.append(surf_hand_bak)
        if display_wireframe:
            hand_edges = p3js.Mesh(
                geometry=geo_hand,
                material=p3js.MeshBasicMaterial(color="black", wireframe=True),
            )
            scene_children.append(hand_edges)

    return scene_children
Beispiel #16
0
def surf2mesh(S, P=(0, 0, 0), D=(0, 0, 0), wire=False):

    color = "#ffff00"

    points, polylist = S.polylist()

    #Conversion para quethreejs la entienda

    polylist = list(polylist)

    lpoly = []
    lpoints = []

    for l in points:
        lpoints.append(list(l))

    for l in polylist:
        lpoly.append(list(map(int, l)))

    vertices = lpoints

    faces = lpoly

    # Map the vertex colors into the 'color' slot of the faces
    # Map the normals
    nfaces = []
    for f in faces:
        p0 = points[f[0]]
        p1 = points[f[1]]
        p2 = points[f[2]]
        v0 = array(p1) - array(p0)
        v1 = array(p2) - array(p0)
        v3 = cross(v0, v1)
        v3 = tuple(v3 / sqrt(v3[0]**2 + v3[1]**2 + v3[2]**2))

        nfaces.append(f + [v3, color, None])

    # Create the geometry:

    surfaceGeometry = py3js.Geometry(
        vertices=vertices,
        faces=nfaces,
        #colors=vertexcolors
    )

    #surfaceGeometry = py3js.SphereGeometry(radius=300, widthSegments=32, heightSegments=24)

    if wire:
        surfaceGeometry = py3js.WireframeGeometry(surfaceGeometry)

    # Calculate normals per face, for nice crisp edges:
    surfaceGeometry.exec_three_obj_method('computeFaceNormals')

    surfaceMaterial = py3js.MeshPhongMaterial(color=color,
                                              ambient="#050505",
                                              specular="#ffffff",
                                              shininess=15,
                                              emissive="#000000",
                                              side='DoubleSide',
                                              transparent=True,
                                              opacity=.8)
    #surfaceMaterial = py3js.MeshLambertMaterial(color='red',side='DoubleSide')

    # Create a mesh. Note that the material need to be told to use the vertex colors.
    surfaceMesh = py3js.Mesh(
        geometry=surfaceGeometry,
        material=surfaceMaterial,
    )

    surfaceMesh.rotation = *D, "ZYX"
    surfaceMesh.position = tuple(P)
    return surfaceMesh
Beispiel #17
0
def generate_3js_render(
    element_groups,
    canvas_size,
    zoom,
    camera_fov=30,
    background_color="white",
    background_opacity=1.0,
    reuse_objects=False,
    use_atom_arrays=False,
    use_label_arrays=False,
):
    """Create a pythreejs scene of the elements.

    Regarding initialisation performance, see: https://github.com/jupyter-widgets/pythreejs/issues/154
    """
    import pythreejs as pjs

    key_elements = {}
    group_elements = pjs.Group()
    key_elements["group_elements"] = group_elements

    unique_atom_sets = {}
    for el in element_groups["atoms"]:
        element_hash = (
            ("radius", el.sradius),
            ("color", el.color),
            ("fill_opacity", el.fill_opacity),
            ("stroke_color", el.get("stroke_color", "black")),
            ("ghost", el.ghost),
        )
        unique_atom_sets.setdefault(element_hash, []).append(el)

    group_atoms = pjs.Group()
    group_ghosts = pjs.Group()

    atom_geometries = {}
    atom_materials = {}
    outline_materials = {}

    for el_hash, els in unique_atom_sets.items():
        el = els[0]
        data = dict(el_hash)

        if reuse_objects:
            atom_geometry = atom_geometries.setdefault(
                el.sradius,
                pjs.SphereBufferGeometry(radius=el.sradius,
                                         widthSegments=30,
                                         heightSegments=30),
            )
        else:
            atom_geometry = pjs.SphereBufferGeometry(radius=el.sradius,
                                                     widthSegments=30,
                                                     heightSegments=30)

        if reuse_objects:
            atom_material = atom_materials.setdefault(
                (el.color, el.fill_opacity),
                pjs.MeshLambertMaterial(color=el.color,
                                        transparent=True,
                                        opacity=el.fill_opacity),
            )
        else:
            atom_material = pjs.MeshLambertMaterial(color=el.color,
                                                    transparent=True,
                                                    opacity=el.fill_opacity)

        if use_atom_arrays:
            atom_mesh = pjs.Mesh(geometry=atom_geometry,
                                 material=atom_material)
            atom_array = pjs.CloneArray(
                original=atom_mesh,
                positions=[e.position.tolist() for e in els],
                merge=False,
            )
        else:
            atom_array = [
                pjs.Mesh(
                    geometry=atom_geometry,
                    material=atom_material,
                    position=e.position.tolist(),
                    name=e.info_string,
                ) for e in els
            ]

        data["geometry"] = atom_geometry
        data["material_body"] = atom_material

        if el.ghost:
            key_elements["group_ghosts"] = group_ghosts
            group_ghosts.add(atom_array)
        else:
            key_elements["group_atoms"] = group_atoms
            group_atoms.add(atom_array)

        if el.get("stroke_width", 1) > 0:
            if reuse_objects:
                outline_material = outline_materials.setdefault(
                    el.get("stroke_color", "black"),
                    pjs.MeshBasicMaterial(
                        color=el.get("stroke_color", "black"),
                        side="BackSide",
                        transparent=True,
                        opacity=el.get("stroke_opacity", 1.0),
                    ),
                )
            else:
                outline_material = pjs.MeshBasicMaterial(
                    color=el.get("stroke_color", "black"),
                    side="BackSide",
                    transparent=True,
                    opacity=el.get("stroke_opacity", 1.0),
                )
            # TODO use stroke width to dictate scale
            if use_atom_arrays:
                outline_mesh = pjs.Mesh(
                    geometry=atom_geometry,
                    material=outline_material,
                    scale=(1.05, 1.05, 1.05),
                )
                outline_array = pjs.CloneArray(
                    original=outline_mesh,
                    positions=[e.position.tolist() for e in els],
                    merge=False,
                )
            else:
                outline_array = [
                    pjs.Mesh(
                        geometry=atom_geometry,
                        material=outline_material,
                        position=e.position.tolist(),
                        scale=(1.05, 1.05, 1.05),
                    ) for e in els
                ]

            data["material_outline"] = outline_material

            if el.ghost:
                group_ghosts.add(outline_array)
            else:
                group_atoms.add(outline_array)

        key_elements.setdefault("atom_arrays", []).append(data)

    group_elements.add(group_atoms)
    group_elements.add(group_ghosts)

    group_labels = add_labels(element_groups, key_elements, use_label_arrays)
    group_elements.add(group_labels)

    if len(element_groups["cell_lines"]) > 0:
        cell_line_mat = pjs.LineMaterial(
            linewidth=1,
            color=element_groups["cell_lines"].group_properties["color"])
        cell_line_geo = pjs.LineSegmentsGeometry(positions=[
            el.position.tolist() for el in element_groups["cell_lines"]
        ])
        cell_lines = pjs.LineSegments2(geometry=cell_line_geo,
                                       material=cell_line_mat)
        key_elements["cell_lines"] = cell_lines
        group_elements.add(cell_lines)

    if len(element_groups["bond_lines"]) > 0:
        bond_line_mat = pjs.LineMaterial(
            linewidth=element_groups["bond_lines"].
            group_properties["stroke_width"],
            vertexColors="VertexColors",
        )
        bond_line_geo = pjs.LineSegmentsGeometry(
            positions=[
                el.position.tolist() for el in element_groups["bond_lines"]
            ],
            colors=[[Color(c).rgb for c in el.color]
                    for el in element_groups["bond_lines"]],
        )
        bond_lines = pjs.LineSegments2(geometry=bond_line_geo,
                                       material=bond_line_mat)
        key_elements["bond_lines"] = bond_lines
        group_elements.add(bond_lines)

    group_millers = pjs.Group()
    if len(element_groups["miller_lines"]) or len(
            element_groups["miller_planes"]):
        key_elements["group_millers"] = group_millers

    if len(element_groups["miller_lines"]) > 0:
        miller_line_mat = pjs.LineMaterial(
            linewidth=3,
            vertexColors="VertexColors"  # TODO use stroke_width
        )
        miller_line_geo = pjs.LineSegmentsGeometry(
            positions=[
                el.position.tolist() for el in element_groups["miller_lines"]
            ],
            colors=[[Color(el.stroke_color).rgb] * 2
                    for el in element_groups["miller_lines"]],
        )
        miller_lines = pjs.LineSegments2(geometry=miller_line_geo,
                                         material=miller_line_mat)
        group_millers.add(miller_lines)

    for el in element_groups["miller_planes"]:
        vertices = el.position.tolist()
        faces = [(
            0,
            1,
            2,
            triangle_normal(vertices[0], vertices[1], vertices[2]),
            "black",
            0,
        )]
        if len(vertices) == 4:
            faces.append((
                2,
                3,
                0,
                triangle_normal(vertices[2], vertices[3], vertices[0]),
                "black",
                0,
            ))
        elif len(vertices) != 3:
            raise NotImplementedError("polygons with more than 4 points")
        plane_geom = pjs.Geometry(vertices=vertices, faces=faces)
        plane_mat = pjs.MeshBasicMaterial(
            color=el.fill_color,
            transparent=True,
            opacity=el.fill_opacity,
            side="DoubleSide",
        )
        plane_mesh = pjs.Mesh(geometry=plane_geom, material=plane_mat)
        group_millers.add(plane_mesh)

    group_elements.add(group_millers)

    scene = pjs.Scene(background=None)
    scene.add([group_elements])

    view_width, view_height = canvas_size

    minp, maxp = element_groups.get_position_range()
    # compute a minimum camera distance, that is guaranteed to encapsulate all elements
    camera_dist = maxp[2] + sqrt(maxp[0]**2 + maxp[1]**2) / tan(
        radians(camera_fov / 2))

    camera = pjs.PerspectiveCamera(
        fov=camera_fov,
        position=[0, 0, camera_dist],
        aspect=view_width / view_height,
        zoom=zoom,
    )
    scene.add([camera])
    ambient_light = pjs.AmbientLight(color="lightgray")
    key_elements["ambient_light"] = ambient_light
    direct_light = pjs.DirectionalLight(position=(maxp * 2).tolist())
    key_elements["direct_light"] = direct_light
    scene.add([camera, ambient_light, direct_light])

    camera_control = pjs.OrbitControls(controlling=camera,
                                       screenSpacePanning=True)

    atom_picker = pjs.Picker(controlling=group_atoms, event="dblclick")
    key_elements["atom_picker"] = atom_picker
    material = pjs.SpriteMaterial(
        map=create_arrow_texture(right=False),
        transparent=True,
        depthWrite=False,
        depthTest=False,
    )
    atom_pointer = pjs.Sprite(material=material,
                              scale=(4, 3, 1),
                              visible=False)
    scene.add(atom_pointer)
    key_elements["atom_pointer"] = atom_pointer

    renderer = pjs.Renderer(
        camera=camera,
        scene=scene,
        controls=[camera_control, atom_picker],
        width=view_width,
        height=view_height,
        alpha=True,
        clearOpacity=background_opacity,
        clearColor=background_color,
    )
    return renderer, key_elements