Exemple #1
0
 def EraseAll(self):
     self._shapes = {}
     self._displayed_pickable_objects = Group()
     self._current_shape_selection = None
     self._current_mesh_selection = None
     self._current_selection_material = None
     self._renderer.scene = Scene(children=[])
Exemple #2
0
    def group(self) -> Group:
        """
        The pythreejs Group for all the objects in the layer
        """
        if self._group is None:
            self._group = Group()
            for obj in self._objects:
                self._group.add(obj)

        return self._group
Exemple #3
0
    def __init__(
        self,
        width=600,
        height=400,
        quality=0.1,
        angular_tolerance=0.1,
        edge_accuracy=0.01,
        render_edges=True,
        default_mesh_color=None,
        default_edge_color=None,
        info=None,
        timeit=False,
    ):

        self.width = width
        self.height = height
        self.quality = quality
        self.angular_tolerance = angular_tolerance
        self.edge_accuracy = edge_accuracy
        self.render_edges = render_edges
        self.info = info
        self.timeit = timeit

        self.camera_distance_factor = 6
        self.camera_initial_zoom = 2.5

        self.features = ["mesh", "edges"]

        self.bb = None

        self.default_mesh_color = default_mesh_color or self._format_color(166, 166, 166)
        self.default_edge_color = default_edge_color or self._format_color(128, 128, 128)
        self.pick_color = self._format_color(232, 176, 36)

        self.shapes = []
        self.pickable_objects = Group()
        self.pick_last_mesh = None
        self.pick_last_mesh_color = None
        self.pick_mapping = []

        self.camera = None
        self.axes = None
        self.grid = None
        self.scene = None
        self.controller = None
        self.renderer = None

        self.savestate = None
 def EraseAll(self):
     self._shapes = {}
     self._displayed_pickable_objects = Group()
     self._current_shape_selection = None
     self._current_mesh_selection = None
     self._current_selection_material = None
     self._renderer.scene = Scene(children=[])
Exemple #5
0
 def __init__(self,
              num_cells=5,
              color='#cccccc',
              linewidth=1,
              cellsize=0.5):
     Group.__init__(self)
     material = LineBasicMaterial(color=color, linewidth=linewidth)
     for i in range(num_cells + 1):
         edge = cellsize * num_cells / 2
         position = edge - (i * cellsize)
         geometry_h = Geometry(vertices=[(-edge, position,
                                          0), (edge, position, 0)])
         geometry_v = Geometry(vertices=[(position, -edge,
                                          0), (position, edge, 0)])
         self.add(pythreejs.Line(geometry=geometry_h, material=material))
         self.add(pythreejs.Line(geometry=geometry_v, material=material))
    def __init__(self,
                 width=600,
                 height=400,
                 quality=0.5,
                 render_edges=True,
                 default_mesh_color=None,
                 default_edge_color=None,
                 info=None):
        self.width = width
        self.height = height
        self.quality = quality
        self.render_edges = render_edges
        self.info = info

        self.features = ["mesh", "edges"]

        self.bb = None

        self.default_mesh_color = default_mesh_color or self._format_color(
            166, 166, 166)
        self.default_edge_color = default_edge_color or self._format_color(
            128, 128, 128)
        self.pick_color = self._format_color(232, 176, 36)

        self.shapes = []
        self.pickable_objects = Group()
        self.pick_last_mesh = None
        self.pick_last_mesh_color = None
        self.pick_mapping = []

        self.camera = None
        self.axes = None
        self.grid = None
        self.scene = None
        self.controller = None
        self.renderer = None

        self.savestate = None
Exemple #7
0
    def show(self):
        """
        Render the scene for the viewer.

        This method can be called serveral times in order to generate several
        visualizations which are "yoked" together.

        """
        scene = Scene(
            background=self.background,
            children=[
                self._camera,
                AmbientLight(color="#cccccc"),
            ],
        )
        g = Group()
        for _, v in self._layer_lookup.items():
            g.add(v)

        p = Picker(controlling=g, event='click')
        p.observe(self._interact_callback, names=["point"])
        self.html = HTML("")
        scene.add(g)
        self.controls.append(p)

        self._renderer = Renderer(
            width=self._figsize[0],
            height=self._figsize[1],
            camera=self._camera,
            scene=scene,
            alpha=True,
            clearOpacity=0,
            controls=self.controls,
        )
        self._scene = scene
        display(self.html, self._renderer)
Exemple #8
0
    def __init__(self,
                 size=(640, 480),
                 compute_normals_mode=NORMAL.SERVER_SIDE,
                 parallel=False):
        """ Creates a jupyter renderer.
        size: a tuple (width, height). Must be a square, or shapes will look like deformed
        compute_normals_mode: optional, set to SERVER_SIDE by default. This flag lets you choose the
        way normals are computed. If SERVER_SIDE is selected (default value), then normals
        will be computed by the Tesselator, packed as a python tuple, and send as a json structure
        to the client. If, on the other hand, CLIENT_SIDE is chose, then the computer only compute vertex
        indices, and let the normals be computed by the client (the web js machine embedded in the webrowser).

        * SERVER_SIDE: higher server load, loading time increased, lower client load. Poor performance client will
          choose this option (mobile terminals for instance)
        * CLIENT_SIDE: lower server load, loading time decreased, higher client load. Higher performance clients will
                            choose this option (laptops, desktop machines).
        * parallel: optional, False by default. If set to True, meshing runs in parallelized mode.
        """
        self._background = 'white'
        self._background_opacity = 1
        self._size = size
        self._compute_normals_mode = compute_normals_mode
        self._parallel = parallel

        self.html = HTML("Selected shape : None")

        self._bb = None  # the bounding box, necessary to compute camera position

        # the default camera object
        self._camera_target = [0., 0., 0.]  # the point to look at
        self._camera_position = [0, 0., 100.]  # the camera initial position
        self._camera = None

        # a dictionnary of all the shapes belonging to the renderer
        # each element is a key 'mesh_id:shape'
        self._shapes = {}

        # we save the renderer so that is can be accessed
        self._renderer = None

        # the group of 3d and 2d objects to render
        self._displayed_pickable_objects = Group()

        # the group of other objects (grid, trihedron etc.) that can't be selected
        self._displayed_non_pickable_objects = Group()

        # event manager/selection manager
        self._picker = Picker(controlling=self._displayed_pickable_objects, event='mousedown')

        self._current_shape_selection = None
        self._current_mesh_selection = None
        self._selection_color = format_color(232, 176, 36)

        self._select_callbacks = []  # a list of all functions called after an object is selected


        def click(value):
            """ called whenever a shape  or edge is clicked
            """
            obj = value.owner.object
            if self._current_mesh_selection is not None:
                self._current_mesh_selection.material.color = self._current_selection_material_color
            if obj is not None:
                id_clicked = obj.name  # the mesh id clicked
                self._current_mesh_selection = obj
                self._current_selection_material_color = obj.material.color
                obj.material.color = self._selection_color
                # get the shape from this mesh id
                selected_shape = self._shapes[id_clicked]
                html_value = "<b>Shape type:</b> %s<br>" % get_type_as_string(selected_shape)
                html_value += "<b>Shape id:</b> %s<br>" % selected_shape
                self.html.value = html_value
                self._current_shape_selection = selected_shape
            else:
                self.html.value = "<b>Shape type:</b> None<br><b>Shape id:</b> None"
            # then execute calbacks
            for callback in self._select_callbacks:
                callback(self._current_shape_selection)

        self._picker.observe(click)
