Ejemplo n.º 1
0
    def getView(self, regenerateView=False, viewWidth=600, viewHeight=400):
        if regenerateView or self.view is None:
            center = self.model.get_center()
            camera = three.PerspectiveCamera(
                position=(center + np.array([0, 200, 0])).tolist(),
                aspect=viewWidth / viewHeight)
            key_light = three.DirectionalLight(position=[0, 10, 10])
            ambient_light = three.AmbientLight()

            scene = three.Scene(
                children=[self.selectable, camera, key_light, ambient_light])

            three.Picker(controlling=scene, event='mousemove')
            controller = three.OrbitControls(controlling=camera,
                                             screenSpacePanning=True,
                                             target=center.tolist())
            camera.lookAt(center.tolist())

            self.coord_label = HTML('Select beads by double-clicking')
            selIncrButton = Button(description='Increase selection')
            selIncrButton.on_click(self.increaseSelection)
            selDecrButton = Button(description='Decrease selection')
            selDecrButton.on_click(self.decreaseSelection)

            click_picker = three.Picker(controlling=self.selectable,
                                        event='dblclick')
            click_picker.observe(self.selectBead, names=['point'])

            renderer = three.Renderer(camera=camera,
                                      scene=scene,
                                      controls=[controller, click_picker],
                                      width=viewWidth,
                                      height=viewHeight,
                                      antialias=True)

            self.view = VBox([
                self.coord_label, renderer,
                HBox([selDecrButton, selIncrButton])
            ])
        return self.view
Ejemplo n.º 2
0
 def __initialize_picker(self, mesh):
     picker = three.Picker(controlling=mesh, event='dblclick')
     return picker
Ejemplo n.º 3
0
 def __initialize_picker(self):
     pickable_objects = self.drawable.mesh
     picker = three.Picker(controlling=pickable_objects, event='click')
     return picker
Ejemplo n.º 4
0
    def _render_obj(self, rendered_obj, **kw):
        obj_geometry = pjs.BufferGeometry(attributes=dict(
            position=pjs.BufferAttribute(rendered_obj.plot_verts),
            color=pjs.BufferAttribute(rendered_obj.base_cols),
            normal=pjs.BufferAttribute(
                rendered_obj.face_normals.astype('float32'))))
        vertices = rendered_obj.vertices

        # 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],
        )

        line_material = pjs.LineBasicMaterial(color='#ffffff',
                                              transparent=True,
                                              opacity=0.3,
                                              linewidth=1.0)
        my_object_wireframe_mesh = pjs.LineSegments(
            geometry=obj_geometry,
            material=line_material,
            position=[0, 0, 0],
        )

        n_vert = vertices.shape[0]
        center = vertices.mean(axis=0)

        extents = self._get_extents(vertices)
        max_delta = np.max(extents[:, 1] - extents[:, 0])
        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)

        v = [0.0, 0.0, 0.0]
        if n_vert > 0: v = vertices[0].tolist()
        select_point_geom = pjs.SphereGeometry(radius=1.0)
        select_point_mesh = pjs.Mesh(
            select_point_geom,
            material=pjs.MeshBasicMaterial(color=SELECTED_VERTEX_COLOR),
            position=v,
            scale=(0.0, 0.0, 0.0))

        #select_edge_mesh = pjs.ArrowHelper(dir=pjs.Vector3(1.0, 0.0, 0.0), origin=pjs.Vector3(0.0, 0.0, 0.0), length=1.0,
        #                                  hex=SELECTED_EDGE_COLOR_INT, headLength=0.1, headWidth=0.05)

        arrow_cyl_mesh = pjs.Mesh(geometry=pjs.SphereGeometry(radius=0.01),
                                  material=pjs.MeshLambertMaterial())
        arrow_head_mesh = pjs.Mesh(geometry=pjs.SphereGeometry(radius=0.001),
                                   material=pjs.MeshLambertMaterial())

        scene_things = [
            my_object_mesh, my_object_wireframe_mesh, select_point_mesh,
            arrow_cyl_mesh, arrow_head_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)

        click_picker = pjs.Picker(controlling=my_object_mesh, event='dblclick')
        out = Output()
        top_msg = HTML()

        def on_dblclick(change):
            if change['name'] == 'point':
                try:
                    point = np.array(change['new'])
                    face = click_picker.faceIndex
                    face_points = rendered_obj.face_verts[face]
                    face_vecs = face_points - np.roll(face_points, 1, axis=0)
                    edge_lens = np.sqrt((face_vecs**2).sum(axis=1))
                    point_vecs = face_points - point[np.newaxis, :]
                    point_dists = (point_vecs**2).sum(axis=1)
                    min_point = np.argmin(point_dists)
                    v1s = point_vecs.copy()
                    v2s = np.roll(v1s, -1, axis=0)
                    edge_mids = 0.5 * (v2s + v1s)
                    edge_mid_dists = (edge_mids**2).sum(axis=1)
                    min_edge_point = np.argmin(edge_mid_dists)
                    edge_start = min_edge_point
                    edge = face * 3 + edge_start
                    close_vert = rendered_obj.face_verts[face, min_point]
                    edge_start_vert = rendered_obj.face_verts[face, edge_start]
                    edge_end_vert = rendered_obj.face_verts[face,
                                                            (edge_start + 1) %
                                                            3]

                    vertex = face * 3 + min_point
                    radius = min(
                        [edge_lens.max() * 0.02, 0.1 * edge_lens.min()])

                    edge_head_length = radius * 4
                    edge_head_width = radius * 2
                    select_point_mesh.scale = (radius, radius, radius)
                    top_msg.value = '<font color="{}">selected face: {}</font>, <font color="{}">edge: {}</font>, <font color="{}"> vertex: {}</font>'.format(
                        SELECTED_FACE_COLOR, face, SELECTED_EDGE_COLOR, edge,
                        SELECTED_VERTEX_COLOR, vertex)
                    newcols = rendered_obj.base_cols.copy()
                    newcols[face * 3:(face + 1) * 3] = np.array(
                        SELECTED_FACE_RGB, dtype='float32')
                    select_point_mesh.position = close_vert.tolist()
                    obj_geometry.attributes['color'].array = newcols

                    with out:
                        make_arrow(arrow_cyl_mesh, arrow_head_mesh,
                                   edge_start_vert, edge_end_vert, radius / 2,
                                   radius, radius * 3, SELECTED_EDGE_COLOR)

                except:
                    with out:
                        print(traceback.format_exc())

        click_picker.observe(on_dblclick, names=['point'])

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

        display_things = [top_msg, renderer_obj, out]
        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(VBox(display_things))
