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
def __initialize_picker(self, mesh): picker = three.Picker(controlling=mesh, event='dblclick') return picker
def __initialize_picker(self): pickable_objects = self.drawable.mesh picker = three.Picker(controlling=pickable_objects, event='click') return picker
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))
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
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