Exemple #9
0
class JupyterRenderer(object):
    def __init__(self,
                 size=(640, 480),
                 compute_normals_mode=NORMAL.SERVER_SIDE,
                 parallel=False):
        """ Creates a jupyter renderer.
        size: a tuple (width, height). Must be a square, or shapes will look like deformed
        compute_normals_mode: optional, set to SERVER_SIDE by default. This flag lets you choose the
        way normals are computed. If SERVER_SIDE is selected (default value), then normals
        will be computed by the Tesselator, packed as a python tuple, and send as a json structure
        to the client. If, on the other hand, CLIENT_SIDE is chose, then the computer only compute vertex
        indices, and let the normals be computed by the client (the web js machine embedded in the webrowser).

        * SERVER_SIDE: higher server load, loading time increased, lower client load. Poor performance client will
          choose this option (mobile terminals for instance)
        * CLIENT_SIDE: lower server load, loading time decreased, higher client load. Higher performance clients will
                            choose this option (laptops, desktop machines).
        * parallel: optional, False by default. If set to True, meshing runs in parallelized mode.
        """
        self._background = 'white'
        self._background_opacity = 1
        self._size = size
        self._compute_normals_mode = compute_normals_mode
        self._parallel = parallel

        self.html = HTML("Selected shape : None")

        self._bb = None  # the bounding box, necessary to compute camera position

        # the default camera object
        self._camera_target = [0., 0., 0.]  # the point to look at
        self._camera_position = [0, 0., 100.]  # the camera initial position
        self._camera = None

        # a dictionnary of all the shapes belonging to the renderer
        # each element is a key 'mesh_id:shape'
        self._shapes = {}

        # we save the renderer so that is can be accessed
        self._renderer = None

        # the group of 3d and 2d objects to render
        self._displayed_pickable_objects = Group()

        # the group of other objects (grid, trihedron etc.) that can't be selected
        self._displayed_non_pickable_objects = Group()

        # event manager/selection manager
        self._picker = Picker(controlling=self._displayed_pickable_objects, event='mousedown')

        self._current_shape_selection = None
        self._current_mesh_selection = None
        self._selection_color = format_color(232, 176, 36)

        self._select_callbacks = []  # a list of all functions called after an object is selected


        def click(value):
            """ called whenever a shape  or edge is clicked
            """
            obj = value.owner.object
            if self._current_mesh_selection is not None:
                self._current_mesh_selection.material.color = self._current_selection_material_color
            if obj is not None:
                id_clicked = obj.name  # the mesh id clicked
                self._current_mesh_selection = obj
                self._current_selection_material_color = obj.material.color
                obj.material.color = self._selection_color
                # get the shape from this mesh id
                selected_shape = self._shapes[id_clicked]
                html_value = "<b>Shape type:</b> %s<br>" % get_type_as_string(selected_shape)
                html_value += "<b>Shape id:</b> %s<br>" % selected_shape
                self.html.value = html_value
                self._current_shape_selection = selected_shape
            else:
                self.html.value = "<b>Shape type:</b> None<br><b>Shape id:</b> None"
            # then execute calbacks
            for callback in self._select_callbacks:
                callback(self._current_shape_selection)

        self._picker.observe(click)


    def register_select_callback(self, callback):
        """ Adds a callback that will be called each time a shape is selected
        """
        if not callable(callback):
            raise AssertionError("You must provide a callable to register the callback")
        else:
            self._select_callbacks.append(callback)


    def unregister_callback(self, callback):
        """ Remove a callback from the callback list
        """
        if not callback in self._select_callbacks:
            raise AssertionError("This callback is not registered")
        else:
            self._select_callbacks.remove(callback)


    def GetSelectedShape(self):
        """ Returns the selected shape
        """
        return self._current_shape_selection


    def DisplayMesh(self,
                    mesh,
                    color=default_mesh_color):
        """ Display a MEFISTO2 triangle mesh
        """
        if not HAVE_SMESH:
            print("SMESH not installed, DisplayMesh method unavailable.")
            return
        if not isinstance(mesh, SMESH_Mesh):
            raise AssertionError("You mush provide an SMESH_Mesh instance")
        mesh_ds = mesh.GetMeshDS()  # the mesh data source
        face_iter = mesh_ds.facesIterator()
        # vertices positions are stored to a liste
        vertices_position = []
        for _ in range(mesh_ds.NbFaces()-1):
            face = face_iter.next()
            #print('Face %i, type %i' % (i, face.GetType()))
            #print(dir(face))
            # if face.GetType == 3 : triangle mesh, then 3 nodes
            for j in range(3):
                node = face.GetNode(j)
                #print('Coordinates of node %i:(%f,%f,%f)'%(i, node.X(), node.Y(), node.Z()))
                vertices_position.append(node.X())
                vertices_position.append(node.Y())
                vertices_position.append(node.Z())
        number_of_vertices = len(vertices_position)
        # then we build the vertex and faces collections as numpy ndarrays
        np_vertices = np.array(vertices_position, dtype='float32').reshape(int(number_of_vertices / 3), 3)
        # Note: np_faces is just [0, 1, 2, 3, 4, 5, ...], thus arange is used
        np_faces = np.arange(np_vertices.shape[0], dtype='uint32')
        # set geometry properties
        buffer_geometry_properties = {'position': BufferAttribute(np_vertices),
                                      'index'   : BufferAttribute(np_faces)}
        # build a BufferGeometry instance
        mesh_geometry = BufferGeometry(attributes=buffer_geometry_properties)

        mesh_geometry.exec_three_obj_method('computeVertexNormals')

        # then a default material
        mesh_material = MeshPhongMaterial(color=color,
                                          polygonOffset=True,
                                          polygonOffsetFactor=1,
                                          polygonOffsetUnits=1,
                                          shininess=0.5,
                                          wireframe=False,
                                          side='DoubleSide')
        edges_material = MeshPhongMaterial(color='black',
                                           polygonOffset=True,
                                           polygonOffsetFactor=1,
                                           polygonOffsetUnits=1,
                                           shininess=0.5,
                                           wireframe=True)
        # create a mesh unique id
        mesh_id = uuid.uuid4().hex

        # finally create the mash
        shape_mesh = Mesh(geometry=mesh_geometry,
                          material=mesh_material,
                          name=mesh_id)
        edges_mesh = Mesh(geometry=mesh_geometry,
                          material=edges_material,
                          name=mesh_id)


        # a special display for the mesh
        camera_target = [0., 0., 0.]  # the point to look at
        camera_position = [0, 0., 100.]  # the camera initial position
        camera = PerspectiveCamera(position=camera_position,
                                   lookAt=camera_target,
                                   up=[0, 0, 1],
                                   fov=50,
                                   children=[DirectionalLight(color='#ffffff',
                                                              position=[50, 50, 50],
                                                              intensity=0.9)])
        scene_shp = Scene(children=[shape_mesh, edges_mesh, camera, AmbientLight(color='#101010')])

        renderer = Renderer(camera=camera,
                            background=self._background,
                            background_opacity=self._background_opacity,
                            scene=scene_shp,
                            controls=[OrbitControls(controlling=camera, target=camera_target)],
                            width=self._size[0],
                            height=self._size[1],
                            antialias=True)

        display(renderer)


    def DisplayShape(self,
                     shp,  # the TopoDS_Shape to be displayed
                     shape_color=default_shape_color,  # the default
                     render_edges=False,
                     edge_color=default_edge_color,
                     compute_uv_coords=False,
                     quality=1.0,
                     transparency=False,
                     opacity=1.,
                     topo_level='default',
                     update=False):
        """ Displays a topods_shape in the renderer instance.
        shp: the TopoDS_Shape to render
        shape_color: the shape color, in html corm, eg '#abe000'
        render_edges: optional, False by default. If True, compute and dislay all
                      edges as a linear interpolation of segments.
        edge_color: optional, black by default. The color used for edge rendering,
                    in html form eg '#ff00ee'
        compute_uv_coords: optional, false by default. If True, compute texture
                           coordinates (required if the shape has to be textured)
        quality: optional, 1.0 by default. If set to something lower than 1.0,
                      mesh will be more precise. If set to something higher than 1.0,
                      mesh will be less precise, i.e. lower numer of triangles.
        transparency: optional, False by default (opaque).
        opacity: optional, float, by default to 1 (opaque). if transparency is set to True,
                 0. is fully opaque, 1. is fully transparent.
        topo_level: "default" by default. The value should be either "compound", "shape", "vertex".
        update: optional, False by default. If True, render all the shapes.
        """
        if is_wire(shp) or is_edge(shp):
            self.AddCurveToScene(shp, shape_color)
        if topo_level != "default":
            t = TopologyExplorer(shp)
            map_type_and_methods = {"Solid": t.solids, "Face": t.faces, "Shell": t.shells,
                                    "Compound": t.compounds, "Compsolid": t.comp_solids}
            for subshape in map_type_and_methods[topo_level]():
                self.AddShapeToScene(subshape, shape_color, render_edges, edge_color, compute_uv_coords, quality,
                                     transparency, opacity)
        else:
            self.AddShapeToScene(shp, shape_color, render_edges, edge_color, compute_uv_coords, quality,
                                 transparency, opacity)
        if update:
            self.Display()


    def AddCurveToScene(self, shp, color):
        """ shp is either a TopoDS_Wire or a TopodS_Edge.
        """
        if is_edge(shp):
            pnts = discretize_edge(shp)
        elif is_wire(shp):
            pnts = discretize_wire(shp)
        np_edge_vertices = np.array(pnts, dtype=np.float32)
        np_edge_indices = np.arange(np_edge_vertices.shape[0], dtype=np.uint32)
        edge_geometry = BufferGeometry(attributes={
            'position': BufferAttribute(np_edge_vertices),
            'index'   : BufferAttribute(np_edge_indices)
        })
        edge_material = LineBasicMaterial(color=color, linewidth=1)
        edge_lines = Line(geometry=edge_geometry, material=edge_material)

        # Add geometries to pickable or non pickable objects
        self._displayed_pickable_objects.add(edge_lines)


    def AddShapeToScene(self,
                        shp,  # the TopoDS_Shape to be displayed
                        shape_color=default_shape_color,  # the default
                        render_edges=False,
                        edge_color=default_edge_color,
                        compute_uv_coords=False,
                        quality=1.0,
                        transparency=False,
                        opacity=1.):
        # first, compute the tesselation
        tess = Tesselator(shp)
        tess.Compute(uv_coords=compute_uv_coords,
                     compute_edges=render_edges,
                     mesh_quality=quality,
                     parallel=self._parallel)
        # get vertices and normals
        vertices_position = tess.GetVerticesPositionAsTuple()

        number_of_triangles = tess.ObjGetTriangleCount()
        number_of_vertices = len(vertices_position)

        # number of vertices should be a multiple of 3
        if number_of_vertices % 3 != 0:
            raise AssertionError("Wrong number of vertices")
        if number_of_triangles * 9 != number_of_vertices:
            raise AssertionError("Wrong number of triangles")

        # then we build the vertex and faces collections as numpy ndarrays
        np_vertices = np.array(vertices_position, dtype='float32').reshape(int(number_of_vertices / 3), 3)
        # Note: np_faces is just [0, 1, 2, 3, 4, 5, ...], thus arange is used
        np_faces = np.arange(np_vertices.shape[0], dtype='uint32')

        # set geometry properties
        buffer_geometry_properties = {'position': BufferAttribute(np_vertices),
                                      'index'   : BufferAttribute(np_faces)}
        if self._compute_normals_mode == NORMAL.SERVER_SIDE:
            # get the normal list, converts to a numpy ndarray. This should not raise
            # any issue, since normals have been computed by the server, and are available
            # as a list of floats
            np_normals = np.array(tess.GetNormalsAsTuple(), dtype='float32').reshape(-1, 3)
            # quick check
            if np_normals.shape != np_vertices.shape:
                raise AssertionError("Wrong number of normals/shapes")
            buffer_geometry_properties['normal'] = BufferAttribute(np_normals)

        # build a BufferGeometry instance
        shape_geometry = BufferGeometry(attributes=buffer_geometry_properties)

        # if the client has to render normals, add the related js instructions
        if self._compute_normals_mode == NORMAL.CLIENT_SIDE:
            shape_geometry.exec_three_obj_method('computeVertexNormals')

        # then a default material
        shp_material = self._material(shape_color, transparent=transparency, opacity=opacity)
        # create a mesh unique id
        mesh_id = uuid.uuid4().hex

        # finally create the mash
        shape_mesh = Mesh(geometry=shape_geometry,
                          material=shp_material,
                          name=mesh_id)


        # and to the dict of shapes, to have a mapping between meshes and shapes
        self._shapes[mesh_id] = shp

        # edge rendering, if set to True
        edge_lines = None
        if render_edges:
            edges = list(map(lambda i_edge: [tess.GetEdgeVertex(i_edge, i_vert) for i_vert in range(tess.ObjEdgeGetVertexCount(i_edge))], range(tess.ObjGetEdgeCount())))
            edges = list(filter(lambda edge: len(edge) == 2, edges))
            np_edge_vertices = np.array(edges, dtype=np.float32).reshape(-1, 3)
            np_edge_indices = np.arange(np_edge_vertices.shape[0], dtype=np.uint32)
            edge_geometry = BufferGeometry(attributes={
                'position': BufferAttribute(np_edge_vertices),
                'index'   : BufferAttribute(np_edge_indices)
            })
            edge_material = LineBasicMaterial(color=edge_color, linewidth=1)
            edge_lines = LineSegments(geometry=edge_geometry, material=edge_material)

        # Add geometries to pickable or non pickable objects
        self._displayed_pickable_objects.add(shape_mesh)
        if render_edges:
            self._displayed_non_pickable_objects.add(edge_lines)

    def _scale(self, vec):
        r = self._bb.diagonal * 2.5
        n = np.linalg.norm(vec)
        new_vec = [v / n * r for v in vec]
        return self._add(new_vec, self._bb.center)

    def _add(self, vec1, vec2):
        return list(v1 + v2 for v1, v2 in zip(vec1, vec2))

    def _material(self, color, transparent=False, opacity=1.0):
        material = CustomMaterial("standard")
        material.color = color
        material.clipping = True
        material.side = "DoubleSide"
        material.alpha = 0.7
        material.polygonOffset = False
        material.polygonOffsetFactor = 1
        material.polygonOffsetUnits = 1
        material.transparent = transparent
        material.opacity = opacity
        material.update("metalness", 0.3)
        material.update("roughness", 0.8)
        return material

    def EraseAll(self):
        self._shapes = {}
        self._displayed_pickable_objects = Group()
        self._current_shape_selection = None
        self._current_mesh_selection = None
        self._current_selection_material = None
        self._renderer.scene = Scene(children=[])


    def Display(self):
        # Get the overall bounding box
        if self._shapes:
            self._bb = BoundingBox([self._shapes.values()])
        else:  # if nothing registered yet, create a fake bb
            self._bb = BoundingBox([[BRepPrimAPI_MakeSphere(5.).Shape()]])
        bb_max = self._bb.max
        bb_diag = 2 * self._bb.diagonal

        # Set up camera
        camera_target = self._bb.center
        camera_position = self._scale([1, 1, 1])


        self._camera = CombinedCamera(position=camera_position,
                                      width=self._size[0], height=self._size[1],
                                      far=10 * bb_diag, orthoFar=10 * bb_diag)
        self._camera.up = (0.0, 0.0, 1.0)
        self._camera.lookAt(camera_target)
        self._camera.mode = 'orthographic'
        self._camera_target = camera_target
        self._camera.position = camera_position

        # Set up lights in every of the 8 corners of the global bounding box
        key_lights = [
            DirectionalLight(color='white', position=position, intensity=0.12)
            for position in list(itertools.product((-bb_diag, bb_diag), (-bb_diag, bb_diag), (-bb_diag, bb_diag)))
        ]
        ambient_light = AmbientLight(intensity=1.0)

        # Set up Helpers
        self.axes = Axes(bb_center=self._bb.center, length=bb_max * 1.1)
        self.grid = Grid(bb_center=self._bb.center, maximum=bb_max, colorCenterLine='#aaa', colorGrid='#ddd')

        # Set up scene
        environment = self.axes.axes + key_lights + [ambient_light, self.grid.grid, self._camera]

        scene_shp = Scene(children=[self._displayed_pickable_objects,
                                    self._displayed_non_pickable_objects] + environment)

        # Set up Controllers
        self._controller = OrbitControls(controlling=self._camera, target=camera_target)

        self._renderer = Renderer(camera=self._camera,
                                  background=self._background,
                                  background_opacity=self._background_opacity,
                                  scene=scene_shp,
                                  controls=[self._controller, self._picker],
                                  width=self._size[0],
                                  height=self._size[1],
                                  antialias=True)

        # needs to be done after setup of camera
        self.grid.set_rotation((math.pi / 2.0, 0, 0, "XYZ"))
        self.grid.set_position((0, 0, 0))

        # Workaround: Zoom forth and back to update frame. Sometimes necessary :(
        self._camera.zoom = 1.01
        self._update()
        self._camera.zoom = 1.0
        self._update()

        # then display both 3d widgets and webui
        display(HBox([self._renderer, self.html]))

    def _update(self):
        self._controller.exec_three_obj_method('update')

    def __repr__(self):
        self.Display()
        return ""