Ejemplo n.º 5
0
def create_gui(geometry=None,
               callback=None,
               opts_choice=None,
               opts_range=None,
               opts_color=None,
               initial_values=None,
               layout=None,
               height=400,
               width=400,
               background='gray',
               orthographic=False,
               camera_position=[0, 0, -10],
               view=(10, -10, -10, 10),
               fov=50,
               add_objects=True,
               add_labels=True,
               show_object_info=False,
               otype_column=None,
               jslink=True):
    """ creates simple gui to visualise 3d geometry,
    with a callback to update geometry according to option widgets 

    Properties
    ----------
    geometry : pandas3js.models.GeometricCollection
    callback : function
        callback(GeometricCollection, options_dict)
    opts_choice : None or dict
        {opt_name:(initial, list)} create dropdown boxes with callbacks to callback
    opts_range : None or dict
        {opt_name:(initial, list)} create select slider with callbacks to callback
    opts_color : None or list
        {opt_name:init_color,...} create select color palette with callbacks to callback
    inital_values : None or dict
        initial values for options (default is first value of list)
    layout : None or list
        (tab_name,[option_name, ...]) pairs, 
        if nested list, then these will be vertically aligned
        by default all go in 'Other' tab
    height : int
        renderer height
    width : int
        renderer width
    background : str
        renderer background color (html)
    orthographic : bool
        use orthographic camera (True) or perspective (False) 
    camera_position : tuple
        position of camera in scene
    view : tuple
        initial view extents (top,bottom,left,right) (orthographic only)
    fov : float
        camera field of view (perspective only)
    add_objects : bool
        add objects to scene
    add_labels : bool
        add object labels to scene
    show_object_info : bool
        if True, show coordinate of object under mouse (currently only works for Perspective)
    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
    -------
    gui : widgets.Box
        containing rendered scene and option widgets
    gcollect : pandas3js.GeometricCollection
        the collection of current geometric objects
    options_view : dict_items
        a view of the current options values
        
    Examples
    --------

    >>> import pandas3js as pjs
    >>> import pandas as pd
    >>> data = {1:{'id':[0],'position':[(0,0,0)],
    ...              'c1':'red','c2':'blue'},
    ...         2:{'id':[0],'position':[(1,2,3)],
    ...               'c1':'red','c2':'blue'}}
    ...
    >>> def callback(geometry,options):
    ...     df = pd.DataFrame(data[options['config']])
    ...     ctype = options.get('color','c1')
    ...     df['color'] = df[ctype]
    ...     df['label'] = 'myobject'
    ...     df['otype'] = 'pandas3js.models.Sphere'
    ...     geometry.change_by_df(df[['id','position','otype',
    ...                               'color','label']],otype_column='otype')
    ...
    >>> gui, collect, opts = pjs.views.create_gui(callback=callback,
    ...                     opts_choice={'color':['c1','c2']},
    ...                     opts_range={'config':[1,2]})
    ...
    >>> [pjs.utils.obj_to_str(c) for c in gui.children]
    ['ipywidgets.widgets.widget_selectioncontainer.Tab', 'pythreejs.pythreejs.Renderer']
    >>> collect.trait_df().loc[0]
    color                                              red
    groups                                          (all,)
    id                                                   0
    label                                         myobject
    label_color                                        red
    label_transparency                                   1
    label_visible                                    False
    other_info                                            
    otype                 pandas3js.models.idobject.Sphere
    position                               (0.0, 0.0, 0.0)
    radius                                               1
    transparency                                         1
    visible                                           True
    Name: 0, dtype: object
    >>> config_select = gui.children[0].children[1].children[1].children[1]
    >>> pjs.utils.obj_to_str(config_select)
    'ipywidgets.widgets.widget_selection.SelectionSlider'
    >>> config_select.value = 2
    >>> collect.trait_df().loc[0]
    color                                              red
    groups                                          (all,)
    id                                                   0
    label                                         myobject
    label_color                                        red
    label_transparency                                   1
    label_visible                                    False
    other_info                                            
    otype                 pandas3js.models.idobject.Sphere
    position                               (1.0, 2.0, 3.0)
    radius                                               1
    transparency                                         1
    visible                                           True
    Name: 0, dtype: object
    >>> color_select = gui.children[0].children[1].children[1].children[0]
    >>> pjs.utils.obj_to_str(color_select)
    'ipywidgets.widgets.widget_selection.ToggleButtons'
    >>> color_select.value = 'c2'
    >>> collect.trait_df().loc[0]
    color                                             blue
    groups                                          (all,)
    id                                                   0
    label                                         myobject
    label_color                                        red
    label_transparency                                   1
    label_visible                                    False
    other_info                                            
    otype                 pandas3js.models.idobject.Sphere
    position                               (1.0, 2.0, 3.0)
    radius                                               1
    transparency                                         1
    visible                                           True
    Name: 0, dtype: object
    
    """
    ## intialise options
    init_vals = {} if initial_values is None else initial_values

    opts_choice = {} if opts_choice is None else opts_choice
    all_options = {
        label: init_vals[label] if label in init_vals else options[0]
        for label, options in opts_choice.items()
    }
    opts_range = {} if opts_range is None else opts_range
    all_options.update({
        label: init_vals[label] if label in init_vals else options[0]
        for label, options in opts_range.items()
    })
    opts_color = {} if opts_color is None else opts_color
    all_options.update({
        label: init_vals[label] if label in init_vals else init
        for label, init in opts_color.items()
    })

    if len(all_options
           ) != len(opts_choice) + len(opts_range) + len(opts_color):
        raise ValueError(
            'options in opts_choice, opts_slide, and opts_color are not unique'
        )

    ## intialise layout
    layout = [] if layout is None else layout
    layout_dict = OrderedDict(layout)
    if len(layout_dict) != len(layout):
        raise ValueError('layout tab names are not unique')

    ## initialise renderer
    if geometry is None:
        gcollect = pjs.models.GeometricCollection()
    else:
        gcollect = geometry
    scene = pjs.views.create_js_scene_view(gcollect,
                                           add_objects=add_objects,
                                           add_labels=add_labels,
                                           jslink=jslink)
    camera, renderer = pjs.views.create_jsrenderer(
        scene,
        orthographic=orthographic,
        camera_position=camera_position,
        view=view,
        fov=fov,
        height=height,
        width=width,
        background=background)

    ## create minimal callback
    if callback is None:

        def callback(geometry, options):
            return

    ## initialise geometry in renderer
    with renderer.hold_trait_notifications():
        callback(gcollect, all_options)

    ## Create controls and callbacks
    controls = {}

    # a check box for showing labels
    if add_labels:
        toggle = widgets.Checkbox(value=False, description='View Label:')

        def handle_toggle(change):
            for obj in gcollect.idobjects:
                obj.label_visible = change.new

        toggle.observe(handle_toggle, names='value')
        controls['View Label'] = toggle

    # zoom sliders for orthographic
    if orthographic:
        top, bottom, left, right = view
        axiszoom = widgets.FloatSlider(
            value=0,
            min=-10,
            max=10,
            step=0.1,
            description='zoom',
            continuous_update=True,
        )

        def handle_axiszoom(change):
            if change.new > 1:
                zoom = 1. / change.new
            elif change.new < -1:
                zoom = -change.new
            else:
                zoom = 1
            with renderer.hold_trait_notifications():
                camera.left = zoom * left
                camera.right = zoom * right
                camera.top = zoom * top
                camera.bottom = zoom * bottom

        axiszoom.observe(handle_axiszoom, names='value')

        controls['Orthographic Zoom'] = axiszoom

    # add additional options

    dd_min = 4  # min amount of options before switch to toggle buttons
    for label in opts_choice:
        options = opts_choice[label]
        initial = init_vals[label] if label in init_vals else options[0]
        assert initial in list(
            options), "initial value {0} for {1} not in range: {2}".format(
                initial, label, list(options))
        if (len(options) == 2 and True in options and False in options
                and isinstance(options[0], bool)
                and isinstance(options[1], bool)):
            ddown = widgets.Checkbox(value=initial, description=label)
        elif len(options) < dd_min:
            ddown = widgets.ToggleButtons(options=list(options),
                                          description=label,
                                          value=initial)
        else:
            ddown = widgets.Dropdown(options=list(options),
                                     description=label,
                                     value=initial)
        handle = _create_callback(renderer, ddown, callback, gcollect,
                                  all_options)
        ddown.observe(handle, names='value')

        controls[label] = ddown

    for label in opts_range:
        options = opts_range[label]
        initial = init_vals[label] if label in init_vals else options[0]
        assert initial in list(
            options), "initial value {0} for {1} not in range: {2}".format(
                initial, label, list(options))
        slider = widgets.SelectionSlider(description=label,
                                         value=initial,
                                         options=list(options),
                                         continuous_update=False)
        handle = _create_callback(renderer, slider, callback, gcollect,
                                  all_options)
        slider.observe(handle, names='value')

        controls[label] = slider

    for label in opts_color:
        option = init_vals[label] if label in init_vals else opts_color[label]
        color = widgets.ColorPicker(description=label,
                                    value=option,
                                    concise=False)
        handle = _create_callback(renderer, color, callback, gcollect,
                                  all_options)
        color.observe(handle, names='value')

        controls[label] = slider

    # add mouse hover information box
    # TODO doesn't work for orthographic https://github.com/jovyan/pythreejs/issues/101
    if not orthographic and show_object_info:
        # create information box
        click_picker = tjs.Picker(root=scene.children[0], event='mousemove')
        infobox = widgets.HTMLMath()

        def change_info(change):
            if click_picker.object:
                infobox.value = 'Object Coordinate: ({1:.3f}, {2:.3f}, {3:.3f})<br>{0}'.format(
                    click_picker.object.other_info,
                    *click_picker.object.position)
            else:
                infobox.value = ''

        click_picker.observe(change_info, names=['object'])
        renderer.controls = renderer.controls + [click_picker]
        renderer = widgets.HBox([renderer, infobox])

    if not controls:
        return (renderer, gcollect, all_options.viewitems() if hasattr(
            all_options, 'viewitems') else all_options.items()
                )  # python 2/3 compatability

    ## layout tabs and controls
    tabs = OrderedDict()
    for tab_name, clist in layout_dict.items():
        vbox_list = []

        for cname in clist:
            if isinstance(cname, list):
                hbox_list = [controls.pop(subcname) for subcname in cname]
                vbox_list.append(widgets.HBox(hbox_list))
            else:
                vbox_list.append(controls.pop(cname))
        tabs[tab_name] = widgets.VBox(vbox_list)

    if 'Orthographic Zoom' in controls:
        tabs.setdefault('View', widgets.Box())
        tabs['View'] = widgets.VBox(
            [tabs['View'], controls.pop('Orthographic Zoom')])

    if 'View Label' in controls:
        tabs.setdefault('View', widgets.Box())
        tabs['View'] = widgets.VBox([tabs['View'], controls.pop('View Label')])

    # deal with remaining controls
    if controls:
        vbox_list = []
        for cname in natural_sort(controls):
            vbox_list.append(controls.pop(cname))
        tabs.setdefault('Other', widgets.Box())
        tabs['Other'] = widgets.VBox([tabs['Other'], widgets.VBox(vbox_list)])

    options = widgets.Tab(children=tuple(tabs.values()))
    for i, name in enumerate(tabs):
        options.set_title(i, name)

    return (widgets.VBox([options,
                          renderer]), gcollect, all_options.viewitems()
            if hasattr(all_options, 'viewitems') else all_options.items()
            )  # python 2/3 compatability
Ejemplo n.º 6
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