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 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
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 __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
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)
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=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 ""
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("")
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 ""
def __init__(self, filename, scale=1.0): Group.__init__(self) self._dae = Collada(filename) self._load_mesh(self._dae, scale=scale)
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 ""
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)
def __init__(self): self._view_items = [] self._meshes = [] # the group of 3d and 2d objects to render self._displayed_pickable_objects = Group()
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
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)
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