Exemple #10
0
    def __init__(
            self,
            size=(640, 480),
            compute_normals_mode=NORMAL.SERVER_SIDE,
            default_shape_color=format_color(166, 166, 166),  # light grey
            default_edge_color=format_color(32, 32, 32),  # dark grey
            default_vertex_color=format_color(8, 8, 8),  # darker grey
            pick_color=format_color(232, 176, 36),  # orange
            background_color='white'):
        """ Creates a jupyter renderer.
        size: a tuple (width, height). Must be a square, or shapes will look like deformed
        compute_normals_mode: optional, set to SERVER_SIDE by default. This flag lets you choose the
        way normals are computed. If SERVER_SIDE is selected (default value), then normals
        will be computed by the Tesselator, packed as a python tuple, and send as a json structure
        to the client. If, on the other hand, CLIENT_SIDE is chose, then the computer only compute vertex
        indices, and let the normals be computed by the client (the web js machine embedded in the webrowser).

        * SERVER_SIDE: higher server load, loading time increased, lower client load. Poor performance client will
          choose this option (mobile terminals for instance)
        * CLIENT_SIDE: lower server load, loading time decreased, higher client load. Higher performance clients will
                            choose this option (laptops, desktop machines).
        * default_shape_color
        * default_e1dge_color:
        * default_pick_color:
        * background_color:
        """
        self._default_shape_color = default_shape_color
        self._default_edge_color = default_edge_color
        self._default_vertex_color = default_vertex_color
        self._pick_color = pick_color

        self._background = background_color
        self._background_opacity = 1
        self._size = size
        self._compute_normals_mode = compute_normals_mode

        self._bb = None  # the bounding box, necessary to compute camera position

        # the default camera object
        self._camera_target = [0., 0., 0.]  # the point to look at
        self._camera_position = [0, 0., 100.]  # the camera initial position
        self._camera = None
        self._camera_distance_factor = 6
        self._camera_initial_zoom = 2.5

        # a dictionnary of all the shapes belonging to the renderer
        # each element is a key 'mesh_id:shape'
        self._shapes = {}

        # we save the renderer so that is can be accessed
        self._renderer = None

        # the group of 3d and 2d objects to render
        self._displayed_pickable_objects = Group()

        # the group of other objects (grid, trihedron etc.) that can't be selected
        self._displayed_non_pickable_objects = Group()

        # event manager/selection manager
        self._picker = None

        self._current_shape_selection = None
        self._current_mesh_selection = None
        self._savestate = None

        self._selection_color = format_color(232, 176, 36)

        self._select_callbacks = [
        ]  # a list of all functions called after an object is selected

        # UI
        self.layout = Layout(width='auto', height='auto')
        self._toggle_shp_visibility_button = self.create_button(
            "Hide/Show", "Toggle Shape Visibility", True,
            self.toggle_shape_visibility)
        self._shp_properties_button = Dropdown(options=[
            'Compute', 'Inertia', 'Recognize Face', 'Aligned BBox',
            'Oriented BBox'
        ],
                                               value='Compute',
                                               description='',
                                               layout=self.layout,
                                               disabled=True)
        self._shp_properties_button.observe(self.on_compute_change)
        self._remove_shp_button = self.create_button(
            "Remove", "Permanently remove the shape from the Scene", True,
            self.remove_shape)
        self._controls = [
            self.create_checkbox("axes", "Axes", True,
                                 self.toggle_axes_visibility),
            self.create_checkbox("grid", "Grid", True,
                                 self.toggle_grid_visibility),
            self.create_button("Reset View", "Restore default view", False,
                               self._reset), self._shp_properties_button,
            self._toggle_shp_visibility_button, self._remove_shp_button
        ]
        self.html = HTML("")
Exemple #11
0
class JupyterRenderer:
    def __init__(
            self,
            size=(640, 480),
            compute_normals_mode=NORMAL.SERVER_SIDE,
            default_shape_color=format_color(166, 166, 166),  # light grey
            default_edge_color=format_color(32, 32, 32),  # dark grey
            default_vertex_color=format_color(8, 8, 8),  # darker grey
            pick_color=format_color(232, 176, 36),  # orange
            background_color='white'):
        """ Creates a jupyter renderer.
        size: a tuple (width, height). Must be a square, or shapes will look like deformed
        compute_normals_mode: optional, set to SERVER_SIDE by default. This flag lets you choose the
        way normals are computed. If SERVER_SIDE is selected (default value), then normals
        will be computed by the Tesselator, packed as a python tuple, and send as a json structure
        to the client. If, on the other hand, CLIENT_SIDE is chose, then the computer only compute vertex
        indices, and let the normals be computed by the client (the web js machine embedded in the webrowser).

        * SERVER_SIDE: higher server load, loading time increased, lower client load. Poor performance client will
          choose this option (mobile terminals for instance)
        * CLIENT_SIDE: lower server load, loading time decreased, higher client load. Higher performance clients will
                            choose this option (laptops, desktop machines).
        * default_shape_color
        * default_e1dge_color:
        * default_pick_color:
        * background_color:
        """
        self._default_shape_color = default_shape_color
        self._default_edge_color = default_edge_color
        self._default_vertex_color = default_vertex_color
        self._pick_color = pick_color

        self._background = background_color
        self._background_opacity = 1
        self._size = size
        self._compute_normals_mode = compute_normals_mode

        self._bb = None  # the bounding box, necessary to compute camera position

        # the default camera object
        self._camera_target = [0., 0., 0.]  # the point to look at
        self._camera_position = [0, 0., 100.]  # the camera initial position
        self._camera = None
        self._camera_distance_factor = 6
        self._camera_initial_zoom = 2.5

        # a dictionnary of all the shapes belonging to the renderer
        # each element is a key 'mesh_id:shape'
        self._shapes = {}

        # we save the renderer so that is can be accessed
        self._renderer = None

        # the group of 3d and 2d objects to render
        self._displayed_pickable_objects = Group()

        # the group of other objects (grid, trihedron etc.) that can't be selected
        self._displayed_non_pickable_objects = Group()

        # event manager/selection manager
        self._picker = None

        self._current_shape_selection = None
        self._current_mesh_selection = None
        self._savestate = None

        self._selection_color = format_color(232, 176, 36)

        self._select_callbacks = [
        ]  # a list of all functions called after an object is selected

        # UI
        self.layout = Layout(width='auto', height='auto')
        self._toggle_shp_visibility_button = self.create_button(
            "Hide/Show", "Toggle Shape Visibility", True,
            self.toggle_shape_visibility)
        self._shp_properties_button = Dropdown(options=[
            'Compute', 'Inertia', 'Recognize Face', 'Aligned BBox',
            'Oriented BBox'
        ],
                                               value='Compute',
                                               description='',
                                               layout=self.layout,
                                               disabled=True)
        self._shp_properties_button.observe(self.on_compute_change)
        self._remove_shp_button = self.create_button(
            "Remove", "Permanently remove the shape from the Scene", True,
            self.remove_shape)
        self._controls = [
            self.create_checkbox("axes", "Axes", True,
                                 self.toggle_axes_visibility),
            self.create_checkbox("grid", "Grid", True,
                                 self.toggle_grid_visibility),
            self.create_button("Reset View", "Restore default view", False,
                               self._reset), self._shp_properties_button,
            self._toggle_shp_visibility_button, self._remove_shp_button
        ]
        self.html = HTML("")

    def create_button(self, description, tooltip, disabled, handler):
        button = Button(disabled=disabled,
                        tooltip=tooltip,
                        description=description,
                        layout=self.layout)
        button.on_click(handler)
        return button

    def create_checkbox(self, kind, description, value, handler):
        checkbox = Checkbox(value=value,
                            description=description,
                            layout=self.layout)
        checkbox.observe(handler, "value")
        checkbox.add_class("view_%s" % kind)
        return checkbox

    def remove_shape(self, *kargs):
        self.clicked_obj.visible = not self.clicked_obj.visible
        # remove shape fro mthe mapping dict
        cur_id = self.clicked_obj.name
        del self._shapes[cur_id]
        self._remove_shp_button.disabled = True

    def on_compute_change(self, change):
        if change['type'] == 'change' and change['name'] == 'value':
            selection = change['new']
            output = ""
            if 'Inertia' in selection:
                cog, mass, mass_property = measure_shape_mass_center_of_gravity(
                    self._current_shape_selection)
                # display this point (type gp_Pnt)
                self.DisplayShape([cog])
                output += "<u><b>Center of Gravity</b></u>:<br><b>Xcog=</b>%.3f<br><b>Ycog=</b>%.3f<br><b>Zcog=</b>%.3f<br>" % (
                    cog.X(), cog.Y(), cog.Z())
                output += "<u><b>%s=</b></u>:<b>%.3f</b><br>" % (mass_property,
                                                                 mass)
            elif 'Oriented' in selection:
                center, dim, oobb_shp = get_oriented_boundingbox(
                    self._current_shape_selection)
                self.DisplayShape(oobb_shp,
                                  render_edges=True,
                                  transparency=True,
                                  opacity=0.2,
                                  selectable=False)
                output += "<u><b>OOBB center</b></u>:<br><b>X=</b>%.3f<br><b>Y=</b>%.3f<br><b>Z=</b>%.3f<br>" % (
                    center.X(), center.Y(), center.Z())
                output += "<u><b>OOBB dimensions</b></u>:<br><b>dX=</b>%.3f<br><b>dY=</b>%.3f<br><b>dZ=</b>%.3f<br>" % (
                    dim[0], dim[1], dim[2])
                output += "<u><b>OOBB volume</b></u>:<br><b>V=</b>%.3f<br>" % (
                    dim[0] * dim[1] * dim[2])
            elif 'Aligned' in selection:
                center, dim, albb_shp = get_aligned_boundingbox(
                    self._current_shape_selection)
                self.DisplayShape(albb_shp,
                                  render_edges=True,
                                  transparency=True,
                                  opacity=0.2,
                                  selectable=False)
                output += "<u><b>ABB center</b></u>:<br><b>X=</b>%.3f<br><b>Y=</b>%.3f<br><b>Z=</b>%.3f<br>" % (
                    center.X(), center.Y(), center.Z())
                output += "<u><b>ABB dimensions</b></u>:<br><b>dX=</b>%.3f<br><b>dY=</b>%.3f<br><b>dZ=</b>%.3f<br>" % (
                    dim[0], dim[1], dim[2])
                output += "<u><b>ABB volume</b></u>:<br><b>V=</b>%.3f<br>" % (
                    dim[0] * dim[1] * dim[2])
            elif 'Recognize' in selection:
                # try featrue recognition
                kind, pnt, vec = recognize_face(self._current_shape_selection)
                output += "<u><b>Type</b></u>: %s<br>" % kind
                if kind == "Plane":
                    self.DisplayShape([pnt])
                    output += "<u><b>Properties</b></u>:<br>"
                    output += "<u><b>Point</b></u>:<br><b>X=</b>%.3f<br><b>Y=</b>%.3f<br><b>Z=</b>%.3f<br>" % (
                        pnt.X(), pnt.Y(), pnt.Z())
                    output += "<u><b>Normal</b></u>:<br><b>u=</b>%.3f<br><b>v=</b>%.3f<br><b>w=</b>%.3f<br>" % (
                        vec.X(), vec.Y(), vec.Z())
                elif kind == "Cylinder":
                    self.DisplayShape([pnt])
                    output += "<u><b>Properties</b></u>:<br>"
                    output += "<u><b>Axis point</b></u>:<br><b>X=</b>%.3f<br><b>Y=</b>%.3f<br><b>Z=</b>%.3f<br>" % (
                        pnt.X(), pnt.Y(), pnt.Z())
                    output += "<u><b>Axis direction</b></u>:<br><b>u=</b>%.3f<br><b>v=</b>%.3f<br><b>w=</b>%.3f<br>" % (
                        vec.X(), vec.Y(), vec.Z())
            self.html.value = output

    def toggle_shape_visibility(self, *kargs):
        self.clicked_obj.visible = not self.clicked_obj.visible

    def toggle_axes_visibility(self, change):
        self.axes.set_visibility(_bool_or_new(change))

    def toggle_grid_visibility(self, change):
        self.horizontal_grid.set_visibility(_bool_or_new(change))
        self.vertical_grid.set_visibility(_bool_or_new(change))

    def click(self, value):
        """ called whenever a shape  or edge is clicked
        """
        obj = value.owner.object
        self.clicked_obj = obj
        if self._current_mesh_selection != obj:
            if self._current_mesh_selection is not None:
                self._current_mesh_selection.material.color = self._current_selection_material_color
                self._current_mesh_selection.material.transparent = False
                self._current_mesh_selection = None
                self._current_selection_material_color = None
                self._shp_properties_button.value = "Compute"
                self._shp_properties_button.disabled = True
                self._toggle_shp_visibility_button.disabled = True
                self._remove_shp_button.disabled = True
                self._current_shape_selection = None
            if obj is not None:
                self._shp_properties_button.disabled = False
                self._toggle_shp_visibility_button.disabled = False
                self._remove_shp_button.disabled = False
                id_clicked = obj.name  # the mesh id clicked
                self._current_mesh_selection = obj
                self._current_selection_material_color = obj.material.color
                obj.material.color = self._selection_color
                # selected part becomes transparent
                obj.material.transparent = True
                obj.material.opacity = 0.5
                # get the shape from this mesh id
                selected_shape = self._shapes[id_clicked]
                html_value = "<b>Shape type:</b> %s<br>" % get_type_as_string(
                    selected_shape)
                html_value += "<b>Shape id:</b> %s<br>" % id_clicked
                self.html.value = html_value
                self._current_shape_selection = selected_shape
            else:
                self.html.value = "<b>Shape type:</b> None<br><b>Shape id:</b> None"
            # then execute calbacks
            for callback in self._select_callbacks:
                callback(self._current_shape_selection)

    def register_select_callback(self, callback):
        """ Adds a callback that will be called each time a shape is selected
        """
        if not callable(callback):
            raise AssertionError(
                "You must provide a callable to register the callback")
        else:
            self._select_callbacks.append(callback)

    def unregister_callback(self, callback):
        """ Remove a callback from the callback list
        """
        if callback not in self._select_callbacks:
            raise AssertionError("This callback is not registered")
        else:
            self._select_callbacks.remove(callback)

    def GetSelectedShape(self):
        """ Returns the selected shape
        """
        return self._current_shape_selection

    def DisplayShapeAsSVG(self,
                          shp,
                          export_hidden_edges=True,
                          location=gp_Pnt(0, 0, 0),
                          direction=gp_Dir(1, 1, 1),
                          color="black",
                          line_width=0.5):
        svg_string = export_shape_to_svg(
            shp,
            export_hidden_edges=export_hidden_edges,
            location=location,
            direction=direction,
            color=color,
            line_width=line_width,
            margin_left=0,
            margin_top=0)
        svg = SVG(data=svg_string)
        display(svg)

    def DisplayShape(self,
                     shp,
                     shape_color=None,
                     render_edges=False,
                     edge_color=None,
                     edge_deflection=0.05,
                     vertex_color=None,
                     quality=1.0,
                     transparency=False,
                     opacity=1.,
                     topo_level='default',
                     update=False,
                     selectable=True):
        """ Displays a topods_shape in the renderer instance.
        shp: the TopoDS_Shape to render
        shape_color: the shape color, in html corm, eg '#abe000'
        render_edges: optional, False by default. If True, compute and dislay all
                      edges as a linear interpolation of segments.
        edge_color: optional, black by default. The color used for edge rendering,
                    in html form eg '#ff00ee'
        edge_deflection: optional, 0.05 by default
        vertex_color: optional
        quality: optional, 1.0 by default. If set to something lower than 1.0,
                      mesh will be more precise. If set to something higher than 1.0,
                      mesh will be less precise, i.e. lower numer of triangles.
        transparency: optional, False by default (opaque).
        opacity: optional, float, by default to 1 (opaque). if transparency is set to True,
                 0. is fully opaque, 1. is fully transparent.
        topo_level: "default" by default. The value should be either "compound", "shape", "vertex".
        update: optional, False by default. If True, render all the shapes.
        selectable: if True, can be doubleclicked from the 3d window
        """
        if edge_color is None:
            edge_color = self._default_edge_color
        if shape_color is None:
            shape_color = self._default_shape_color
        if vertex_color is None:
            vertex_color = self._default_vertex_color

        output = []  # a list of all geometries created from the shape
        # is it list of gp_Pnt ?
        if isinstance(shp, list) and isinstance(shp[0], gp_Pnt):
            result = self.AddVerticesToScene(shp, vertex_color)
            output.append(result)
        # or a 1d element such as edge or wire ?
        elif is_wire(shp) or is_edge(shp):
            result = self.AddCurveToScene(shp, edge_color, edge_deflection)
            output.append(result)
        elif topo_level != "default":
            t = TopologyExplorer(shp)
            map_type_and_methods = {
                "Solid": t.solids,
                "Face": t.faces,
                "Shell": t.shells,
                "Compound": t.compounds,
                "Compsolid": t.comp_solids
            }
            for subshape in map_type_and_methods[topo_level]():
                result = self.AddShapeToScene(subshape, shape_color,
                                              render_edges, edge_color,
                                              vertex_color, quality,
                                              transparency, opacity)
                output.append(result)
        else:
            result = self.AddShapeToScene(shp, shape_color, render_edges,
                                          edge_color, vertex_color, quality,
                                          transparency, opacity)
            output.append(result)

        if selectable:  # Add geometries to pickable or non pickable objects
            for elem in output:
                self._displayed_pickable_objects.add(elem)

        if update:
            self.Display()

    def AddVerticesToScene(self, pnt_list, vertex_color, vertex_width=5):
        """ shp is a list of gp_Pnt
        """
        vertices_list = []  # will be passed to pythreejs
        BB = BRep_Builder()
        compound = TopoDS_Compound()
        BB.MakeCompound(compound)

        for vertex in pnt_list:
            vertex_to_add = BRepBuilderAPI_MakeVertex(vertex).Shape()
            BB.Add(compound, vertex_to_add)
            vertices_list.append([vertex.X(), vertex.Y(), vertex.Z()])

        # map the Points and the AIS_PointCloud
        # and to the dict of shapes, to have a mapping between meshes and shapes
        point_cloud_id = "%s" % uuid.uuid4().hex
        self._shapes[point_cloud_id] = compound

        vertices_list = np.array(vertices_list, dtype=np.float32)
        attributes = {
            "position": BufferAttribute(vertices_list, normalized=False)
        }
        mat = PointsMaterial(color=vertex_color,
                             sizeAttenuation=True,
                             size=vertex_width)
        geom = BufferGeometry(attributes=attributes)
        points = Points(geometry=geom, material=mat, name=point_cloud_id)
        return points

    def AddCurveToScene(self, shp, edge_color, deflection):
        """ shp is either a TopoDS_Wire or a TopodS_Edge.
        """
        if is_edge(shp):
            pnts = discretize_edge(shp, deflection)
        elif is_wire(shp):
            pnts = discretize_wire(shp, deflection)
        np_edge_vertices = np.array(pnts, dtype=np.float32)
        np_edge_indices = np.arange(np_edge_vertices.shape[0], dtype=np.uint32)
        edge_geometry = BufferGeometry(
            attributes={
                'position': BufferAttribute(np_edge_vertices),
                'index': BufferAttribute(np_edge_indices)
            })
        edge_material = LineBasicMaterial(color=edge_color, linewidth=1)

        # and to the dict of shapes, to have a mapping between meshes and shapes
        edge_id = "%s" % uuid.uuid4().hex
        self._shapes[edge_id] = shp

        edge_line = Line(geometry=edge_geometry,
                         material=edge_material,
                         name=edge_id)

        # and to the dict of shapes, to have a mapping between meshes and shapes
        edge_id = "%s" % uuid.uuid4().hex
        self._shapes[edge_id] = shp

        return edge_line

    def AddShapeToScene(
            self,
            shp,
            shape_color=None,  # the default
            render_edges=False,
            edge_color=None,
            vertex_color=None,
            quality=1.0,
            transparency=False,
            opacity=1.):
        # first, compute the tesselation
        tess = Tesselator(shp)
        tess.Compute(compute_edges=render_edges,
                     mesh_quality=quality,
                     parallel=True)
        # get vertices and normals
        vertices_position = tess.GetVerticesPositionAsTuple()

        number_of_triangles = tess.ObjGetTriangleCount()
        number_of_vertices = len(vertices_position)

        # number of vertices should be a multiple of 3
        if number_of_vertices % 3 != 0:
            raise AssertionError("Wrong number of vertices")
        if number_of_triangles * 9 != number_of_vertices:
            raise AssertionError("Wrong number of triangles")

        # then we build the vertex and faces collections as numpy ndarrays
        np_vertices = np.array(vertices_position, dtype='float32').reshape(
            int(number_of_vertices / 3), 3)
        # Note: np_faces is just [0, 1, 2, 3, 4, 5, ...], thus arange is used
        np_faces = np.arange(np_vertices.shape[0], dtype='uint32')

        # set geometry properties
        buffer_geometry_properties = {
            'position': BufferAttribute(np_vertices),
            'index': BufferAttribute(np_faces)
        }
        if self._compute_normals_mode == NORMAL.SERVER_SIDE:
            # get the normal list, converts to a numpy ndarray. This should not raise
            # any issue, since normals have been computed by the server, and are available
            # as a list of floats
            np_normals = np.array(tess.GetNormalsAsTuple(),
                                  dtype='float32').reshape(-1, 3)
            # quick check
            if np_normals.shape != np_vertices.shape:
                raise AssertionError("Wrong number of normals/shapes")
            buffer_geometry_properties['normal'] = BufferAttribute(np_normals)

        # build a BufferGeometry instance
        shape_geometry = BufferGeometry(attributes=buffer_geometry_properties)

        # if the client has to render normals, add the related js instructions
        if self._compute_normals_mode == NORMAL.CLIENT_SIDE:
            shape_geometry.exec_three_obj_method('computeVertexNormals')

        # then a default material
        shp_material = self._material(shape_color,
                                      transparent=transparency,
                                      opacity=opacity)

        # and to the dict of shapes, to have a mapping between meshes and shapes
        mesh_id = "%s" % uuid.uuid4().hex
        self._shapes[mesh_id] = shp

        # finally create the mesh
        shape_mesh = Mesh(geometry=shape_geometry,
                          material=shp_material,
                          name=mesh_id)

        # edge rendering, if set to True
        if render_edges:
            edges = list(
                map(
                    lambda i_edge: [
                        tess.GetEdgeVertex(i_edge, i_vert)
                        for i_vert in range(tess.ObjEdgeGetVertexCount(i_edge))
                    ], range(tess.ObjGetEdgeCount())))
            edge_list = _flatten(list(map(_explode, edges)))
            lines = LineSegmentsGeometry(positions=edge_list)
            mat = LineMaterial(linewidth=1, color=edge_color)
            edge_lines = LineSegments2(lines, mat)
            self._displayed_non_pickable_objects.add(edge_lines)

        return shape_mesh

    def _scale(self, vec):
        r = self._bb._max_dist_from_center() * self._camera_distance_factor
        n = np.linalg.norm(vec)
        new_vec = [v / n * r for v in vec]
        return new_vec

    def _material(self, color, transparent=False, opacity=1.0):
        #material = MeshPhongMaterial()
        material = CustomMaterial("standard")
        material.color = color
        material.clipping = True
        material.side = "DoubleSide"
        material.polygonOffset = True
        material.polygonOffsetFactor = 1
        material.polygonOffsetUnits = 1
        material.transparent = transparent
        material.opacity = opacity
        material.update("metalness", 0.3)
        material.update("roughness", 0.8)
        return material

    def EraseAll(self):
        self._shapes = {}
        self._displayed_pickable_objects = Group()
        self._current_shape_selection = None
        self._current_mesh_selection = None
        self._current_selection_material = None
        self._renderer.scene = Scene(children=[])

    def Display(self, position=None, rotation=None):
        # Get the overall bounding box
        if self._shapes:
            self._bb = BoundingBox([self._shapes.values()])
        else:  # if nothing registered yet, create a fake bb
            self._bb = BoundingBox([[BRepPrimAPI_MakeSphere(5.).Shape()]])
        bb_max = self._bb.max
        orbit_radius = 1.5 * self._bb._max_dist_from_center()

        # Set up camera
        camera_target = self._bb.center
        camera_position = _add(
            self._bb.center,
            self._scale(
                [1, 1, 1] if position is None else self._scale(position)))
        camera_zoom = self._camera_initial_zoom

        self._camera = CombinedCamera(position=camera_position,
                                      width=self._size[0],
                                      height=self._size[1])
        self._camera.up = (0.0, 0.0, 1.0)
        self._camera.mode = 'orthographic'
        self._camera_target = camera_target
        self._camera.position = camera_position
        if rotation is not None:
            self._camera.rotation = rotation
        # Set up lights in every of the 8 corners of the global bounding box
        positions = list(
            itertools.product(*[(-orbit_radius, orbit_radius)] * 3))
        key_lights = [
            DirectionalLight(color='white', position=pos, intensity=0.5)
            for pos in positions
        ]
        ambient_light = AmbientLight(intensity=0.1)

        # Set up Helpers
        self.axes = Axes(bb_center=self._bb.center, length=bb_max * 1.1)
        self.horizontal_grid = Grid(bb_center=self._bb.center,
                                    maximum=bb_max,
                                    colorCenterLine='#aaa',
                                    colorGrid='#ddd')
        self.vertical_grid = Grid(bb_center=self._bb.center,
                                  maximum=bb_max,
                                  colorCenterLine='#aaa',
                                  colorGrid='#ddd')
        # Set up scene
        environment = self.axes.axes + key_lights + [
            ambient_light, self.horizontal_grid.grid, self.vertical_grid.grid,
            self._camera
        ]

        scene_shp = Scene(children=[
            self._displayed_pickable_objects,
            self._displayed_non_pickable_objects
        ] + environment)

        # Set up Controllers
        self._controller = OrbitControls(controlling=self._camera,
                                         target=camera_target,
                                         target0=camera_target)
        # Update controller to instantiate camera position
        self._camera.zoom = camera_zoom
        self._update()

        # setup Picker
        self._picker = Picker(controlling=self._displayed_pickable_objects,
                              event='dblclick')
        self._picker.observe(self.click)

        self._renderer = Renderer(camera=self._camera,
                                  background=self._background,
                                  background_opacity=self._background_opacity,
                                  scene=scene_shp,
                                  controls=[self._controller, self._picker],
                                  width=self._size[0],
                                  height=self._size[1],
                                  antialias=True)

        # set rotation and position for each grid
        self.horizontal_grid.set_position((0, 0, 0))
        self.horizontal_grid.set_rotation((math.pi / 2.0, 0, 0, "XYZ"))

        self.vertical_grid.set_position((0, -bb_max, 0))

        self._savestate = (self._camera.rotation, self._controller.target)

        # then display both 3d widgets and webui
        display(HBox([VBox([HBox(self._controls), self._renderer]),
                      self.html]))

    def ExportToHTML(self, filename):
        embed.embed_minimal_html(filename,
                                 views=self._renderer,
                                 title='pythonocc')

    def _reset(self, *kargs):
        self._camera.rotation, self._controller.target = self._savestate
        self._camera.position = _add(self._bb.center, self._scale((1, 1, 1)))
        self._camera.zoom = self._camera_initial_zoom
        self._update()

    def _update(self):
        self._controller.exec_three_obj_method('update')

    def __repr__(self):
        self.Display()
        return ""
Exemple #12
0
    def __init__(self, filename, scale=1.0):
        Group.__init__(self)

        self._dae = Collada(filename)
        self._load_mesh(self._dae, scale=scale)
    def __init__(self,
                 size=(640, 480),
                 compute_normals_mode=NORMAL.SERVER_SIDE,
                 parallel=False):
        """ Creates a jupyter renderer.
        size: a tuple (width, height). Must be a square, or shapes will look like deformed
        compute_normals_mode: optional, set to SERVER_SIDE by default. This flag lets you choose the
        way normals are computed. If SERVER_SIDE is selected (default value), then normals
        will be computed by the Tesselator, packed as a python tuple, and send as a json structure
        to the client. If, on the other hand, CLIENT_SIDE is chose, then the computer only compute vertex
        indices, and let the normals be computed by the client (the web js machine embedded in the webrowser).

        * SERVER_SIDE: higher server load, loading time increased, lower client load. Poor performance client will
          choose this option (mobile terminals for instance)
        * CLIENT_SIDE: lower server load, loading time decreased, higher client load. Higher performance clients will
                            choose this option (laptops, desktop machines).
        * parallel: optional, False by default. If set to True, meshing runs in parallelized mode.
        """
        self._background = 'white'
        self._background_opacity = 1
        self._size = size
        self._compute_normals_mode = compute_normals_mode
        self._parallel = parallel

        self.html = HTML("Selected shape : None")

        self._bb = None  # the bounding box, necessary to compute camera position

        # the default camera object
        self._camera_target = [0., 0., 0.]  # the point to look at
        self._camera_position = [0, 0., 100.]  # the camera initial position
        self._camera = None

        # a dictionnary of all the shapes belonging to the renderer
        # each element is a key 'mesh_id:shape'
        self._shapes = {}

        # we save the renderer so that is can be accessed
        self._renderer = None

        # the group of 3d and 2d objects to render
        self._displayed_pickable_objects = Group()

        # the group of other objects (grid, trihedron etc.) that can't be selected
        self._displayed_non_pickable_objects = Group()

        # event manager/selection manager
        self._picker = Picker(controlling=self._displayed_pickable_objects, event='mousedown')

        self._current_shape_selection = None
        self._current_mesh_selection = None
        self._selection_color = format_color(232, 176, 36)

        self._select_callbacks = []  # a list of all functions called after an object is selected


        def click(value):
            """ called whenever a shape  or edge is clicked
            """
            obj = value.owner.object
            if self._current_mesh_selection is not None:
                self._current_mesh_selection.material.color = self._current_selection_material_color
            if obj is not None:
                id_clicked = obj.name  # the mesh id clicked
                self._current_mesh_selection = obj
                self._current_selection_material_color = obj.material.color
                obj.material.color = self._selection_color
                # get the shape from this mesh id
                selected_shape = self._shapes[id_clicked]
                html_value = "<b>Shape type:</b> %s<br>" % get_type_as_string(selected_shape)
                html_value += "<b>Shape id:</b> %s<br>" % selected_shape
                self.html.value = html_value
                self._current_shape_selection = selected_shape
            else:
                self.html.value = "<b>Shape type:</b> None<br><b>Shape id:</b> None"
            # then execute calbacks
            for callback in self._select_callbacks:
                callback(self._current_shape_selection)

        self._picker.observe(click)
class JupyterRenderer(object):
    def __init__(self,
                 size=(640, 480),
                 compute_normals_mode=NORMAL.SERVER_SIDE,
                 parallel=False):
        """ Creates a jupyter renderer.
        size: a tuple (width, height). Must be a square, or shapes will look like deformed
        compute_normals_mode: optional, set to SERVER_SIDE by default. This flag lets you choose the
        way normals are computed. If SERVER_SIDE is selected (default value), then normals
        will be computed by the Tesselator, packed as a python tuple, and send as a json structure
        to the client. If, on the other hand, CLIENT_SIDE is chose, then the computer only compute vertex
        indices, and let the normals be computed by the client (the web js machine embedded in the webrowser).

        * SERVER_SIDE: higher server load, loading time increased, lower client load. Poor performance client will
          choose this option (mobile terminals for instance)
        * CLIENT_SIDE: lower server load, loading time decreased, higher client load. Higher performance clients will
                            choose this option (laptops, desktop machines).
        * parallel: optional, False by default. If set to True, meshing runs in parallelized mode.
        """
        self._background = 'white'
        self._background_opacity = 1
        self._size = size
        self._compute_normals_mode = compute_normals_mode
        self._parallel = parallel

        self.html = HTML("Selected shape : None")

        self._bb = None  # the bounding box, necessary to compute camera position

        # the default camera object
        self._camera_target = [0., 0., 0.]  # the point to look at
        self._camera_position = [0, 0., 100.]  # the camera initial position
        self._camera = None

        # a dictionnary of all the shapes belonging to the renderer
        # each element is a key 'mesh_id:shape'
        self._shapes = {}

        # we save the renderer so that is can be accessed
        self._renderer = None

        # the group of 3d and 2d objects to render
        self._displayed_pickable_objects = Group()

        # the group of other objects (grid, trihedron etc.) that can't be selected
        self._displayed_non_pickable_objects = Group()

        # event manager/selection manager
        self._picker = Picker(controlling=self._displayed_pickable_objects, event='mousedown')

        self._current_shape_selection = None
        self._current_mesh_selection = None
        self._selection_color = format_color(232, 176, 36)

        self._select_callbacks = []  # a list of all functions called after an object is selected


        def click(value):
            """ called whenever a shape  or edge is clicked
            """
            obj = value.owner.object
            if self._current_mesh_selection is not None:
                self._current_mesh_selection.material.color = self._current_selection_material_color
            if obj is not None:
                id_clicked = obj.name  # the mesh id clicked
                self._current_mesh_selection = obj
                self._current_selection_material_color = obj.material.color
                obj.material.color = self._selection_color
                # get the shape from this mesh id
                selected_shape = self._shapes[id_clicked]
                html_value = "<b>Shape type:</b> %s<br>" % get_type_as_string(selected_shape)
                html_value += "<b>Shape id:</b> %s<br>" % selected_shape
                self.html.value = html_value
                self._current_shape_selection = selected_shape
            else:
                self.html.value = "<b>Shape type:</b> None<br><b>Shape id:</b> None"
            # then execute calbacks
            for callback in self._select_callbacks:
                callback(self._current_shape_selection)

        self._picker.observe(click)


    def register_select_callback(self, callback):
        """ Adds a callback that will be called each time a shape is selected
        """
        if not callable(callback):
            raise AssertionError("You must provide a callable to register the callback")
        else:
            self._select_callbacks.append(callback)


    def unregister_callback(self, callback):
        """ Remove a callback from the callback list
        """
        if not callback in self._select_callbacks:
            raise AssertionError("This callback is not registered")
        else:
            self._select_callbacks.remove(callback)


    def GetSelectedShape(self):
        """ Returns the selected shape
        """
        return self._current_shape_selection


    def DisplayMesh(self,
                    mesh,
                    color=default_mesh_color):
        """ Display a MEFISTO2 triangle mesh
        """
        if not HAVE_SMESH:
            print("SMESH not installed, DisplayMesh method unavailable.")
            return
        if not isinstance(mesh, SMESH_Mesh):
            raise AssertionError("You mush provide an SMESH_Mesh instance")
        mesh_ds = mesh.GetMeshDS()  # the mesh data source
        face_iter = mesh_ds.facesIterator()
        # vertices positions are stored to a liste
        vertices_position = []
        for _ in range(mesh_ds.NbFaces()-1):
            face = face_iter.next()
            #print('Face %i, type %i' % (i, face.GetType()))
            #print(dir(face))
            # if face.GetType == 3 : triangle mesh, then 3 nodes
            for j in range(3):
                node = face.GetNode(j)
                #print('Coordinates of node %i:(%f,%f,%f)'%(i, node.X(), node.Y(), node.Z()))
                vertices_position.append(node.X())
                vertices_position.append(node.Y())
                vertices_position.append(node.Z())
        number_of_vertices = len(vertices_position)
        # then we build the vertex and faces collections as numpy ndarrays
        np_vertices = np.array(vertices_position, dtype='float32').reshape(int(number_of_vertices / 3), 3)
        # Note: np_faces is just [0, 1, 2, 3, 4, 5, ...], thus arange is used
        np_faces = np.arange(np_vertices.shape[0], dtype='uint32')
        # set geometry properties
        buffer_geometry_properties = {'position': BufferAttribute(np_vertices),
                                      'index'   : BufferAttribute(np_faces)}
        # build a BufferGeometry instance
        mesh_geometry = BufferGeometry(attributes=buffer_geometry_properties)

        mesh_geometry.exec_three_obj_method('computeVertexNormals')

        # then a default material
        mesh_material = MeshPhongMaterial(color=color,
                                          polygonOffset=True,
                                          polygonOffsetFactor=1,
                                          polygonOffsetUnits=1,
                                          shininess=0.5,
                                          wireframe=False,
                                          side='DoubleSide')
        edges_material = MeshPhongMaterial(color='black',
                                           polygonOffset=True,
                                           polygonOffsetFactor=1,
                                           polygonOffsetUnits=1,
                                           shininess=0.5,
                                           wireframe=True)
        # create a mesh unique id
        mesh_id = uuid.uuid4().hex

        # finally create the mash
        shape_mesh = Mesh(geometry=mesh_geometry,
                          material=mesh_material,
                          name=mesh_id)
        edges_mesh = Mesh(geometry=mesh_geometry,
                          material=edges_material,
                          name=mesh_id)


        # a special display for the mesh
        camera_target = [0., 0., 0.]  # the point to look at
        camera_position = [0, 0., 100.]  # the camera initial position
        camera = PerspectiveCamera(position=camera_position,
                                   lookAt=camera_target,
                                   up=[0, 0, 1],
                                   fov=50,
                                   children=[DirectionalLight(color='#ffffff',
                                                              position=[50, 50, 50],
                                                              intensity=0.9)])
        scene_shp = Scene(children=[shape_mesh, edges_mesh, camera, AmbientLight(color='#101010')])

        renderer = Renderer(camera=camera,
                            background=self._background,
                            background_opacity=self._background_opacity,
                            scene=scene_shp,
                            controls=[OrbitControls(controlling=camera, target=camera_target)],
                            width=self._size[0],
                            height=self._size[1],
                            antialias=True)

        display(renderer)


    def DisplayShape(self,
                     shp,  # the TopoDS_Shape to be displayed
                     shape_color=default_shape_color,  # the default
                     render_edges=False,
                     edge_color=default_edge_color,
                     compute_uv_coords=False,
                     quality=1.0,
                     transparency=False,
                     opacity=1.,
                     topo_level='default',
                     update=False):
        """ Displays a topods_shape in the renderer instance.
        shp: the TopoDS_Shape to render
        shape_color: the shape color, in html corm, eg '#abe000'
        render_edges: optional, False by default. If True, compute and dislay all
                      edges as a linear interpolation of segments.
        edge_color: optional, black by default. The color used for edge rendering,
                    in html form eg '#ff00ee'
        compute_uv_coords: optional, false by default. If True, compute texture
                           coordinates (required if the shape has to be textured)
        quality: optional, 1.0 by default. If set to something lower than 1.0,
                      mesh will be more precise. If set to something higher than 1.0,
                      mesh will be less precise, i.e. lower numer of triangles.
        transparency: optional, False by default (opaque).
        opacity: optional, float, by default to 1 (opaque). if transparency is set to True,
                 0. is fully opaque, 1. is fully transparent.
        topo_level: "default" by default. The value should be either "compound", "shape", "vertex".
        update: optional, False by default. If True, render all the shapes.
        """
        if is_wire(shp) or is_edge(shp):
            self.AddCurveToScene(shp, shape_color)
        if topo_level != "default":
            t = TopologyExplorer(shp)
            map_type_and_methods = {"Solid": t.solids, "Face": t.faces, "Shell": t.shells,
                                    "Compound": t.compounds, "Compsolid": t.comp_solids}
            for subshape in map_type_and_methods[topo_level]():
                self.AddShapeToScene(subshape, shape_color, render_edges, edge_color, compute_uv_coords, quality,
                                     transparency, opacity)
        else:
            self.AddShapeToScene(shp, shape_color, render_edges, edge_color, compute_uv_coords, quality,
                                 transparency, opacity)
        if update:
            self.Display()


    def AddCurveToScene(self, shp, color):
        """ shp is either a TopoDS_Wire or a TopodS_Edge.
        """
        if is_edge(shp):
            pnts = discretize_edge(shp)
        elif is_wire(shp):
            pnts = discretize_wire(shp)
        np_edge_vertices = np.array(pnts, dtype=np.float32)
        np_edge_indices = np.arange(np_edge_vertices.shape[0], dtype=np.uint32)
        edge_geometry = BufferGeometry(attributes={
            'position': BufferAttribute(np_edge_vertices),
            'index'   : BufferAttribute(np_edge_indices)
        })
        edge_material = LineBasicMaterial(color=color, linewidth=2, fog=True)
        edge_lines = LineSegments(geometry=edge_geometry, material=edge_material)

        # Add geometries to pickable or non pickable objects
        self._displayed_pickable_objects.add(edge_lines)


    def AddShapeToScene(self,
                        shp,  # the TopoDS_Shape to be displayed
                        shape_color=default_shape_color,  # the default
                        render_edges=False,
                        edge_color=default_edge_color,
                        compute_uv_coords=False,
                        quality=1.0,
                        transparency=False,
                        opacity=1.):
        # first, compute the tesselation
        tess = Tesselator(shp)
        tess.Compute(uv_coords=compute_uv_coords,
                     compute_edges=render_edges,
                     mesh_quality=quality,
                     parallel=self._parallel)
        # get vertices and normals
        vertices_position = tess.GetVerticesPositionAsTuple()

        number_of_triangles = tess.ObjGetTriangleCount()
        number_of_vertices = len(vertices_position)

        # number of vertices should be a multiple of 3
        if number_of_vertices % 3 != 0:
            raise AssertionError("Wrong number of vertices")
        if number_of_triangles * 9 != number_of_vertices:
            raise AssertionError("Wrong number of triangles")

        # then we build the vertex and faces collections as numpy ndarrays
        np_vertices = np.array(vertices_position, dtype='float32').reshape(int(number_of_vertices / 3), 3)
        # Note: np_faces is just [0, 1, 2, 3, 4, 5, ...], thus arange is used
        np_faces = np.arange(np_vertices.shape[0], dtype='uint32')

        # set geometry properties
        buffer_geometry_properties = {'position': BufferAttribute(np_vertices),
                                      'index'   : BufferAttribute(np_faces)}
        if self._compute_normals_mode == NORMAL.SERVER_SIDE:
            # get the normal list, converts to a numpy ndarray. This should not raise
            # any issue, since normals have been computed by the server, and are available
            # as a list of floats
            np_normals = np.array(tess.GetNormalsAsTuple(), dtype='float32').reshape(-1, 3)
            # quick check
            if np_normals.shape != np_vertices.shape:
                raise AssertionError("Wrong number of normals/shapes")
            buffer_geometry_properties['normal'] = BufferAttribute(np_normals)

        # build a BufferGeometry instance
        shape_geometry = BufferGeometry(attributes=buffer_geometry_properties)

        # if the client has to render normals, add the related js instructions
        if self._compute_normals_mode == NORMAL.CLIENT_SIDE:
            shape_geometry.exec_three_obj_method('computeVertexNormals')

        # then a default material
        shp_material = self._material(shape_color, transparent=transparency, opacity=opacity)
        # create a mesh unique id
        mesh_id = uuid.uuid4().hex

        # finally create the mash
        shape_mesh = Mesh(geometry=shape_geometry,
                          material=shp_material,
                          name=mesh_id)


        # and to the dict of shapes, to have a mapping between meshes and shapes
        self._shapes[mesh_id] = shp

        # edge rendering, if set to True
        edge_lines = None
        if render_edges:
            edges = list(map(lambda i_edge: [tess.GetEdgeVertex(i_edge, i_vert) for i_vert in range(tess.ObjEdgeGetVertexCount(i_edge))], range(tess.ObjGetEdgeCount())))
            edges = list(filter(lambda edge: len(edge) == 2, edges))
            np_edge_vertices = np.array(edges, dtype=np.float32).reshape(-1, 3)
            np_edge_indices = np.arange(np_edge_vertices.shape[0], dtype=np.uint32)
            edge_geometry = BufferGeometry(attributes={
                'position': BufferAttribute(np_edge_vertices),
                'index'   : BufferAttribute(np_edge_indices)
            })
            edge_material = LineBasicMaterial(color=edge_color, linewidth=1)
            edge_lines = LineSegments(geometry=edge_geometry, material=edge_material)

        # Add geometries to pickable or non pickable objects
        self._displayed_pickable_objects.add(shape_mesh)
        if render_edges:
            self._displayed_non_pickable_objects.add(edge_lines)

    def _scale(self, vec):
        r = self._bb.diagonal * 2.5
        n = np.linalg.norm(vec)
        new_vec = [v / n * r for v in vec]
        return self._add(new_vec, self._bb.center)

    def _add(self, vec1, vec2):
        return list(v1 + v2 for v1, v2 in zip(vec1, vec2))

    def _material(self, color, transparent=False, opacity=1.0):
        material = CustomMaterial("standard")
        material.color = color
        material.clipping = True
        material.side = "DoubleSide"
        material.alpha = 0.7
        material.polygonOffset = False
        material.polygonOffsetFactor = 1
        material.polygonOffsetUnits = 1
        material.transparent = transparent
        material.opacity = opacity
        material.update("metalness", 0.3)
        material.update("roughness", 0.8)
        return material

    def EraseAll(self):
        self._shapes = {}
        self._displayed_pickable_objects = Group()
        self._current_shape_selection = None
        self._current_mesh_selection = None
        self._current_selection_material = None
        self._renderer.scene = Scene(children=[])


    def Display(self):
        # Get the overall bounding box
        if self._shapes:
            self._bb = BoundingBox([self._shapes.values()])
        else:  # if nothing registered yet, create a fake bb
            self._bb = BoundingBox([[BRepPrimAPI_MakeSphere(5.).Shape()]])
        bb_max = self._bb.max
        bb_diag = 2 * self._bb.diagonal

        # Set up camera
        camera_target = self._bb.center
        camera_position = self._scale([1, 1, 1])


        self._camera = CombinedCamera(position=camera_position,
                                      width=self._size[0], height=self._size[1],
                                      far=10 * bb_diag, orthoFar=10 * bb_diag)
        self._camera.up = (0.0, 0.0, 1.0)
        self._camera.lookAt(camera_target)
        self._camera.mode = 'orthographic'
        self._camera_target = camera_target
        self._camera.position = camera_position

        # Set up lights in every of the 8 corners of the global bounding box
        key_lights = [
            DirectionalLight(color='white', position=position, intensity=0.12)
            for position in list(itertools.product((-bb_diag, bb_diag), (-bb_diag, bb_diag), (-bb_diag, bb_diag)))
        ]
        ambient_light = AmbientLight(intensity=1.0)

        # Set up Helpers
        self.axes = Axes(bb_center=self._bb.center, length=bb_max * 1.1)
        self.grid = Grid(bb_center=self._bb.center, maximum=bb_max, colorCenterLine='#aaa', colorGrid='#ddd')

        # Set up scene
        environment = self.axes.axes + key_lights + [ambient_light, self.grid.grid, self._camera]

        scene_shp = Scene(children=[self._displayed_pickable_objects,
                                    self._displayed_non_pickable_objects] + environment)

        # Set up Controllers
        self._controller = OrbitControls(controlling=self._camera, target=camera_target)

        self._renderer = Renderer(camera=self._camera,
                                  background=self._background,
                                  background_opacity=self._background_opacity,
                                  scene=scene_shp,
                                  controls=[self._controller, self._picker],
                                  width=self._size[0],
                                  height=self._size[1],
                                  antialias=True)

        # needs to be done after setup of camera
        self.grid.set_rotation((math.pi / 2.0, 0, 0, "XYZ"))
        self.grid.set_position((0, 0, 0))

        # Workaround: Zoom forth and back to update frame. Sometimes necessary :(
        self._camera.zoom = 1.01
        self._update()
        self._camera.zoom = 1.0
        self._update()

        # then display both 3d widgets and webui
        display(HBox([self._renderer, self.html]))

    def _update(self):
        self._controller.exec_three_obj_method('update')

    def __repr__(self):
        self.Display()
        return ""
Exemple #15
0
class Layer(ABC):
    """
    Abstract Layer class. Not meant to be used on its own.
    """
    _LAYER_NAME = "layer"
    def __init__(self,*args, **kwargs) -> None:
        """
        Abstract Layer class. Not meant to be used on its own.
        """
        if len(args) > 0:
            warn(f'Unused args : {args}')
        if len(kwargs) > 0:
            warn(f'Unused kwargs : {kwargs}')
        self._id = None
        self._objects = []
        self._group = None
    @abstractmethod
    def get_bounding_box(self) -> Tuple[Coord3, Coord3]:
        """
        Gets the bounding box as two coordinates representing opposite corners.
        """
        ...
    @abstractmethod
    def get_preferred_camera_view(self) -> Coord3:
        """
        Gets preferred camera view, as a target tuple to orbit around
        """
        return None
    @property
    def group(self) -> Group:
        """
        The pythreejs Group for all the objects in the layer
        """
        if self._group is None:
            self._group = Group()
            for obj in self._objects:
                self._group.add(obj)

        return self._group
    @property
    def affine(self) -> np.ndarray:
        """
        The affine transform of the entire layer
        """
        return self.group.matrix

    def set_affine(self, a: np.ndarray):
        """
        Set affine transform for the entire layer
        """
        self._affine = a
        sc = self.group
        sc.matrix = self._affine
    def rotate(self,
        x:float,
        y:float,
        z:float,
        order:str="XYZ"
        ):
        """
        Sets the rotation of the entire layer.
        x,y,z: the rotation around each axes, in degrees.
        order: the order of rotation
        """
        sc = self.group
        sc.rotation = (x,y,z,order)
    def translate(self, x,y,z):
        """
        Sets the translation of the entire layer.
        x,y,z: translation along each axis
        """
        sc = self.group
        xyz = np.array(sc.position)
        sc.position = tuple(xyz + [x,y,z])
    def on_click(self, picker):
        """
        Click callback. See
        https://pythreejs.readthedocs.io/en/stable/api/controls/Picker_autogen.html#
        for docs on picker
        args:
            picker: Picker instance
        """
        s = f"""
            layer: {self._LAYER_NAME}
            point clicked: {picker.point}
            """

        return s
    def _on_click(self, picker):
        return self.on_click(picker)
Exemple #16
0
    def __init__(self):
        self._view_items = []
        self._meshes = []

        # the group of 3d and 2d objects to render
        self._displayed_pickable_objects = Group()
Exemple #17
0
class CadqueryView(object):
    def __init__(self,
                 width=600,
                 height=400,
                 quality=0.5,
                 render_edges=True,
                 default_mesh_color=None,
                 default_edge_color=None,
                 info=None):
        self.width = width
        self.height = height
        self.quality = quality
        self.render_edges = render_edges
        self.info = info

        self.features = ["mesh", "edges"]

        self.bb = None

        self.default_mesh_color = default_mesh_color or self._format_color(
            166, 166, 166)
        self.default_edge_color = default_edge_color or self._format_color(
            128, 128, 128)
        self.pick_color = self._format_color(232, 176, 36)

        self.shapes = []
        self.pickable_objects = Group()
        self.pick_last_mesh = None
        self.pick_last_mesh_color = None
        self.pick_mapping = []

        self.camera = None
        self.axes = None
        self.grid = None
        self.scene = None
        self.controller = None
        self.renderer = None

        self.savestate = None

    def _format_color(self, r, g, b):
        return '#%02x%02x%02x' % (r, g, b)

    def _material(self, color, transparent=False, opacity=1.0):
        material = CustomMaterial("standard")
        material.color = color
        material.clipping = True
        material.side = "DoubleSide"
        material.alpha = 0.7
        material.polygonOffset = False
        material.polygonOffsetFactor = 1
        material.polygonOffsetUnits = 1
        material.transparent = transparent
        material.opacity = opacity
        material.update("metalness", 0.3)
        material.update("roughness", 0.8)
        return material

    def _render_shape(self,
                      shape_index,
                      shape=None,
                      edges=None,
                      vertices=None,
                      mesh_color=None,
                      edge_color=None,
                      vertex_color=None,
                      render_edges=False,
                      edge_width=1,
                      vertex_width=5,
                      deflection=0.05,
                      transparent=False,
                      opacity=1.0):

        edge_list = None
        edge_lines = None
        points = None
        shape_mesh = None

        if shape is not None:
            if mesh_color is None:
                mesh_color = self.default_mesh_color
            if edge_color is None:
                edge_color = self.default_edge_color
            if vertex_color is None:
                vertex_color = self.default_edge_color  # same as edge_color

            # BEGIN copy
            # The next lines are copied with light modifications from
            # https://github.com/tpaviot/pythonocc-core/blob/master/src/Display/WebGl/jupyter_renderer.py

            # first, compute the tesselation
            tess = Tesselator(shape)
            tess.Compute(uv_coords=False,
                         compute_edges=render_edges,
                         mesh_quality=self.quality,
                         parallel=True)

            # get vertices and normals
            vertices_position = tess.GetVerticesPositionAsTuple()

            number_of_triangles = tess.ObjGetTriangleCount()
            number_of_vertices = len(vertices_position)

            # number of vertices should be a multiple of 3
            if number_of_vertices % 3 != 0:
                raise AssertionError("Wrong number of vertices")
            if number_of_triangles * 9 != number_of_vertices:
                raise AssertionError("Wrong number of triangles")

            # then we build the vertex and faces collections as numpy ndarrays
            np_vertices = np.array(vertices_position, dtype='float32')\
                            .reshape(int(number_of_vertices / 3), 3)
            # Note: np_faces is just [0, 1, 2, 3, 4, 5, ...], thus arange is used
            np_faces = np.arange(np_vertices.shape[0], dtype='uint32')

            # compute normals
            np_normals = np.array(tess.GetNormalsAsTuple(),
                                  dtype='float32').reshape(-1, 3)
            if np_normals.shape != np_vertices.shape:
                raise AssertionError("Wrong number of normals/shapes")

            # build a BufferGeometry instance
            shape_geometry = BufferGeometry(
                attributes={
                    'position': BufferAttribute(np_vertices),
                    'index': BufferAttribute(np_faces),
                    'normal': BufferAttribute(np_normals)
                })

            shp_material = self._material(mesh_color,
                                          transparent=True,
                                          opacity=opacity)

            shape_mesh = Mesh(geometry=shape_geometry,
                              material=shp_material,
                              name="mesh_%d" % shape_index)

            if render_edges:
                edge_list = list(
                    map(
                        lambda i_edge: [
                            tess.GetEdgeVertex(i_edge, i_vert)
                            for i_vert in range(
                                tess.ObjEdgeGetVertexCount(i_edge))
                        ], range(tess.ObjGetEdgeCount())))

            # END copy

        if vertices is not None:
            vertices_list = []
            for vertex in vertices:
                p = BRep_Tool.Pnt(vertex)
                vertices_list.append((p.X(), p.Y(), p.Z()))
            vertices_list = np.array(vertices_list, dtype=np.float32)

            attributes = {
                "position": BufferAttribute(vertices_list, normalized=False)
            }

            mat = PointsMaterial(color=vertex_color,
                                 sizeAttenuation=False,
                                 size=vertex_width)
            geom = BufferGeometry(attributes=attributes)
            points = Points(geometry=geom, material=mat)

        if edges is not None:
            edge_list = [discretize_edge(edge, deflection) for edge in edges]

        if edge_list is not None:
            edge_list = _flatten(list(map(_explode, edge_list)))
            lines = LineSegmentsGeometry(positions=edge_list)
            mat = LineMaterial(linewidth=edge_width, color=edge_color)
            edge_lines = LineSegments2(lines,
                                       mat,
                                       name="edges_%d" % shape_index)

        if shape_mesh is not None or edge_lines is not None or points is not None:
            index_mapping = {"mesh": None, "edges": None, "shape": shape_index}
            if shape_mesh is not None:
                ind = len(self.pickable_objects.children)
                self.pickable_objects.add(shape_mesh)
                index_mapping["mesh"] = ind
            if edge_lines is not None:
                ind = len(self.pickable_objects.children)
                self.pickable_objects.add(edge_lines)
                index_mapping["edges"] = ind
            if points is not None:
                ind = len(self.pickable_objects.children)
                self.pickable_objects.add(points)
                index_mapping["mesh"] = ind
            self.pick_mapping.append(index_mapping)

    def get_transparent(self):
        # if one object is transparent, all are
        return self.pickable_objects.children[0].material.transparent

    def _scale(self, vec):
        r = self.bb.diagonal * 2.5
        n = np.linalg.norm(vec)
        new_vec = [v / n * r for v in vec]
        return self._add(new_vec, self.bb.center)

    def _add(self, vec1, vec2):
        return list(v1 + v2 for v1, v2 in zip(vec1, vec2))

    def _sub(self, vec1, vec2):
        return list(v1 - v2 for v1, v2 in zip(vec1, vec2))

    def _norm(self, vec):
        n = np.linalg.norm(vec)
        return [v / n for v in vec]

    def _minus(self, vec):
        return [-v for v in vec]

    def direction(self):
        return self._norm(self._sub(self.camera.position, self.bb.center))

    def set_plane(self, i):
        plane = self.renderer.clippingPlanes[i]
        plane.normal = self._minus(self.direction())

    def _update(self):
        self.controller.exec_three_obj_method('update')
        pass

    def _reset(self):
        self.camera.rotation, self.controller.target = self.savestate
        self.camera.position = self._scale((1, 1, 1))
        self.camera.zoom = 1.0
        self._update()

    # UI Handler

    def change_view(self, typ, directions):
        def reset(b):
            self._reset()

        def refit(b):
            self.camera.zoom = 1.0
            self._update()

        def change(b):
            self.camera.position = self._scale(directions[typ])
            self._update()

        if typ == "fit":
            return refit
        elif typ == "reset":
            return reset
        else:
            return change

    def bool_or_new(self, val):
        return val if isinstance(val, bool) else val["new"]

    def toggle_axes(self, change):
        self.axes.set_visibility(self.bool_or_new(change))

    def toggle_grid(self, change):
        self.grid.set_visibility(self.bool_or_new(change))

    def toggle_center(self, change):
        self.grid.set_center(self.bool_or_new(change))
        self.axes.set_center(self.bool_or_new(change))

    def toggle_ortho(self, change):
        self.camera.mode = 'orthographic' if self.bool_or_new(
            change) else 'perspective'

    def toggle_transparent(self, change):
        value = self.bool_or_new(change)
        for i in range(0, len(self.pickable_objects.children), 2):
            self.pickable_objects.children[i].material.transparent = value

    def toggle_black_edges(self, change):
        value = self.bool_or_new(change)
        for obj in self.pickable_objects.children:
            if isinstance(obj, LineSegments2):
                _, ind = obj.name.split("_")
                ind = int(ind)
                if isinstance(self.shapes[ind]["shape"][0], TopoDS_Compound):
                    obj.material.color = "#000" if value else self.default_edge_color

    def set_visibility(self, ind, i, state):
        feature = self.features[i]
        group_index = self.pick_mapping[ind][feature]
        if group_index is not None:
            self.pickable_objects.children[group_index].visible = (state == 1)

    def change_visibility(self, mapping):
        def f(states):
            diffs = state_diff(states.get("old"), states.get("new"))
            for diff in diffs:
                obj, val = _decomp(diff)
                self.set_visibility(mapping[obj], val["icon"], val["new"])

        return f

    def pick(self, value):
        if self.pick_last_mesh != value.owner.object:
            # Reset
            if value.owner.object is None or self.pick_last_mesh is not None:
                self.pick_last_mesh.material.color = self.pick_last_mesh_color
                self.pick_last_mesh = None
                self.pick_last_mesh_color = None

            # Change highlighted mesh
            if isinstance(value.owner.object, Mesh):
                _, ind = value.owner.object.name.split("_")
                shape = self.shapes[int(ind)]
                bbox = BoundingBox([shape["shape"]])

                self.info.bb_info(shape["name"],
                                  ((bbox.xmin, bbox.xmax),
                                   (bbox.ymin, bbox.ymax),
                                   (bbox.zmin, bbox.zmax), bbox.center))
                self.pick_last_mesh = value.owner.object
                self.pick_last_mesh_color = self.pick_last_mesh.material.color
                self.pick_last_mesh.material.color = self.pick_color

    def clip(self, index):
        def f(change):
            self.renderer.clippingPlanes[index].constant = change["new"]

        return f

    # public methods to add shapes and render the view

    def add_shape(self, name, shape, color="#ff0000"):
        self.shapes.append({"name": name, "shape": shape, "color": color})

    def is_ortho(self):
        return (self.camera.mode == "orthographic")

    def is_transparent(self):
        return self.pickable_objects.children[0].material.transparent

    def render(self, position=None, rotation=None, zoom=None):

        # Render all shapes
        for i, shape in enumerate(self.shapes):
            s = shape["shape"]
            # Assume that all are edges when first element is an edge
            if is_edge(s[0]):
                self._render_shape(i,
                                   edges=s,
                                   render_edges=True,
                                   edge_color=shape["color"],
                                   edge_width=3)
            elif is_vertex(s[0]):
                self._render_shape(i,
                                   vertices=s,
                                   render_edges=False,
                                   vertex_color=shape["color"],
                                   vertex_width=6)
            else:
                # shape has only 1 object, hence first=True
                self._render_shape(i,
                                   shape=s[0],
                                   render_edges=True,
                                   mesh_color=shape["color"])

        # Get the overall bounding box
        self.bb = BoundingBox([shape["shape"] for shape in self.shapes])

        bb_max = self.bb.max
        bb_diag = 2 * self.bb.diagonal

        # Set up camera
        camera_target = self.bb.center
        camera_position = self._scale(
            [1, 1, 1] if position is None else position)
        camera_zoom = 1.0 if zoom is None else zoom

        self.camera = CombinedCamera(position=camera_position,
                                     width=self.width,
                                     height=self.height,
                                     far=10 * bb_diag,
                                     orthoFar=10 * bb_diag)
        self.camera.up = (0.0, 0.0, 1.0)
        self.camera.lookAt(camera_target)
        self.camera.mode = 'orthographic'
        self.camera.position = camera_position
        if rotation is not None:
            self.camera.rotation = rotation

        # Set up lights in every of the 8 corners of the global bounding box
        key_lights = [
            DirectionalLight(color='white', position=position, intensity=0.12)
            for position in list(
                itertools.product((-bb_diag, bb_diag), (-bb_diag,
                                                        bb_diag), (-bb_diag,
                                                                   bb_diag)))
        ]
        ambient_light = AmbientLight(intensity=1.0)

        # Set up Helpers
        self.axes = Axes(bb_center=self.bb.center, length=bb_max * 1.1)
        self.grid = Grid(bb_center=self.bb.center,
                         maximum=bb_max,
                         colorCenterLine='#aaa',
                         colorGrid='#ddd')

        # Set up scene
        environment = self.axes.axes + key_lights + [
            ambient_light, self.grid.grid, self.camera
        ]
        self.scene = Scene(children=environment + [self.pickable_objects])

        # Set up Controllers
        self.controller = OrbitControls(controlling=self.camera,
                                        target=camera_target)

        self.picker = Picker(controlling=self.pickable_objects,
                             event='dblclick')
        self.picker.observe(self.pick)

        # Create Renderer instance
        self.renderer = Renderer(scene=self.scene,
                                 camera=self.camera,
                                 controls=[self.controller, self.picker],
                                 antialias=True,
                                 width=self.width,
                                 height=self.height)

        self.renderer.localClippingEnabled = True
        self.renderer.clippingPlanes = [
            Plane((1, 0, 0), self.grid.size / 2),
            Plane((0, 1, 0), self.grid.size / 2),
            Plane((0, 0, 1), self.grid.size / 2)
        ]

        # needs to be done after setup of camera
        self.grid.set_rotation((math.pi / 2.0, 0, 0, "XYZ"))
        self.grid.set_position((0, 0, 0))

        self.savestate = (self.camera.rotation, self.controller.target)

        # Workaround: Zoom forth and back to update frame. Sometimes necessary :(
        self.camera.zoom = camera_zoom + 0.01
        self._update()
        self.camera.zoom = camera_zoom
        self._update()

        return self.renderer
Exemple #18
0
def get_objects_renderer(
    root_node: coin.SoSeparator,
    names: List[str],
    renderer_config: RendererConfig = RendererConfig()
) -> Tuple[Renderer, HTML]:
    """
    Return a `Renderer` and `HTML` for rendering any coin root node of a scene graph containing LineSets
    or FaceSets inside Jupyter notebook.
    """
    view_width = renderer_config.view_width
    view_height = renderer_config.view_height
    geometries = Group()
    part_indices = [
    ]  # contains the partIndex indices that relate triangle faces to shape faces

    render_face_set = True
    i = 0
    for res in bfs_traversal(root_node, print_tree=False):
        if isinstance(res[0], coin.SoIndexedFaceSet) and render_face_set:
            render_face_set = False
            continue
        if isinstance(res[0], coin.SoIndexedFaceSet):
            render_face_set = True
            part_index_list = list(res[0].partIndex)
            part_indices.append(part_index_list)
        elif isinstance(res[0], coin.SoIndexedLineSet):
            pass
        else:
            continue

        geoms = create_geometry(res,
                                show_edges=renderer_config.show_edges,
                                show_faces=renderer_config.show_faces)

        for obj3d in geoms:
            obj3d.name = str(res[3]) + " " + str(
                i)  #the name of the object is `object_index i`
            i += 1

        for geom in geoms:
            if renderer_config.show_normals:
                helper = VertexNormalsHelper(geom)
                geometries.add(helper)
            if renderer_config.show_mesh and not isinstance(geom, Line):
                geometries.add(get_line_geometries(geom))
            else:
                geometries.add(geom)

    light = PointLight(color="white",
                       position=[40, 40, 40],
                       intensity=1.0,
                       castShadow=True)
    ambient_light = AmbientLight(intensity=0.5)
    camera = PerspectiveCamera(position=[0, -40, 20],
                               fov=40,
                               aspect=view_width / view_height)
    children = [camera, light, ambient_light]
    children.append(geometries)
    scene = Scene(children=children)
    scene.background = "#65659a"

    controls = [OrbitControls(controlling=camera)]
    html = HTML()

    if renderer_config.selection_mode:
        html, picker = generate_picker(geometries, part_indices, "mousemove")
        controls.append(picker)

    renderer = Renderer(camera=camera,
                        scene=scene,
                        controls=controls,
                        width=view_width,
                        height=view_height)
    return (renderer, html)
Exemple #19
0
class CadqueryView(object):
    def __init__(
        self,
        width=600,
        height=400,
        quality=0.1,
        angular_tolerance=0.1,
        edge_accuracy=0.01,
        render_edges=True,
        default_mesh_color=None,
        default_edge_color=None,
        info=None,
        timeit=False,
    ):

        self.width = width
        self.height = height
        self.quality = quality
        self.angular_tolerance = angular_tolerance
        self.edge_accuracy = edge_accuracy
        self.render_edges = render_edges
        self.info = info
        self.timeit = timeit

        self.camera_distance_factor = 6
        self.camera_initial_zoom = 2.5

        self.features = ["mesh", "edges"]

        self.bb = None

        self.default_mesh_color = default_mesh_color or self._format_color(166, 166, 166)
        self.default_edge_color = default_edge_color or self._format_color(128, 128, 128)
        self.pick_color = self._format_color(232, 176, 36)

        self.shapes = []
        self.pickable_objects = Group()
        self.pick_last_mesh = None
        self.pick_last_mesh_color = None
        self.pick_mapping = []

        self.camera = None
        self.axes = None
        self.grid = None
        self.scene = None
        self.controller = None
        self.renderer = None

        self.savestate = None

    def _format_color(self, r, g, b):
        return "#%02x%02x%02x" % (r, g, b)

    def _start_timer(self):
        return time.time() if self.timeit else None

    def _stop_timer(self, msg, start):
        if self.timeit:
            print("%20s: %7.2f sec" % (msg, time.time() - start))

    def _material(self, color, transparent=False, opacity=1.0):
        material = CustomMaterial("standard")
        material.color = color
        material.clipping = True
        material.side = "DoubleSide"
        material.alpha = 0.7
        material.polygonOffset = True
        material.polygonOffsetFactor = 1
        material.polygonOffsetUnits = 1
        material.transparent = transparent
        material.opacity = opacity
        material.update("metalness", 0.3)
        material.update("roughness", 0.8)
        return material

    def _render_shape(
        self,
        shape_index,
        shape=None,
        edges=None,
        vertices=None,
        mesh_color=None,
        edge_color=None,
        vertex_color=None,
        render_edges=False,
        edge_width=1,
        vertex_width=5,
        transparent=False,
        opacity=1.0,
    ):

        edge_list = None
        edge_lines = None
        points = None
        shape_mesh = None

        start_render_time = self._start_timer()
        if shape is not None:
            if mesh_color is None:
                mesh_color = self.default_mesh_color
            if edge_color is None:
                edge_color = self.default_edge_color
            if vertex_color is None:
                vertex_color = self.default_edge_color  # same as edge_color

            # Compute the tesselation
            start_tesselation_time = self._start_timer()

            np_vertices, np_triangles, np_normals = tessellate(
                shape, self.quality, self.angular_tolerance
            )

            if np_normals.shape != np_vertices.shape:
                raise AssertionError("Wrong number of normals/shapes")

            self._stop_timer("tesselation time", start_tesselation_time)

            # build a BufferGeometry instance
            shape_geometry = BufferGeometry(
                attributes={
                    "position": BufferAttribute(np_vertices),
                    "index": BufferAttribute(np_triangles.ravel()),
                    "normal": BufferAttribute(np_normals),
                }
            )

            shp_material = self._material(mesh_color, transparent=True, opacity=opacity)

            shape_mesh = Mesh(
                geometry=shape_geometry, material=shp_material, name="mesh_%d" % shape_index
            )

            if render_edges:
                edges = get_edges(shape)

        if vertices is not None:
            vertices_list = []
            for vertex in vertices:
                vertices_list.append(get_point(vertex))
            vertices_list = np.array(vertices_list, dtype=np.float32)

            attributes = {"position": BufferAttribute(vertices_list, normalized=False)}

            mat = PointsMaterial(color=vertex_color, sizeAttenuation=False, size=vertex_width)
            geom = BufferGeometry(attributes=attributes)
            points = Points(geometry=geom, material=mat)

        if edges is not None:
            start_discretize_time = self._start_timer()
            edge_list = [discretize_edge(edge, self.edge_accuracy) for edge in edges]
            self._stop_timer("discretize time", start_discretize_time)

        if edge_list is not None:
            edge_list = flatten(list(map(explode, edge_list)))
            lines = LineSegmentsGeometry(positions=edge_list)
            mat = LineMaterial(linewidth=edge_width, color=edge_color)
            edge_lines = LineSegments2(lines, mat, name="edges_%d" % shape_index)

        if shape_mesh is not None or edge_lines is not None or points is not None:
            index_mapping = {"mesh": None, "edges": None, "shape": shape_index}
            if shape_mesh is not None:
                ind = len(self.pickable_objects.children)
                self.pickable_objects.add(shape_mesh)
                index_mapping["mesh"] = ind
            if edge_lines is not None:
                ind = len(self.pickable_objects.children)
                self.pickable_objects.add(edge_lines)
                index_mapping["edges"] = ind
            if points is not None:
                ind = len(self.pickable_objects.children)
                self.pickable_objects.add(points)
                index_mapping["mesh"] = ind
            self.pick_mapping.append(index_mapping)
        self._stop_timer("shape render time", start_render_time)

    def get_transparent(self):
        # if one object is transparent, all are
        return self.pickable_objects.children[0].material.transparent

    def _scale(self, vec):
        r = self.bb.max_dist_from_center() * self.camera_distance_factor
        n = np.linalg.norm(vec)
        new_vec = [v / n * r for v in vec]
        return new_vec

    def _add(self, vec1, vec2):
        return list(v1 + v2 for v1, v2 in zip(vec1, vec2))

    def _sub(self, vec1, vec2):
        return list(v1 - v2 for v1, v2 in zip(vec1, vec2))

    def _norm(self, vec):
        n = np.linalg.norm(vec)
        return [v / n for v in vec]

    def _minus(self, vec):
        return [-v for v in vec]

    def direction(self):
        return self._norm(self._sub(self.camera.position, self.bb.center))

    def set_plane(self, i):
        plane = self.renderer.clippingPlanes[i]
        plane.normal = self._minus(self.direction())

    def _update(self):
        self.controller.exec_three_obj_method("update")
        pass

    def _reset(self):
        self.camera.rotation, self.controller.target = self.savestate
        self.camera.position = self._add(self.bb.center, self._scale((1, 1, 1)))
        self.camera.zoom = self.camera_initial_zoom
        self._update()

    # UI Handler

    def change_view(self, typ, directions):
        def reset(b):
            self._reset()

        def refit(b):
            self.camera.zoom = self.camera_initial_zoom
            self._update()

        def change(b):
            self.camera.position = self._add(self.bb.center, self._scale(directions[typ]))
            self._update()

        if typ == "fit":
            return refit
        elif typ == "reset":
            return reset
        else:
            return change

    def bool_or_new(self, val):
        return val if isinstance(val, bool) else val["new"]

    def toggle_axes(self, change):
        self.axes.set_visibility(self.bool_or_new(change))

    def toggle_grid(self, change):
        self.grid.set_visibility(self.bool_or_new(change))

    def toggle_center(self, change):
        self.grid.set_center(self.bool_or_new(change))
        self.axes.set_center(self.bool_or_new(change))

    def toggle_ortho(self, change):
        self.camera.mode = "orthographic" if self.bool_or_new(change) else "perspective"

    def toggle_transparent(self, change):
        value = self.bool_or_new(change)
        for i in range(0, len(self.pickable_objects.children), 2):
            self.pickable_objects.children[i].material.transparent = value

    def toggle_black_edges(self, change):
        value = self.bool_or_new(change)
        for obj in self.pickable_objects.children:
            if isinstance(obj, LineSegments2):
                _, ind = obj.name.split("_")
                ind = int(ind)
                if is_compound(self.shapes[ind]["shape"][0]):
                    obj.material.color = "#000" if value else self.default_edge_color

    def set_visibility(self, ind, i, state):
        feature = self.features[i]
        group_index = self.pick_mapping[ind][feature]
        if group_index is not None:
            self.pickable_objects.children[group_index].visible = state == 1

    def change_visibility(self, mapping):
        def f(states):
            diffs = state_diff(states.get("old"), states.get("new"))
            for diff in diffs:
                [[obj, val]] = diff.items()
                self.set_visibility(mapping[obj], val["icon"], val["new"])

        return f

    def pick(self, value):
        if self.pick_last_mesh != value.owner.object:
            # Reset
            if value.owner.object is None or self.pick_last_mesh is not None:
                self.pick_last_mesh.material.color = self.pick_last_mesh_color
                self.pick_last_mesh = None
                self.pick_last_mesh_color = None

            # Change highlighted mesh
            if isinstance(value.owner.object, Mesh):
                _, ind = value.owner.object.name.split("_")
                shape = self.shapes[int(ind)]
                bbox = BoundingBox([shape["shape"]])

                self.info.bb_info(
                    shape["name"],
                    (
                        (bbox.xmin, bbox.xmax),
                        (bbox.ymin, bbox.ymax),
                        (bbox.zmin, bbox.zmax),
                        bbox.center,
                    ),
                )
                self.pick_last_mesh = value.owner.object
                self.pick_last_mesh_color = self.pick_last_mesh.material.color
                self.pick_last_mesh.material.color = self.pick_color

    def clip(self, index):
        def f(change):
            self.renderer.clippingPlanes[index].constant = change["new"]

        return f

    # public methods to add shapes and render the view

    def add_shape(self, name, shape, color="#ff0000"):
        self.shapes.append({"name": name, "shape": shape, "color": color})

    def is_ortho(self):
        return self.camera.mode == "orthographic"

    def is_transparent(self):
        return self.pickable_objects.children[0].material.transparent

    def render(self, position=None, rotation=None, zoom=2.5):
        self.camera_initial_zoom = zoom

        start_render_time = self._start_timer()
        # Render all shapes
        for i, shape in enumerate(self.shapes):
            s = shape["shape"]
            # Assume that all are edges when first element is an edge
            if is_edge(s[0]):
                self._render_shape(
                    i, edges=s, render_edges=True, edge_color=shape["color"], edge_width=3
                )
            elif is_vertex(s[0]):
                self._render_shape(
                    i, vertices=s, render_edges=False, vertex_color=shape["color"], vertex_width=6
                )
            else:
                # shape has only 1 object, hence first=True
                self._render_shape(i, shape=s[0], render_edges=True, mesh_color=shape["color"])

        # Get the overall bounding box
        self.bb = BoundingBox([shape["shape"] for shape in self.shapes])

        bb_max = self.bb.max
        orbit_radius = 2 * self.bb.max_dist_from_center()

        # Set up camera
        camera_target = self.bb.center
        camera_up = (0.0, 0.0, 1.0)

        if rotation != (0, 0, 0):
            position = rotate(position, *rotation)

        camera_position = self._add(
            self.bb.center, self._scale([1, 1, 1] if position is None else self._scale(position))
        )

        self.camera = CombinedCamera(
            position=camera_position,
            width=self.width,
            height=self.height,
            far=10 * orbit_radius,
            orthoFar=10 * orbit_radius,
        )
        self.camera.up = camera_up

        self.camera.mode = "orthographic"
        self.camera.position = camera_position

        # Set up lights in every of the 8 corners of the global bounding box
        positions = list(itertools.product(*[(-orbit_radius, orbit_radius)] * 3))
        key_lights = [
            DirectionalLight(color="white", position=position, intensity=0.12)
            for position in positions
        ]
        ambient_light = AmbientLight(intensity=1.0)

        # Set up Helpers
        self.axes = Axes(bb_center=self.bb.center, length=bb_max * 1.1)
        self.grid = Grid(
            bb_center=self.bb.center, maximum=bb_max, colorCenterLine="#aaa", colorGrid="#ddd"
        )

        # Set up scene
        environment = self.axes.axes + key_lights + [ambient_light, self.grid.grid, self.camera]
        self.scene = Scene(children=environment + [self.pickable_objects])

        # Set up Controllers
        self.controller = OrbitControls(
            controlling=self.camera, target=camera_target, target0=camera_target
        )

        # Update controller to instantiate camera position
        self.camera.zoom = zoom
        self._update()

        self.picker = Picker(controlling=self.pickable_objects, event="dblclick")
        self.picker.observe(self.pick)

        # Create Renderer instance
        self.renderer = Renderer(
            scene=self.scene,
            camera=self.camera,
            controls=[self.controller, self.picker],
            antialias=True,
            width=self.width,
            height=self.height,
        )

        self.renderer.localClippingEnabled = True
        self.renderer.clippingPlanes = [
            Plane((1, 0, 0), self.grid.size / 2),
            Plane((0, 1, 0), self.grid.size / 2),
            Plane((0, 0, 1), self.grid.size / 2),
        ]

        # needs to be done after setup of camera
        self.grid.set_rotation((math.pi / 2.0, 0, 0, "XYZ"))
        self.grid.set_position((0, 0, 0))

        self.savestate = (self.camera.rotation, self.controller.target)

        self._stop_timer("overall render time", start_render_time)

        return self.renderer