├── Examples ├── HelloBooleanOps0.py ├── HelloBooleanOps1.py ├── HelloBooleanOps2.py ├── HelloBooleanOps3.py ├── HelloBooleanOps4.py └── HelloPolyscope.py ├── LICENSE ├── PyGraphite ├── README.md ├── auto_gui.py ├── graphite_app.py ├── imgui_ext.py ├── mesh_grob_ops.py ├── polyscope_views.py ├── pygraphite.py └── terminal.py └── README.md /Examples/HelloBooleanOps0.py: -------------------------------------------------------------------------------- 1 | # HelloBooleanOps0: 2 | # How to create Graphite objects and display them in Polyscope 3 | # (there is no boolean op for now) 4 | 5 | import polyscope as ps 6 | import numpy as np 7 | import gompy 8 | OGF = gom.meta_types.OGF # All graphite classes are there, create a shortcut 9 | 10 | def register_graphite_object(O: OGF.MeshGrob): 11 | """ 12 | @brief Registers a graphite object to Polyscope 13 | @param[in] O the graphite object to be registered 14 | """ 15 | # One can directly access the points and the triangles in a graphite 16 | # object as numpy arrays, using the Editor interface, as follows: 17 | pts = np.asarray(O.I.Editor.get_points()) 18 | tri = np.asarray(O.I.Editor.get_triangles()) 19 | # Then the numpy arrays are sent to polyscope as follows: 20 | ps.register_surface_mesh(O.name,pts,tri) 21 | 22 | def register_graphite_objects(scene_graph: OGF.SceneGraph): 23 | """ 24 | @brief Registers all the graphite objects in a scene graph to Polyscope 25 | @param[in] scene_graph the Graphite scene graph 26 | """ 27 | for objname in dir(scene_graph.objects): 28 | register_graphite_object(scene_graph.resolve(objname)) 29 | 30 | # First create a SceneGraph. It will contain 31 | # all the subsequently created Graphite objects. 32 | scene_graph = OGF.SceneGraph() 33 | 34 | 35 | # Create an empty mesh. The specified string is the name of the mesh as 36 | # it will appear in Polyscope (and also how it is bound in the SceneGraph) 37 | S1 = OGF.MeshGrob('S1') 38 | 39 | 40 | # Create a sphere in the mesh 41 | # Functions that operate on a mesh are available through "Interfaces". All 42 | # the interfaces are accessed through S1.I. ... 43 | # To learn what you can do, try this: 44 | # In an interactive session, create a mesh (M = OGF.Mesh()), then use 45 | # autocompletion (M.I. then press 'tab') 46 | # All functions have an online help, use M.I.Shapes.create_sphere.help() to 47 | # display it (for instance) 48 | S1.I.Shapes.create_sphere(center=[0,0,0]) 49 | 50 | # Initialize polyscope 51 | ps.init() 52 | 53 | # Register all graphite objects to polyscope 54 | register_graphite_objects(scene_graph) 55 | 56 | # Enter polyscope main loop 57 | ps.show() 58 | -------------------------------------------------------------------------------- /Examples/HelloBooleanOps1.py: -------------------------------------------------------------------------------- 1 | # HelloBooleanOps1: 2 | # Let us create two spheres and compute their intersections 3 | # We shall also see how to change Polyscope graphic attributes 4 | 5 | import polyscope as ps 6 | import numpy as np 7 | import gompy 8 | OGF = gom.meta_types.OGF 9 | 10 | # ---------------------- Same as before -------------------------------- 11 | 12 | def register_graphite_object(O: OGF.MeshGrob): 13 | """ 14 | @brief Registers a graphite object to Polyscope 15 | @param[in] O the graphite object to be registered 16 | """ 17 | pts = np.asarray(O.I.Editor.find_attribute('vertices.point')) 18 | tri = np.asarray(O.I.Editor.get_triangles()) 19 | ps.register_surface_mesh(O.name,pts,tri) 20 | 21 | def register_graphite_objects(scene_graph: OGF.SceneGraph): 22 | """ 23 | @brief Registers all the graphite objects in a scene graph to Polyscope 24 | @param[in] scene_graph the Graphite scene graph 25 | """ 26 | for objname in dir(scene_graph.objects): 27 | register_graphite_object(scene_graph.resolve(objname)) 28 | 29 | # ---------------------------------------------------------------------- 30 | 31 | # Create the SceneGraph 32 | scene_graph = OGF.SceneGraph() 33 | 34 | # Create two spheres 35 | S1 = OGF.MeshGrob('S1') 36 | S1.I.Shapes.create_sphere(center=[0,0,0]) 37 | 38 | S2 = OGF.MeshGrob('S2') 39 | S2.I.Shapes.create_sphere(center=[0.5,0,0]) 40 | 41 | # Compute the intersection between the two spheres 42 | Intersection = OGF.MeshGrob('Intersection') 43 | S1.I.Surface.compute_intersection(other=S2,result=Intersection) 44 | 45 | # Polyscope display 46 | ps.init() 47 | register_graphite_objects(scene_graph) 48 | 49 | # Change some polyscope graphic attributes to better show the result 50 | ps.get_surface_mesh('S1').set_transparency(0.5) 51 | ps.get_surface_mesh('S2').set_transparency(0.5) 52 | ps.get_surface_mesh('Intersection').set_edge_width(2) 53 | 54 | ps.show() 55 | -------------------------------------------------------------------------------- /Examples/HelloBooleanOps2.py: -------------------------------------------------------------------------------- 1 | # HelloBooleanOps2: 2 | # Computing boolean ops for moving objects is more fun ! 3 | 4 | import polyscope as ps 5 | import numpy as np 6 | import gompy 7 | import math 8 | 9 | OGF = gom.meta_types.OGF 10 | scene_graph = OGF.SceneGraph() 11 | 12 | # ---------------------- Same as before -------------------------------- 13 | 14 | def register_graphite_object(O: OGF.MeshGrob): 15 | """ 16 | @brief Registers a graphite object to Polyscope 17 | @param[in] O the graphite object to be registered 18 | """ 19 | pts = np.asarray(O.I.Editor.find_attribute('vertices.point')) 20 | tri = np.asarray(O.I.Editor.get_triangles()) 21 | ps.register_surface_mesh(O.name,pts,tri) 22 | 23 | def register_graphite_objects(scene_graph: OGF.SceneGraph): 24 | """ 25 | @brief Registers all the graphite objects in a scene graph to Polyscope 26 | @param[in] scene_graph the Graphite scene graph 27 | """ 28 | for objname in dir(scene_graph.objects): 29 | register_graphite_object(scene_graph.resolve(objname)) 30 | 31 | # new function to unregister Graphite objects from PolyScope 32 | def unregister_graphite_objects(scene_graph: OGF.SceneGraph): 33 | """ 34 | @brief Unregisters all the graphite objects in a scene graph to Polyscope 35 | @param[in] scene_graph the Graphite scene graph 36 | """ 37 | for objname in dir(scene_graph.objects): 38 | ps.remove_surface_mesh(objname) 39 | 40 | # ---------------------------------------------------------------------- 41 | 42 | def draw_scene(alpha=0.25): 43 | """ 44 | @brief The function called for each frame 45 | @param[in] alpha the shifting amount of both spheres 46 | """ 47 | unregister_graphite_objects(scene_graph) 48 | scene_graph.clear() 49 | S1 = OGF.MeshGrob('S1') 50 | S1.I.Shapes.create_sphere( 51 | center=[alpha, 0, 0], precision=3 52 | ) 53 | S2 = OGF.MeshGrob('S2') 54 | S2.I.Shapes.create_sphere( 55 | center=[-alpha, 0, 0], precision=3 56 | ) 57 | R = OGF.MeshGrob('R') 58 | S1.I.Surface.compute_intersection(S2,R) 59 | register_graphite_objects(scene_graph) 60 | ps.get_surface_mesh('S1').set_transparency(0.5) 61 | ps.get_surface_mesh('S2').set_transparency(0.5) 62 | ps.get_surface_mesh('R').set_edge_width(2) 63 | 64 | ps.init() 65 | 66 | # instead of calling ps.show(), we have our own display loop, 67 | # that updates the scene at each frame. At the end of the 68 | # frame, we call ps.frame_tick() to let PolyScope display the frame. 69 | frame = 0 70 | while True: 71 | frame = frame+1 72 | draw_scene(math.sin(frame*0.1)) 73 | ps.frame_tick() 74 | -------------------------------------------------------------------------------- /Examples/HelloBooleanOps3.py: -------------------------------------------------------------------------------- 1 | # HelloBooleanOps3: 2 | # Adding a new Polyscope window and interactive controls to change the 3 | # boolean operation 4 | 5 | import polyscope as ps 6 | import numpy as np 7 | import gompy 8 | import math 9 | 10 | OGF = gom.meta_types.OGF 11 | scene_graph = OGF.SceneGraph() 12 | running = True 13 | op = 1 # the operation, 0: union, 1: intersection, 2: difference 14 | 15 | def draw_GUI(): 16 | """ 17 | @brief Called by Polyscope to draw and handle additional windows 18 | """ 19 | global running, op, scene_graph 20 | # The "quit" button 21 | if ps.imgui.Button('quit'): 22 | running = False 23 | # The combo-box to chose the boolean operation 24 | ops = ['union','intersection','difference'] 25 | _,op = ps.imgui.Combo('operation',op,ops) 26 | # Display number of vertices and facets in result mesh 27 | R = scene_graph.objects.R 28 | nv = R.I.Editor.nb_vertices 29 | nf = R.I.Editor.nb_facets 30 | ps.imgui.Text('Result of boolean operation:') 31 | ps.imgui.Text(' vertices: ' + str(nv)) 32 | ps.imgui.Text(' facets: ' + str(nf)) 33 | 34 | # ---------------------- Same as before -------------------------------- 35 | 36 | def register_graphite_object(O: OGF.MeshGrob): 37 | """ 38 | @brief Registers a graphite object to Polyscope 39 | @param[in] O the graphite object to be registered 40 | """ 41 | pts = np.asarray(O.I.Editor.find_attribute('vertices.point')) 42 | tri = np.asarray(O.I.Editor.get_triangles()) 43 | ps.register_surface_mesh(O.name,pts,tri) 44 | 45 | def register_graphite_objects(scene_graph: OGF.SceneGraph): 46 | """ 47 | @brief Registers all the graphite objects in a scene graph to Polyscope 48 | @param[in] scene_graph the Graphite scene graph 49 | """ 50 | for objname in dir(scene_graph.objects): 51 | register_graphite_object(scene_graph.resolve(objname)) 52 | 53 | def unregister_graphite_objects(scene_graph: OGF.SceneGraph): 54 | """ 55 | @brief Unregisters all the graphite objects in a scene graph to Polyscope 56 | @param[in] scene_graph the Graphite scene graph 57 | """ 58 | for objname in dir(scene_graph.objects): 59 | ps.remove_surface_mesh(objname) 60 | 61 | # ---------------------------------------------------------------------- 62 | 63 | def draw_scene(alpha=0.25): 64 | """ 65 | @brief The function called for each frame 66 | @param[in] alpha the shifting amount of both spheres 67 | """ 68 | unregister_graphite_objects(scene_graph) 69 | scene_graph.clear() 70 | S1 = OGF.MeshGrob('S1') 71 | S1.I.Shapes.create_sphere( 72 | center=[alpha, 0, 0], precision=3 73 | ) 74 | S2 = OGF.MeshGrob('S2') 75 | S2.I.Shapes.create_sphere( 76 | center=[-alpha, 0, 0], precision=3 77 | ) 78 | R = OGF.MeshGrob('R') 79 | if op == 0: 80 | S1.I.Surface.compute_union(S2,R) 81 | elif op == 1: 82 | S1.I.Surface.compute_intersection(S2,R) 83 | elif op == 2: 84 | S1.I.Surface.compute_difference(S2,R) 85 | register_graphite_objects(scene_graph) 86 | ps.get_surface_mesh('S1').set_transparency(0.5) 87 | ps.get_surface_mesh('S2').set_transparency(0.5) 88 | ps.get_surface_mesh('R').set_edge_width(2) 89 | 90 | ps.init() 91 | # Tell polyscope that it should call our function in each frame 92 | ps.set_user_callback(draw_GUI) 93 | 94 | 95 | frame = 0 96 | while running: 97 | frame = frame+1 98 | draw_scene(math.sin(frame*0.1)) 99 | ps.frame_tick() 100 | -------------------------------------------------------------------------------- /Examples/HelloBooleanOps4.py: -------------------------------------------------------------------------------- 1 | # HelloBooleanOps4: 2 | # More options (choose objects, animate, next/prev frame) 3 | # Cleaner code, create an Application class, no globals 4 | 5 | import polyscope as ps 6 | import numpy as np 7 | import gompy 8 | import math 9 | import time 10 | 11 | OGF = gom.meta_types.OGF # shortcut 12 | 13 | # ============================================================================= 14 | 15 | class Application: 16 | """ 17 | @brief A generic Polyscope/geogram application framework 18 | """ 19 | 20 | def __init__(self): 21 | self.scene_graph = OGF.SceneGraph() 22 | self.running = True 23 | 24 | def draw_scene(self): 25 | """ 26 | @brief To be overloaded in subclasses 27 | """ 28 | 29 | def draw_GUI(self): 30 | """ 31 | @brief To be overloaded in subclasses 32 | """ 33 | 34 | def register_graphite_object(self, O: OGF.MeshGrob): 35 | """ 36 | @brief Registers a graphite object to Polyscope 37 | @param[in] O the graphite object to be registered 38 | """ 39 | pts = np.asarray(O.I.Editor.find_attribute('vertices.point')) 40 | tri = np.asarray(O.I.Editor.get_triangles()) 41 | ps.register_surface_mesh(O.name,pts,tri) 42 | 43 | def register_graphite_objects(self): 44 | """ 45 | @brief Registers all the graphite objects in a scene graph to Polyscope 46 | @param[in] scene_graph the Graphite scene graph 47 | """ 48 | for objname in dir(self.scene_graph.objects): 49 | self.register_graphite_object(self.scene_graph.resolve(objname)) 50 | 51 | def unregister_graphite_objects(self): 52 | """ 53 | @brief Unregisters all the graphite objects in a scene graph to Polyscope 54 | @param[in] scene_graph the Graphite scene graph 55 | """ 56 | for objname in dir(self.scene_graph.objects): 57 | ps.remove_surface_mesh(objname) 58 | 59 | def main_loop(self): 60 | """ 61 | @brief The main application loop 62 | @details Initializes polyscope and displays the scene while application 63 | is running 64 | """ 65 | ps.init() 66 | # Tell polyscope that it should call our function in each frame 67 | ps.set_user_callback(self.draw_GUI) 68 | self.frame = 0 69 | while self.running: 70 | self.draw_scene() 71 | ps.frame_tick() 72 | # Be nice with the CPU/computer, sleep a little bit 73 | time.sleep(0.01) # micro-sieste: 1/100th second 74 | 75 | # ============================================================================= 76 | 77 | # constants for boolean ops and shapes 78 | UNION=0; INTERSECTION=1; DIFFERENCE=2 79 | SPHERE=0; CUBE=1; ICOSAHEDRON=2 80 | 81 | class MyApplication(Application): 82 | 83 | def __init__(self): 84 | super().__init__() 85 | self.op = 1 86 | self.shape1 = 0 87 | self.shape2 = 0 88 | self.animate = True 89 | self.show_input_shapes = True 90 | self.frame = 0 91 | 92 | def create_shape( 93 | self, shape: int, center: list, name: str 94 | ) -> OGF.MeshGrob: 95 | """ 96 | @brief creates a sphere, cube or icosahedron 97 | @param[in] shape one of SPHERE, CUBE, ICOSAHEDRON 98 | @param[in] center the center as a list of 3 coordinates [x, y, z] 99 | @param[in] name the name of the mesh in the scene graph 100 | @return the newly created mesh 101 | """ 102 | result = OGF.MeshGrob(name) 103 | result.I.Editor.clear() 104 | if shape == SPHERE: 105 | result.I.Shapes.create_sphere( 106 | center=center, radius=1.0, precision=3 107 | ) 108 | elif shape == CUBE: 109 | result.I.Shapes.create_box( 110 | [ center[0]-0.5, center[1]-0.5, center[2]-0.5], 111 | [ center[0]+0.5, center[1]+0.5, center[2]+0.5] 112 | ) 113 | result.I.Surface.triangulate() # needed by boolean ops 114 | elif shape == ICOSAHEDRON: 115 | result.I.Shapes.create_icosahedron(center) 116 | return result 117 | 118 | def draw_GUI(self): 119 | """ 120 | Called by Polyscope to draw and handle additional windows 121 | """ 122 | # The "quit" button 123 | if ps.imgui.Button('quit'): 124 | self.running = False 125 | 126 | ps.imgui.SameLine() 127 | _,self.show_input_shapes = ps.imgui.Checkbox( 128 | 'show inputs',self.show_input_shapes 129 | ) 130 | 131 | ps.imgui.SameLine() 132 | _,self.animate = ps.imgui.Checkbox('animate',self.animate) 133 | 134 | if not self.animate: 135 | ps.imgui.SameLine() 136 | if ps.imgui.Button('<'): 137 | self.frame = self.frame - 1 138 | ps.imgui.SameLine() 139 | if ps.imgui.Button('>'): 140 | self.frame = self.frame + 1 141 | 142 | # The combo-boxes to chose two shapes 143 | shapes = ['sphere','cube','icosahedron'] 144 | _,self.shape1 = ps.imgui.Combo('shape 1',self.shape1,shapes) 145 | _,self.shape2 = ps.imgui.Combo('shape 2',self.shape2,shapes) 146 | 147 | # The combo-box to chose the boolean operation 148 | ops = ['union','intersection','difference'] 149 | _,self.op = ps.imgui.Combo('operation',self.op,ops) 150 | 151 | # Display number of vertices and facets in result mesh 152 | R = self.scene_graph.objects.R 153 | if R != None: 154 | nv = R.I.Editor.nb_vertices 155 | nf = R.I.Editor.nb_facets 156 | ps.imgui.Text('Result of boolean operation:') 157 | ps.imgui.Text(' vertices: ' + str(nv)) 158 | ps.imgui.Text(' facets: ' + str(nf)) 159 | 160 | def draw_scene(self,alpha=0.25): 161 | """ 162 | The function called for each frame 163 | @param[in] alpha the shifting amount of both spheres 164 | """ 165 | alpha = math.sin(self.frame*0.1) 166 | self.unregister_graphite_objects() 167 | self.scene_graph.clear() 168 | S1 = self.create_shape(self.shape1, [0,0,-alpha], 'S1') 169 | S2 = self.create_shape(self.shape2, [0,0, alpha], 'S2') 170 | R = OGF.MeshGrob('R') 171 | if self.op == UNION: 172 | S1.I.Surface.compute_union(S2,R) 173 | elif self.op == INTERSECTION: 174 | S1.I.Surface.compute_intersection(S2,R) 175 | elif self.op == DIFFERENCE: 176 | S1.I.Surface.compute_difference(S2,R) 177 | self.register_graphite_objects() 178 | ps.get_surface_mesh('S1').set_transparency(0.5) 179 | ps.get_surface_mesh('S2').set_transparency(0.5) 180 | ps.get_surface_mesh('R').set_edge_width(2) 181 | ps.get_surface_mesh('S1').set_enabled(self.show_input_shapes) 182 | ps.get_surface_mesh('S2').set_enabled(self.show_input_shapes) 183 | if self.animate: 184 | self.frame = self.frame+1 185 | 186 | # ============================================================================= 187 | 188 | app = MyApplication() 189 | app.main_loop() 190 | -------------------------------------------------------------------------------- /Examples/HelloPolyscope.py: -------------------------------------------------------------------------------- 1 | import polyscope as ps 2 | import numpy as np 3 | 4 | ps.init() 5 | 6 | ps.register_surface_mesh( 7 | 'cube', 8 | np.array( 9 | [[ 0, 0, 0 ], 10 | [ 0, 0, 1 ], 11 | [ 0, 1, 0 ], 12 | [ 0, 1, 1 ], 13 | [ 1, 0, 0 ], 14 | [ 1, 0, 1 ], 15 | [ 1, 1, 0 ], 16 | [ 1, 1, 1 ]] 17 | ), # cube vertices 18 | np.array( 19 | [[ 3, 6, 2 ], 20 | [ 3, 7, 6 ], 21 | [ 0, 3, 2 ], 22 | [ 0, 1, 3 ], 23 | [ 1, 7, 3 ], 24 | [ 1, 5, 7 ], 25 | [ 5, 6, 7 ], 26 | [ 5, 4, 6 ], 27 | [ 0, 5, 1 ], 28 | [ 0, 4, 5 ], 29 | [ 2, 4, 0 ], 30 | [ 2, 6, 4 ]], 31 | dtype=np.uint32 32 | ) # triangular facets 33 | ) 34 | 35 | ps.show() 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Bruno Levy 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /PyGraphite/README.md: -------------------------------------------------------------------------------- 1 | # PyGraphite 2 | 3 | Here you will find a version of [Graphite](https://github.com/BrunoLevy/GraphiteThree) that runs 4 | in [PolyScope](https://polyscope.run/) using [PolyScope Python bindings](https://polyscope.run/py/). 5 | 6 | What is Graphite ? 7 | ------------------ 8 | 9 | Graphite is an experimental 3D modeler, built around 10 | [geogram](https://github.com/BrunoLevy/geogram). 11 | 12 | It has [Pointset processing and reconstruction](Points), 13 | [Surface remeshing and repair](Remeshing) and many other functionalities, 14 | see [Mesh commands reference](Mesh) for the complete list. 15 | 16 | It contains the main results in Geometry Processing from the former 17 | ALICE Inria project, that is, more than 30 research articles published 18 | in ACM SIGGRAPH, ACM Transactions on Graphics, Symposium on Geometry 19 | Processing and Eurographics. It was supported by two grants from the 20 | European Research Council (ERC): GOODSHAPE and VORPALINE. 21 | 22 | How to install PyGraphite ? 23 | --------------------------- 24 | 25 | You will need to compile Graphite and gompy (see below). In the future I'll do something to have a pip-installable or a conda-installable pre-compiled package (but I need to learn how to do that, will take me a while). 26 | 27 | - Step 1: install Graphite from sources, see [instructions here](https://github.com/BrunoLevy/GraphiteThree/wiki#installing) 28 | - Step 2: install gompy (Python bindings for Graphite), see [instructions here](https://github.com/BrunoLevy/GraphiteThree/wiki/python) 29 | - Step 3: install Python bindings for PolyScope, see [instructions here](https://github.com/nmwsharp/polyscope-py?tab=readme-ov-file#installation) 30 | - Step 4: clone this repository 31 | ``` 32 | git clone https://github.com/BrunoLevy/pygeogram.git 33 | ``` 34 | - Step 5: run it ! 35 | ``` 36 | python3 pygeogram/PyGraphite/pygraphite.py 37 | ``` 38 | 39 | How to use PyGraphite ? 40 | ----------------------- 41 | 42 | See Graphite tutorial [here](https://github.com/BrunoLevy/GraphiteThree/wiki#manuals-and-tutorials) 43 | 44 | _note: these tutorials are for the regular version of Graphite, appearance and behavior are slightly different, for instance, one needs to right-click on the name of an object in the list to get the list of commands_ -------------------------------------------------------------------------------- /PyGraphite/auto_gui.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import polyscope.imgui as imgui 3 | import gompy 4 | gom = gompy.interpreter() 5 | OGF = gom.meta_types.OGF 6 | 7 | #=============================================================================== 8 | 9 | class MenuMap: 10 | """ @brief Handles the menu hierarchy associated with a grob """ 11 | def __init__(self, grob_meta_class : gom.meta_types.OGF.MetaClass): 12 | """ 13 | @brief MenuMap constructor 14 | @param[in] grob_meta_class the GOM meta-class of a Graphite object 15 | """ 16 | self.root = dict() 17 | grob_class_name = grob_meta_class.name 18 | commands_str = gom.get_environment_value(grob_class_name + '_commands') 19 | for command_class_name in commands_str.split(';'): 20 | # skipped, already in context menu 21 | if command_class_name != 'OGF::SceneGraphSceneCommands': 22 | default_menu_name = command_class_name 23 | mclass = gom.resolve_meta_type(command_class_name) 24 | # Command may be associated with a base class, so we find 25 | # the name of this base class in the 'grob_class_name' attribute 26 | # of the Command and strip it to generate the menu name. 27 | default_menu_name = default_menu_name.removeprefix( 28 | mclass.custom_attribute_value('grob_class_name') 29 | ) 30 | default_menu_name = default_menu_name.removesuffix('Commands') 31 | for i in range(mclass.nb_slots()): 32 | mslot = mclass.ith_slot(i) 33 | menu_name = default_menu_name 34 | if(mslot.has_custom_attribute('menu')): 35 | submenu_name = mslot.custom_attribute_value('menu') 36 | submenu_name.removesuffix('/') 37 | if submenu_name[0] == '/': 38 | menu_name = submenu_name[1:] 39 | # Comment for Graphite (not relevant here, but kept): 40 | # Particular case: SceneGraph commands starting 41 | # with '/', to be rooted in the menu bar, 42 | # are stored in the '/menubar' menumap 43 | # (and handled with specific code in 44 | # graphite_gui.draw_menu_bar()) 45 | if grob_meta_class.name == 'OGF::SceneGraph': 46 | menu_name = 'menubar/'+menu_name 47 | else: 48 | menu_name = menu_name + '/' + submenu_name 49 | # Skip Object and Node functions, we don't want them to 50 | # appear in the GUI 51 | if ( 52 | OGF.Object.find_member(mslot.name)==None 53 | and 54 | OGF.Node.find_member(mslot.name) ==None 55 | ): 56 | self.insert(self.root, menu_name, mslot) 57 | 58 | def draw_menus( 59 | self, o : OGF.Object, menudict : dict = None 60 | ) -> OGF.Request: 61 | """ 62 | @brief Draws the menus stored in a menumap 63 | @param[in] o an object of the meta-class given to the constructor 64 | @param[in] optional menu dict (used internally, uses root if unspecified) 65 | @return a request if a menu item was selected or None 66 | """ 67 | if menudict == None: 68 | menudict = self.root 69 | result = None 70 | for k,v in menudict.items(): 71 | if isinstance(v,dict): 72 | if imgui.BeginMenu(k.replace('_',' ')): 73 | submenu_result = self.draw_menus(o,v) 74 | if submenu_result != None: 75 | result = submenu_result 76 | imgui.EndMenu() 77 | else: 78 | mslot = v 79 | mclass = mslot.container_meta_class() 80 | if imgui.MenuItem(k.replace('_',' ')): 81 | result = getattr(o.query_interface(mclass.name),mslot.name) 82 | if (imgui.IsItemHovered() and 83 | mslot.has_custom_attribute('help')): 84 | imgui.SetTooltip(mslot.custom_attribute_value('help')) 85 | return result 86 | 87 | def insert( 88 | self, 89 | menu_dict : dict, menu_name : str, 90 | mslot : OGF.MetaSlot 91 | ) : 92 | """ 93 | @brief Inserts an entry in the menumap (used internally) 94 | @param[in] menu_dict a menu dictionary 95 | @param[in] menu_name the name of the menu to be inserted, with slashes 96 | @param[in] mslot the meta-slot of the method corresponding to the menu 97 | """ 98 | if menu_name == '': 99 | menu_dict[mslot.name] = mslot 100 | else: 101 | # get leading path component 102 | k = menu_name[0:(menu_name+'/').find('/')] 103 | if k not in menu_dict: 104 | menu_dict[k] = dict() 105 | menu_name = menu_name.removeprefix(k) 106 | menu_name = menu_name.removeprefix('/') 107 | self.insert(menu_dict[k], menu_name, mslot) 108 | 109 | 110 | class ArgList(dict): 111 | """ 112 | @brief A dictionary with attribute-like access 113 | @details used by AutoGUI to set/get values in arglists or GOM objects 114 | with the same syntax 115 | """ 116 | def __getattr__(self, key): 117 | return self[key] 118 | 119 | def __setattr__(self, key, value): 120 | self[key] = value 121 | 122 | def __dir__(self): 123 | return super().__dir__() + [str(k) for k in self.keys()] 124 | 125 | #========================================================================= 126 | 127 | class AutoGUI: 128 | """ 129 | @brief Functions to generate the GUI from GOM meta-information 130 | """ 131 | 132 | #========= GUI handlers for commands ================================= 133 | 134 | def draw_command(request : OGF.Request, args : ArgList): 135 | """ 136 | @brief Handles the GUI for a Graphite command 137 | @details Draws a dialog box to edit the arguments of a Request. A Request 138 | is a closure (object.function) where object is a Graphite object. 139 | @param[in] request the Request being edited 140 | @param[in,out] args the arguments of the Request 141 | """ 142 | mmethod = request.method() 143 | if mmethod.nb_args() != 0: 144 | nb_standard_args = 0 145 | has_advanced_args = False 146 | for i in range(mmethod.nb_args()): 147 | if AutoGUI.ith_arg_is_advanced(mmethod,i): 148 | has_advanced_args = True 149 | else: 150 | nb_standard_args = nb_standard_args + 1 151 | height = 25 + nb_standard_args * 25 152 | if has_advanced_args: 153 | height = height + 25 154 | imgui.BeginListBox('##Command',[-1,height]) 155 | imgui.Spacing() 156 | imgui.Spacing() 157 | for i in range(mmethod.nb_args()): 158 | if not AutoGUI.ith_arg_is_advanced(mmethod,i): 159 | AutoGUI.slot_arg_handler(args, mmethod, i) 160 | if has_advanced_args: 161 | if imgui.TreeNode( 162 | 'Advanced'+'##'+str(request.object())+'.'+mmethod.name 163 | ): 164 | imgui.TreePop() 165 | for i in range(mmethod.nb_args()): 166 | if AutoGUI.ith_arg_is_advanced(mmethod,i): 167 | AutoGUI.slot_arg_handler(args, mmethod, i) 168 | imgui.EndListBox() 169 | 170 | def init_command_args(request : OGF.Request) -> ArgList: 171 | """ 172 | @brief Initializes an ArgList with command arguments 173 | @param[in] request a Request, that is, object.function, where object is 174 | a Graphite object 175 | @return an ArgList with the default values of the Request arguments 176 | """ 177 | args = ArgList() 178 | mmethod = request.method() 179 | # This additional arg makes the command display more information 180 | # in the terminal. It is not set for methods declared in Python 181 | # that need to have the exact same number of args. 182 | if not mmethod.is_a(OGF.DynamicMetaSlot): 183 | args['invoked_from_gui'] = True 184 | # Initialize arguments, get default values as string, convert them to 185 | # correct type. 186 | for i in range(mmethod.nb_args()): 187 | val = '' 188 | if mmethod.ith_arg_has_default_value(i): 189 | val = mmethod.ith_arg_default_value_as_string(i) 190 | mtype = mmethod.ith_arg_type(i) 191 | if mtype == gom.meta_types.bool: 192 | if val == '': 193 | val = False 194 | else: 195 | val = (val == 'true' or val == 'True') 196 | elif ( 197 | mtype == gom.meta_types.int or 198 | mtype == OGF.index_t or 199 | mtype == gom.resolve_meta_type('unsigned int') 200 | ): 201 | if val == '': 202 | val = 0 203 | else: 204 | val = int(val) 205 | elif ( 206 | mtype == gom.meta_types.float or 207 | mtype == gom.meta_types.double 208 | ): 209 | if val == '': 210 | val = 0.0 211 | else: 212 | val = float(val) 213 | args[mmethod.ith_arg_name(i)] = val 214 | return args 215 | 216 | #======================================================================== 217 | 218 | def draw_object_commands_menus(o : OGF.Object) -> OGF.Request: 219 | """ 220 | @brief Draws menus for all commands associated with a Graphite object 221 | @param[in] o the Graphite object 222 | @return a Request with the selected item or None 223 | """ 224 | result = None 225 | # get all interfaces of the object 226 | for interface_name in dir(o.I): 227 | interface = getattr(o.I,interface_name) 228 | # keep only those that inherit OGF::Commands 229 | if interface.is_a(OGF.Commands): 230 | if imgui.BeginMenu(interface_name): 231 | thisresult = AutoGUI.draw_interface_menuitems(interface) 232 | if thisresult != None: 233 | result = thisresult 234 | imgui.EndMenu() 235 | return result 236 | 237 | def draw_interface_menuitems(interface : OGF.Interface) -> OGF.Request: 238 | """ 239 | @brief Draws menu items for all slots of an interface 240 | @param[in] interface the interface, for instance, meshgrob.I.Shapes 241 | @return a Request with the selected item or None 242 | """ 243 | result = None 244 | mclass = interface.meta_class 245 | for i in range(mclass.nb_slots()): 246 | mslot = mclass.ith_slot(i) 247 | if not hasattr(OGF.Interface,mslot.name): 248 | thisresult = AutoGUI.draw_request_menuitem( 249 | getattr(interface,mslot.name) 250 | ) 251 | if thisresult != None: 252 | result = thisresult 253 | return result 254 | 255 | def draw_request_menuitem(request : OGF.Request) -> OGF.Request: 256 | """ 257 | @brief Draws a menu item for a given Request (that is, a closure) 258 | @param[in] request the Request 259 | @return the request if it was selected or None 260 | """ 261 | result = None 262 | if imgui.MenuItem(request.method().name.replace('_',' ')): 263 | result = request 264 | if ( 265 | imgui.IsItemHovered() and 266 | request.method().has_custom_attribute('help') 267 | ): 268 | imgui.SetTooltip(request.method().custom_attribute_value('help')) 269 | return result 270 | 271 | #======================================================================== 272 | 273 | def ith_arg_is_advanced( 274 | mmethod: OGF.MetaMethod, i: int 275 | ) -> bool: 276 | """ 277 | @brief Tests whether an argument of a method is declared as advanced 278 | @details Advanced arguments appear in a pulldown, hidden by default 279 | part of the dialog. They are all the arguments after the @advanced 280 | tag in the function documentation. 281 | @param[in] mmethod a meta-method 282 | @param[in] i the index of the argument 283 | @retval True of the i-th argument of mmethod is advanced 284 | @retval False otherwise 285 | """ 286 | if not mmethod.ith_arg_has_custom_attribute(i,'advanced'): 287 | return False 288 | return (mmethod.ith_arg_custom_attribute_value(i,'advanced') == 'true') 289 | 290 | #========= GUI handlers for command args and properties ============== 291 | 292 | def slot_arg_handler( 293 | args: ArgList, mslot: OGF.MetaSlot, i: int 294 | ): 295 | """ 296 | @brief Handles the GUI for a slot argument 297 | @param[in] args an ArgList 298 | @param[in] mslot the metaslot 299 | @param[in] i the index of the argument 300 | """ 301 | tooltip = '' 302 | if mslot.ith_arg_has_custom_attribute(i,'help'): 303 | tooltip = mslot.ith_arg_custom_attribute_value(i,'help') 304 | handler = AutoGUI.arg_handler 305 | #if mslot.ith_arg_has_custom_attribute(i,'handler'): 306 | # handler_name = mslot.ith_arg_custom_attribute_value(i,'handler') 307 | # if not handler_name.endswith('_handler'): 308 | # handler_name = handler_name + '_handler' 309 | # handler = getattr(AutoGUI,handler_name) 310 | handler( 311 | args, mslot.ith_arg_name(i), mslot.ith_arg_type(i), tooltip 312 | ) 313 | 314 | def arg_handler( 315 | o: object, property_name: str, 316 | mtype: OGF.MetaType, tooltip 317 | ): 318 | """ 319 | @brief Handles the GUI for a property in an object 320 | @param[in,out] o the object 321 | @param[in] property_name the name of the property to be edited 322 | @param[in] mtype the meta-type of the property to be edited 323 | @param[in] an optional tooltip to be displayed 324 | """ 325 | if tooltip == None: 326 | tooltip = '' 327 | # special case: property is an enum 328 | if mtype.is_a(OGF.MetaEnum): 329 | AutoGUI.enum_handler(o, property_name, mtype, tooltip) 330 | return 331 | # general case: do we have a specialized handler ? 332 | handler_name = mtype.name.replace(' ','_').replace(':','_') + '_handler' 333 | if hasattr(AutoGUI, handler_name): 334 | getattr(AutoGUI, handler_name)(o,property_name, mtype, tooltip) 335 | return 336 | # fallback: use a textbox to edit the property as a string 337 | AutoGUI.string_handler(o, property_name, mtype, tooltip) 338 | 339 | def string_handler( 340 | o: object, property_name: str, 341 | mtype: OGF.MetaType, tooltip: str 342 | ): 343 | """ 344 | @brief Handles the GUI for a string property in an object, with a textbox 345 | @details This is also the default fallback handler 346 | @param[in,out] o the object 347 | @param[in] property_name the name of the property to be edited 348 | @param[in] an optional tooltip to be displayed 349 | """ 350 | AutoGUI.label(property_name, tooltip) 351 | imgui.SameLine() 352 | imgui.PushItemWidth(-20) 353 | val = getattr(o,property_name) 354 | _,val = imgui.InputText( 355 | '##properties##' + property_name, val 356 | ) 357 | imgui.PopItemWidth() 358 | setattr(o,property_name,val) 359 | 360 | def bool_handler( 361 | o: object, property_name: str, 362 | mtype: gom.meta_types.bool, tooltip: str 363 | ): 364 | """ 365 | @brief Handles the GUI for a bool property in an object, with a checkbox 366 | @param[in,out] o the object 367 | @param[in] property_name the name of the property to be edited 368 | @param[in] an optional tooltip to be displayed 369 | """ 370 | imgui.PushItemWidth(-1) 371 | val = getattr(o,property_name) 372 | _,val = imgui.Checkbox( 373 | property_name.replace('_',' '), val 374 | ) 375 | if tooltip != None and imgui.IsItemHovered(): 376 | imgui.SetTooltip(tooltip) 377 | imgui.PopItemWidth() 378 | setattr(o,property_name,val) 379 | 380 | def int_handler( 381 | o: object, property_name: str, 382 | mtype: OGF.MetaType, tooltip: str 383 | ): 384 | """ 385 | @brief Handles the GUI for an int property in an object 386 | @param[in,out] o the object 387 | @param[in] property_name the name of the property to be edited 388 | @param[in] an optional tooltip to be displayed 389 | """ 390 | AutoGUI.label(property_name, tooltip) 391 | imgui.SameLine() 392 | imgui.PushItemWidth(-20) 393 | val = getattr(o,property_name) 394 | _,val = imgui.InputInt( 395 | '##properties##' + property_name, val, 1 396 | ) 397 | imgui.PopItemWidth() 398 | setattr(o,property_name,val) 399 | 400 | def unsigned_int_handler( 401 | o: object, property_name: str, 402 | mtype: OGF.MetaType, tooltip: str 403 | ): 404 | """ 405 | @brief Handles the GUI for an unsigned int property in an object 406 | @param[in,out] o the object 407 | @param[in] property_name the name of the property to be edited 408 | @param[in] an optional tooltip to be displayed 409 | """ 410 | AutoGUI.label(property_name, tooltip) 411 | imgui.SameLine() 412 | imgui.PushItemWidth(-20) 413 | val = getattr(o,property_name) 414 | _,val = imgui.InputInt( 415 | '##properties##' + property_name, val, 1 416 | ) 417 | if val < 0: 418 | val = 0 419 | imgui.PopItemWidth() 420 | setattr(o,property_name,val) 421 | 422 | def float_handler( 423 | o: object, property_name: str, 424 | mtype: OGF.MetaType, tooltip: str 425 | ): 426 | """ 427 | @brief Handles the GUI for an unsigned int property in an object 428 | @param[in,out] o the object 429 | @param[in] property_name the name of the property to be edited 430 | @param[in] an optional tooltip to be displayed 431 | """ 432 | AutoGUI.label(property_name, tooltip) 433 | imgui.SameLine() 434 | imgui.PushItemWidth(-20) 435 | val = getattr(o, property_name) 436 | _,val = imgui.InputFloat( 437 | '##properties##' + property_name, val 438 | ) 439 | imgui.PopItemWidth() 440 | setattr(o,property_name,val) 441 | 442 | double_handler = float_handler 443 | 444 | def OGF__GrobName_handler( 445 | o: object, property_name: str, 446 | mtype: OGF.GrobName, tooltip: str 447 | ): 448 | """ 449 | @brief Handles the GUI for a GrobName property in an object 450 | @details Displays a pulldown with names of Grobs in SceneGraph 451 | @param[in,out] o the object 452 | @param[in] property_name the name of the property to be edited 453 | @param[in] an optional tooltip to be displayed 454 | """ 455 | values = gom.get_environment_value('grob_instances') 456 | AutoGUI.combo_box_handler(o, property_name, values, tooltip) 457 | 458 | def OGF__MeshGrobName_handler( 459 | o: object, property_name: str, 460 | mtype: OGF.MeshGrobName, tooltip: str 461 | ): 462 | """ 463 | @brief Handles the GUI for a MeshGrobName property in an object 464 | @details Displays a pulldown with names of MeshGrobs in SceneGraph 465 | @param[in,out] o the object 466 | @param[in] property_name the name of the property to be edited 467 | @param[in] an optional tooltip to be displayed 468 | """ 469 | values = gom.get_environment_value('OGF::MeshGrob_instances') 470 | AutoGUI.combo_box_handler(o, property_name, values, tooltip) 471 | 472 | def OGF__NewMeshGrobName_handler( 473 | o: object, property_name: str, 474 | mtype: OGF.MeshGrobName, tooltip: str 475 | ): 476 | """ 477 | @brief Handles the GUI for a MeshGrobName property in an object 478 | @details Displays a pulldown with names of MeshGrobs in SceneGraph 479 | @param[in,out] o the object 480 | @param[in] property_name the name of the property to be edited 481 | @param[in] an optional tooltip to be displayed 482 | """ 483 | values = gom.get_environment_value('OGF::MeshGrob_instances') 484 | AutoGUI.editable_combo_box_handler(o, property_name, values, tooltip) 485 | 486 | def OGF__VoxelGrobName_handler( 487 | o: object, property_name: str, 488 | mtype: OGF.VoxelGrobName, tooltip: str 489 | ): 490 | """ 491 | @brief Handles the GUI for a VoxelGrobName property in an object 492 | @details Displays a pulldown with names of VoxelGrobs in SceneGraph 493 | @param[in,out] o the object 494 | @param[in] property_name the name of the property to be edited 495 | @param[in] an optional tooltip to be displayed 496 | """ 497 | values = gom.get_environment_value('OGF::VoxelGrob_instances') 498 | AutoGUI.combo_box_handler(o, property_name, values, tooltip) 499 | 500 | def OGF__GrobClassName_handler( 501 | o: object, property_name: str, 502 | mtype: OGF.GrobClassName, tooltip: str 503 | ): 504 | """ 505 | @brief Handles the GUI for a GrobClassName property in an object 506 | @details Displays a pulldown with all possible class names for a Grob 507 | @param[in,out] o the object 508 | @param[in] property_name the name of the property to be edited 509 | @param[in] an optional tooltip to be displayed 510 | """ 511 | values = gom.get_environment_value('grob_types') 512 | AutoGUI.combo_box_handler(o, property_name, values, tooltip) 513 | 514 | def enum_handler( 515 | o: object, property_name: str, 516 | menum: OGF.MetaEnum, tooltip: str 517 | ): 518 | """ 519 | @brief Handles the GUI for an enum property in an object 520 | @details Displays a pulldown with all possible class names for the enum 521 | @param[in,out] o the object 522 | @param[in] property_name the name of the property to be edited 523 | @param[in] menum the meta-type of the enum 524 | @param[in] an optional tooltip to be displayed 525 | """ 526 | values = '' 527 | for i in range(menum.nb_values()): 528 | if i != 0: 529 | values = values + ';' 530 | values = values + menum.ith_name(i) 531 | AutoGUI.combo_box_handler(o, property_name, values, tooltip) 532 | 533 | def combo_box_handler( 534 | o: object, property_name: str, 535 | values: str, tooltip: str 536 | ): 537 | """ 538 | @brief Handles the GUI for a property in an object, using a combobox 539 | @param[in,out] o the object 540 | @param[in] property_name the name of the property to be edited 541 | @param[in] values a ';'-separated string with all enum values 542 | @param[in] an optional tooltip to be displayed 543 | """ 544 | AutoGUI.label(property_name, tooltip) 545 | imgui.SameLine() 546 | imgui.PushItemWidth(-20) 547 | old_value = getattr(o,property_name) 548 | _,new_value = AutoGUI.combo_box( 549 | '##properties##'+property_name, values, old_value 550 | ) 551 | imgui.PopItemWidth() 552 | setattr(o,property_name,new_value) 553 | 554 | def editable_combo_box_handler( 555 | o: object, property_name: str, 556 | values: str, tooltip: str 557 | ): 558 | """ 559 | @brief Handles the GUI for a property in an object, 560 | using a editable combobox 561 | @param[in,out] o the object 562 | @param[in] property_name the name of the property to be edited 563 | @param[in] values a ';'-separated string with all enum values 564 | @param[in] an optional tooltip to be displayed 565 | """ 566 | AutoGUI.label(property_name, tooltip) 567 | imgui.SameLine() 568 | imgui.PushItemWidth(-30) 569 | old_value = getattr(o,property_name) 570 | sel,new_value = imgui.InputText( 571 | '##properties##1_'+property_name, 572 | old_value 573 | ) 574 | setattr(o,property_name,new_value) 575 | imgui.PopItemWidth() 576 | imgui.SameLine() 577 | if imgui.Button('V##properties##btn_'+property_name): 578 | imgui.OpenPopup('##properties##popup_'+property_name) 579 | 580 | if imgui.BeginPopup('##properties##popup_'+property_name): 581 | for val in values.split(';'): 582 | if imgui.Selectable(val): 583 | setattr(o,property_name,val) 584 | imgui.EndPopup() 585 | 586 | def combo_box(label: str, values: str, old_value: str) -> tuple: 587 | """ 588 | @brief Draws and handles the GUI for a combo-box 589 | @param[in] label the ImGui label of te combo-box 590 | @param[in] values a ';'-separated string with all values 591 | @param[in] old_value the previous value of the combo-box 592 | @return selected flag and new value of the combo-box 593 | """ 594 | if values=='': 595 | return false,-1 596 | if values[0] == ';': 597 | values = values[1:] 598 | values = values.split(';') 599 | 600 | found = True 601 | try: 602 | old_index = values.index(old_value) 603 | except: 604 | found = False 605 | old_index = 0 606 | sel,new_index = imgui.Combo(label, old_index, values) 607 | return sel,values[new_index] 608 | 609 | def label(property_name: str, tooltip: str): 610 | """ 611 | @brief Draws the label of a property 612 | @param[in] property_name the name of the property, the underscores are 613 | replaced with spaces when displayed 614 | @param[in] tooltip an optional tooltip to be displayed when the user 615 | hovers the label with the mouse pointer, or '' 616 | """ 617 | imgui.Text(property_name.replace('_',' ')) 618 | if tooltip != '' and imgui.IsItemHovered(): 619 | imgui.SetTooltip(tooltip) 620 | 621 | #========================================================================= 622 | 623 | class PyAutoGUI: 624 | """ 625 | @brief Python-AutoGUI interop 626 | @details Functions to inject Python classes and types into the Graphite 627 | object model so that they are visible from the GUI and scripting as if 628 | they have always been there in C++. 629 | """ 630 | 631 | def register_enum(name: str, values: list): 632 | """ 633 | @brief Declares a new enum type in the Graphite object model 634 | @param[in] name the name of the enum, for instance, 'OGF::MyEnumType'. 635 | Then it is accessible using OGF.MyEnumType 636 | @param[in] values a list with all the symbolic names of the enum values 637 | @return the created meta-type for the enum 638 | """ 639 | menum = OGF.MetaEnum(name) 640 | for index,value in enumerate(values): 641 | menum.add_value(value, index) 642 | gom.bind_meta_type(menum) 643 | return menum 644 | 645 | def register_commands( 646 | scene_graph: OGF.SceneGraph, 647 | grobclass: OGF.MetaClass, 648 | methodsclass: type 649 | ): 650 | """ 651 | @brief Declares a new commands class in the Graphite object model 652 | @param[in] scene_graph the SceneGraph 653 | @param[in] grobclass the Grob meta-class to which the new commands 654 | will be associated. Its name should be something like 655 | MeshGrobXXXCommands or VoxelGrobYYYCommands 656 | @param[in] methodsclass the Python class with the commands. Each command 657 | takes as an argument an interface, the name of the method, then the 658 | method arguments. It needs to have type hints in order to generate 659 | the right GUI elements for the arguments. It can have a docstring 660 | in the doxygen format to generate the tooltips. See the end of this 661 | file for an example. 662 | """ 663 | baseclass = gom.resolve_meta_type(grobclass.name + 'Commands') 664 | mclass = baseclass.create_subclass( 665 | 'OGF::' + methodsclass.__name__ 666 | ) 667 | mclass.add_constructor() 668 | 669 | if hasattr(methodsclass,'__dict__'): 670 | methods = methodsclass.__dict__.keys() # preserves order 671 | else: 672 | methods = dir(methodsclass) 673 | 674 | for method_name in methods: 675 | if ( 676 | not method_name.startswith('__') or 677 | not method_name.endswith('__') 678 | ): 679 | pyfunc = getattr(methodsclass,method_name) 680 | mslot = PyAutoGUI.register_command(mclass, pyfunc) 681 | scene_graph.register_grob_commands(grobclass,mclass) 682 | return mclass 683 | 684 | def register_command(mclass: OGF.MetaClass, pyfunc: callable): 685 | """ 686 | @brief adds a new command in a meta-class, implemented by a Python 687 | function. Used internally by register_commands() 688 | @param[in] mclass the meta-class, previously created by calling 689 | create_subclass() in an existing GOM meta-class 690 | @param[in] pyfunc a Python function or callable. It takes as an 691 | argument an interface, the name of the method, then the 692 | method arguments. It needs to have type hints in order to generate 693 | the right GUI elements for the arguments. It can have a docstring 694 | in the doxygen format to generate the tooltips. See the end of this 695 | file for an example. 696 | """ 697 | # small table to translate standard Python types into 698 | # GOM metatypes 699 | python2gom = { 700 | str: gom.meta_types.std.string, 701 | int: gom.meta_types.int, 702 | float: gom.meta_types.float, 703 | bool: gom.meta_types.bool 704 | } 705 | mslot = mclass.add_slot(pyfunc.__name__,pyfunc) 706 | for argname, argtype in typing.get_type_hints(pyfunc).items(): 707 | if argtype in python2gom: 708 | argtype = python2gom[argtype] 709 | if ( 710 | argname != 'interface' and 711 | argname != 'method' and 712 | argname != 'return' 713 | ): 714 | mslot.add_arg(argname, argtype) 715 | PyAutoGUI.parse_doc(mslot,pyfunc) 716 | return mslot 717 | 718 | def parse_doc(mslot: OGF.MetaSlot, pyfunc: callable): 719 | """ 720 | @brief parses the docstring of a python function or callable 721 | and uses it to document a GOM MetaSlot , used internally by 722 | register_command() 723 | @param[in] mslot the meta-slot 724 | @param[in] pyfunc the Python function or callable 725 | """ 726 | if pyfunc.__doc__ == None: 727 | return 728 | advanced = False 729 | for line in pyfunc.__doc__.split('\n'): 730 | try: 731 | try: 732 | kw,val = line.split(maxsplit=1) 733 | except: 734 | kw = line.strip() 735 | val = None 736 | kw = kw[1:] # remove leading '@' 737 | if kw == 'advanced': 738 | advanced = True 739 | elif kw == 'param[in]': 740 | # get default value from docstring (I'd prefer to get 741 | # it from function's signature but it does not seems 742 | # to be possible in Python) 743 | eqpos = val.find('=') 744 | if eqpos == -1: 745 | argname,argdoc = val.split(maxsplit=1) 746 | else: 747 | val = val.replace('=',' ') 748 | argname,argdef,argdoc = val.split(maxsplit=2) 749 | mslot.set_arg_default_value(argname, argdef) 750 | mslot.set_arg_custom_attribute(argname, 'help', argdoc) 751 | if advanced: 752 | mslot.set_arg_custom_attribute( 753 | argname, 'advanced', 'true' 754 | ) 755 | elif kw == 'brief': 756 | mslot.set_custom_attribute('help',val) 757 | else: 758 | mslot.set_custom_attribute(kw, val) 759 | except: 760 | None 761 | 762 | #=============================================================================== 763 | -------------------------------------------------------------------------------- /PyGraphite/graphite_app.py: -------------------------------------------------------------------------------- 1 | import polyscope as ps, polyscope.imgui as imgui, numpy as np 2 | import time 3 | import gompy 4 | 5 | gom = gompy.interpreter() 6 | OGF = gom.meta_types.OGF 7 | 8 | from auto_gui import MenuMap, ArgList, AutoGUI, PyAutoGUI 9 | from polyscope_views import SceneGraphView 10 | from mesh_grob_ops import MeshGrobOps 11 | from terminal import Terminal 12 | from rlcompleter import Completer 13 | import imgui_ext 14 | 15 | #========================================================================= 16 | 17 | class GraphiteApp: 18 | """ @brief the Graphite Application class """ 19 | 20 | #===== Application logic, callbacks ======================================== 21 | 22 | def redraw(self): 23 | if self.drawing: 24 | return 25 | self.drawing = True 26 | ps.frame_tick() 27 | self.drawing = False 28 | 29 | def progress_begin_CB(self,taskname:str): 30 | """ 31 | @brief Progress bar begin callback 32 | @details Called whenever Graphite wants to start a progress bar. 33 | It generates a new PolyScope frame. Since commands are invoked outside 34 | of a PolyScope frame, and since messages are triggered by commands 35 | only, (normally) there can't be nested PolyScope frames. 36 | @param[in] taskname the name of the task in the progress bar 37 | """ 38 | self.progress_task = taskname 39 | self.progress_percent = 0 40 | if self.running: 41 | self.redraw() 42 | 43 | def progress_CB(self,progress_percent:int): 44 | """ 45 | @brief Progress bar progression callback 46 | @details Called whenever Graphite wants to update a progress bar. 47 | It generates a new PolyScope frame. Since commands are invoked outside 48 | of a PolyScope frame, and since messages are triggered by commands 49 | only, (normally) there can't be nested PolyScope frames. 50 | @param[in] progress_percent percentage of progression 51 | """ 52 | self.progress_percent = progress_percent 53 | if self.running: 54 | self.redraw() 55 | 56 | def progress_end_CB(self): 57 | """ 58 | @brief Progress bar end callback 59 | @details Called whenever Graphite wants to terminate a progress bar. 60 | It generates a new PolyScope frame. Since commands are invoked outside 61 | of a PolyScope frame, and since messages are triggered by commands 62 | only, (normally) there can't be nested PolyScope frames. 63 | """ 64 | self.progress_task = None 65 | if self.running: 66 | self.redraw() 67 | 68 | # ============= constructor ========================================== 69 | 70 | def __init__(self): 71 | """ @brief GraphiteApp constructor """ 72 | # In debug mode, all messages are displayed in standard output 73 | # rather than in-app terminal. This helps debugging when a problem 74 | # comes from a refresh triggered by a message display. 75 | self.debug_mode = False 76 | 77 | self.running = False 78 | 79 | self.menu_maps = {} 80 | self.reset_command() 81 | self.queued_execute_command = False # command execution is queued, for 82 | self.queued_close_command = False # making it happen out off ps CB 83 | 84 | self.scene_graph = OGF.SceneGraph() 85 | 86 | # create a Graphite ApplicationBase. It has the printing and 87 | # progress callbacks, that are redirected here to some functions 88 | # (ending with _CB). 89 | application = OGF.ApplicationBase() 90 | self.scene_graph.application = application 91 | 92 | # terminal 93 | self.terminal = Terminal(self) 94 | 95 | # progress callbacks 96 | self.progress_task = None 97 | self.progress_percent = 0 98 | gom.connect(application.notify_progress_begin, self.progress_begin_CB) 99 | gom.connect(application.notify_progress, self.progress_CB) 100 | gom.connect(application.notify_progress_end, self.progress_end_CB) 101 | 102 | # scene graph edition 103 | self.rename_old = None 104 | self.rename_new = None 105 | 106 | # Views 107 | self.scene_graph_view = SceneGraphView(self.scene_graph) 108 | 109 | # Load/Save 110 | self.scene_file_to_load = '' 111 | self.scene_file_to_save = '' 112 | self.object_file_to_save = '' 113 | self.object_to_save = None 114 | 115 | # Draw 116 | self.drawing = False 117 | 118 | #====== Main application loop ========================================== 119 | 120 | def run(self,args): 121 | """ 122 | @brief Main application loop 123 | @param[in] args command line arguments 124 | """ 125 | 126 | PyAutoGUI.register_commands( 127 | self.scene_graph, OGF.SceneGraph, SceneGraphGraphiteCommands 128 | ) 129 | 130 | for f in args[1:]: 131 | self.scene_graph.load_object(f) 132 | 133 | ps.set_open_imgui_window_for_user_callback(False) # we draw our own win 134 | ps.set_user_callback(self.draw_GUI) 135 | self.running = True 136 | quiet_frames = 0 137 | self.scene_graph.application.start() 138 | 139 | while self.running: 140 | self.redraw() 141 | 142 | # Unhighlight highlighted object based on elapsed time 143 | self.scene_graph_view.unhighlight_object() 144 | 145 | # Handle command out of frame tick so that CBs 146 | # can redraw GUI by calling frame tick again. 147 | self.handle_queued_command() 148 | 149 | # Mechanism to make it sleep a little bit 150 | # if no mouse click/mouse drag happened 151 | # since 2000 frames or more. This leaves 152 | # CPU power for other apps and/or lets the 153 | # CPU cool down. There are two levels of 154 | # cooling down depending on how many frames 155 | # were "quiet" (that is, without mouse click 156 | # or mouse drag) 157 | if ( 158 | imgui.GetIO().MouseDown[0] or 159 | imgui.GetIO().MouseDown[1] or 160 | imgui.GetIO().MouseDown[2] or 161 | imgui.IsKeyPressed(imgui.ImGuiKey_Tab) or 162 | imgui.IsKeyPressed(imgui.ImGuiKey_UpArrow) or 163 | imgui.IsKeyPressed(imgui.ImGuiKey_DownArrow) or 164 | imgui.IsKeyPressed(imgui.ImGuiKey_RightArrow) or 165 | imgui.IsKeyPressed(imgui.ImGuiKey_LeftArrow) 166 | ): 167 | quiet_frames = 0 168 | else: 169 | quiet_frames = quiet_frames + 1 170 | if quiet_frames > 2200: 171 | time.sleep(0.5) # gros dodo: 1/2 second 172 | # (after 2000 + 200*1/20th second) 173 | elif quiet_frames > 2000: 174 | time.sleep(0.05) # petit dodo: 1/20th second 175 | else: 176 | time.sleep(0.01) # micro-sieste: 1/100th second 177 | self.scene_graph.clear() 178 | self.scene_graph.application.stop() 179 | 180 | def draw_GUI(self): 181 | """ @brief Draws Graphite GUI """ 182 | imgui.SetNextWindowPos([340,10],imgui.ImGuiCond_Once) 183 | imgui.SetNextWindowSize( 184 | [300,ps.get_window_size()[1]-20], imgui.ImGuiCond_Once 185 | ) 186 | unfolded,_ = imgui.Begin( 187 | 'Graphite',True,imgui.ImGuiWindowFlags_MenuBar 188 | ) 189 | if unfolded: 190 | self.draw_menubar() 191 | self.draw_scenegraph_GUI() 192 | self.draw_command() 193 | imgui.End() 194 | self.terminal.draw() 195 | self.draw_progressbar_window() 196 | self.draw_dialogs() 197 | 198 | #====== Main elements of GUI ========================================== 199 | 200 | def draw_progressbar_window(self): 201 | """ 202 | @brief Draws the progressbar window 203 | @see progress_begin_CB(), progress_CB(), progress_end_CB() 204 | """ 205 | if self.progress_task != None: 206 | imgui.SetNextWindowPos([660,ps.get_window_size()[1]-55]) 207 | imgui.SetNextWindowSize([600,45]) 208 | imgui.Begin('Progress',True,imgui.ImGuiWindowFlags_NoTitleBar) 209 | if imgui.Button('X'): 210 | self.scene_graph.application.progress_cancel() 211 | if imgui.IsItemHovered(): 212 | imgui.SetTooltip('Cancel task') 213 | imgui.SameLine() 214 | imgui.Text(self.progress_task) 215 | imgui.SameLine() 216 | imgui.ProgressBar(self.progress_percent/100.0, [-1,0]) 217 | imgui.End() 218 | 219 | def draw_menubar(self): 220 | """ 221 | @brief Draws Graphite's main menubar 222 | """ 223 | if imgui.BeginMenuBar(): 224 | if imgui.BeginMenu('File'): 225 | if imgui.MenuItem('Load...'): 226 | exts = gom.get_environment_value( 227 | 'grob_read_extensions' 228 | ).split(';') 229 | exts = [ ext.removeprefix('*.') for ext in exts if ext != ''] 230 | imgui_ext.OpenFileDialog( 231 | 'Load...', 232 | exts, 233 | '', 234 | imgui_ext.ImGuiExtFileDialogFlags_Load 235 | ) 236 | if imgui.MenuItem('Save scene'): 237 | imgui_ext.OpenFileDialog( 238 | 'Save scene', 239 | ['graphite'], 240 | 'scene.graphite', 241 | imgui_ext.ImGuiExtFileDialogFlags_Save 242 | ) 243 | imgui.Separator() 244 | # SceneGraphGraphiteCommands: Implemented in Python, atr 245 | # the end of this file, and registered in run() 246 | request = AutoGUI.draw_interface_menuitems( 247 | self.scene_graph.I.Graphite 248 | ) 249 | if request != None: 250 | self.set_command(request) 251 | imgui.Separator() 252 | if imgui.MenuItem('show all'): 253 | self.scene_graph_view.show_all() 254 | if imgui.MenuItem('hide all'): 255 | self.scene_graph_view.hide_all() 256 | imgui.Separator() 257 | if imgui.MenuItem('quit'): 258 | self.running = False 259 | imgui.EndMenu() 260 | if imgui.BeginMenu('Windows'): 261 | if imgui.MenuItem( 262 | 'show terminal', None, self.terminal.visible 263 | ): 264 | self.terminal.visible = not self.terminal.visible 265 | imgui.EndMenu() 266 | imgui.EndMenuBar() 267 | 268 | def draw_scenegraph_GUI(self): 269 | """ 270 | @brief Draws the GUI of the SceneGraph, with the editable list of objs 271 | """ 272 | # Get scene objects, I do that instead of dir(self.scene_graph.objects) 273 | # to keep the order of the objects. 274 | objects = [] 275 | for i in range(self.scene_graph.nb_children): 276 | objects.append(self.scene_graph.ith_child(i)) 277 | 278 | imgui.BeginListBox('##Objects',[-1,200]) 279 | for object in objects: 280 | self.draw_object_GUI(object) 281 | imgui.EndListBox() 282 | 283 | def draw_object_GUI(self, object: OGF.Grob): 284 | """ 285 | @brief Draws the GUI for editing one Graphite object. 286 | @details Handles visibility button, object menus, renaming, move up / 287 | move down buttons and delete button. Used by draw_scenegraph_GUI() 288 | @param[in] object the Graphite object 289 | """ 290 | objname = object.name 291 | itemwidth = imgui.GetContentRegionAvail()[0] 292 | show_buttons = (self.scene_graph.current_object == objname and 293 | self.rename_old == None) 294 | 295 | if (show_buttons): 296 | itemwidth = itemwidth - 105 297 | 298 | if self.rename_old == objname: # if object is being renamed 299 | if self.rename_old == self.rename_new: 300 | imgui.SetKeyboardFocusHere(0) 301 | sel,self.rename_new=imgui.InputText( 302 | 'rename##' + objname,self.rename_new, 303 | imgui.ImGuiInputTextFlags_EnterReturnsTrue | 304 | imgui.ImGuiInputTextFlags_AutoSelectAll 305 | ) 306 | if sel: # was pressed, rename object 307 | if self.rename_old != self.rename_new: 308 | # backup polyscope parameters before renaming 309 | # (because object will have a completely different 310 | # polyscope structure, and polyscope persistent 311 | # parameters mechanism is not aware that it is the 312 | # same object that was renamed) 313 | old_params = self.scene_graph_view.get_view( 314 | object 315 | ).get_structure_params() 316 | object.rename(self.rename_new) 317 | self.scene_graph.current_object = object.name 318 | # restore polyscope parameters 319 | self.scene_graph_view.get_view( 320 | object 321 | ).set_structure_params(old_params) 322 | self.rename_old = None 323 | self.rename_new = None 324 | else: # standard operation (object is not being renamed) 325 | 326 | view = self.scene_graph_view.get_view(object) 327 | visible = (view != None and view.visible) 328 | 329 | selected,visible = imgui.Checkbox('##visible##'+objname, visible) 330 | imgui.SameLine() 331 | if selected and view != None: 332 | if visible: 333 | view.show() 334 | else: 335 | view.hide() 336 | 337 | selected = (self.scene_graph.current() != None and 338 | self.scene_graph.current().name == objname) 339 | sel,_=imgui.Selectable( 340 | objname, selected, 341 | imgui.ImGuiSelectableFlags_AllowDoubleClick, 342 | [itemwidth,0] 343 | ) 344 | if imgui.IsItemHovered(): 345 | imgui.SetTooltip( 346 | objname + ':' + object.meta_class.name.removeprefix('OGF::') 347 | ) 348 | if sel: 349 | self.scene_graph.current_object = objname 350 | if imgui.IsMouseDoubleClicked(0): 351 | self.scene_graph_view.show_only(object) 352 | self.scene_graph_view.highlight_object(object) 353 | 354 | self.draw_object_menu(object) 355 | 356 | if show_buttons: 357 | imgui.SameLine() 358 | self.draw_object_buttons(object) 359 | 360 | def draw_object_menu(self, object: OGF.Grob): 361 | """ 362 | @brief Draws the contextual menu associated with an object 363 | @details Handles general commands, implemented here, and commands 364 | from Graphite Object models, using the MenuMap. Used by 365 | draw_object_GUI(). 366 | """ 367 | 368 | if imgui.BeginPopupContextItem(object.name+'##ops'): 369 | if imgui.MenuItem('rename'): 370 | self.rename_old = object.name 371 | self.rename_new = object.name 372 | 373 | if imgui.MenuItem('duplicate'): 374 | self.scene_graph.current_object = object.name 375 | sgv = self.scene_graph_view 376 | old_view = self.scene_graph_view.get_view( 377 | self.scene_graph.current() 378 | ) 379 | params = old_view.get_structure_params() 380 | new_object = self.scene_graph.duplicate_current() 381 | self.scene_graph.current_object = new_object.name 382 | new_view = self.scene_graph_view.get_view(new_object) 383 | new_view.set_structure_params(params) 384 | self.rename_old = new_object.name 385 | self.rename_new = new_object.name 386 | 387 | if imgui.MenuItem('save object'): 388 | exts = gom.get_environment_value( 389 | object.meta_class.name + '_write_extensions' 390 | ).split(';') 391 | exts = [ ext.removeprefix('*.') for ext in exts if ext != '' ] 392 | imgui_ext.OpenFileDialog( 393 | 'Save object...', 394 | exts, 395 | object.name + '.' + exts[0], 396 | imgui_ext.ImGuiExtFileDialogFlags_Save 397 | ) 398 | self.object_to_save = object 399 | 400 | if imgui.MenuItem('commit transform'): 401 | self.scene_graph_view.get_view(object).commit_transform() 402 | if imgui.IsItemHovered(): 403 | imgui.SetTooltip( 404 | 'transforms vertices according to Polyscope transform guizmo' 405 | ) 406 | 407 | if imgui.MenuItem('copy style to all'): 408 | object_view = self.scene_graph_view.get_view(object) 409 | params = object_view.get_structure_params() 410 | for v in self.scene_graph_view.get_views(): 411 | if v.grob.is_a(object.meta_class): 412 | v.set_structure_params(params) 413 | if imgui.IsItemHovered(): 414 | imgui.SetTooltip( 415 | 'copy graphic style to all objects of same type' 416 | ) 417 | 418 | imgui.Separator() 419 | request = self.get_menu_map(object).draw_menus(object) 420 | if request != None: 421 | self.set_command(request) 422 | imgui.EndPopup() 423 | 424 | def draw_object_buttons(self, object: OGF.Grob): 425 | """ 426 | @brief Draws and handles the buttons associated with an object 427 | @param[in] object the object 428 | @details Draws the move up, move down and delete buttons. Used 429 | by draw_object_GUI() 430 | """ 431 | imgui.PushStyleVar(imgui.ImGuiStyleVar_FramePadding, [0,0]) 432 | if imgui.ArrowButton('^'+object.name,imgui.ImGuiDir_Up): 433 | self.scene_graph.current_object = object.name 434 | self.scene_graph.move_current_up() 435 | if imgui.IsItemHovered(): 436 | imgui.SetTooltip('Move object up') 437 | imgui.SameLine() 438 | if imgui.ArrowButton('v'+object.name,imgui.ImGuiDir_Down): 439 | self.scene_graph.current_object = object.name 440 | self.scene_graph.move_current_down() 441 | if imgui.IsItemHovered(): 442 | imgui.SetTooltip('Move object down') 443 | imgui.SameLine() 444 | imgui.PushStyleVar(imgui.ImGuiStyleVar_FramePadding, [2,0]) 445 | if imgui.Button('X'+'##'+object.name): 446 | if (self.request != None and 447 | self.get_grob(self.request).name == object.name): 448 | self.reset_command() 449 | self.scene_graph.current_object = object.name 450 | self.scene_graph.delete_current_object() 451 | if imgui.IsItemHovered(): 452 | imgui.SetTooltip('Delete object') 453 | imgui.PopStyleVar() 454 | imgui.PopStyleVar() 455 | 456 | def draw_command(self): 457 | """ @brief Draws the GUI for the current Graphite command """ 458 | if self.request == None: 459 | return 460 | 461 | imgui.Text('Command: ' + self.request.method().name.replace('_',' ')) 462 | if (imgui.IsItemHovered() and 463 | self.request.method().has_custom_attribute('help')): 464 | imgui.SetTooltip( 465 | self.request.method().custom_attribute_value('help') 466 | ) 467 | grob = self.get_grob(self.request) 468 | if grob.is_a(OGF.SceneGraph): 469 | objname = 'scene_graph' 470 | if self.scene_graph.current() != None: 471 | objname = ( objname + ', current=' + 472 | self.scene_graph.current().name ) 473 | imgui.Text('Object: ' + objname) 474 | else: 475 | objname = grob.name 476 | # Ask the meta_class rather than calling is_a(), 477 | # else Graphite will complain that the Interface is 478 | # locked when calling is_a() !!! 479 | if (self.request.object().meta_class.is_subclass_of(OGF.Interface)): 480 | objnames = gom.get_environment_value( 481 | grob.meta_class.name + '_instances' 482 | ) 483 | imgui.Text('Object:') 484 | imgui.SameLine() 485 | _,objname = AutoGUI.combo_box('##Target',objnames,objname) 486 | self.request.object().grob = getattr( 487 | self.scene_graph.objects,objname 488 | ) 489 | else: 490 | imgui.Text('Object: ' + objname) 491 | 492 | AutoGUI.draw_command(self.request, self.args) 493 | 494 | if imgui.Button('OK'): 495 | self.queued_execute_command = True 496 | self.queued_close_command = True 497 | if imgui.IsItemHovered(): 498 | imgui.SetTooltip('Apply and close command') 499 | imgui.SameLine() 500 | if imgui.Button('Apply'): 501 | self.queued_execute_command = True 502 | if imgui.IsItemHovered(): 503 | imgui.SetTooltip('Apply and keep command open') 504 | imgui.SameLine() 505 | if imgui.Button('Cancel'): 506 | self.reset_command() 507 | if imgui.IsItemHovered(): 508 | imgui.SetTooltip('Close command') 509 | imgui.SameLine() 510 | if imgui.Button('Reset'): 511 | self.set_command(self.request) 512 | if imgui.IsItemHovered(): 513 | imgui.SetTooltip('Reset factory settings') 514 | 515 | def draw_dialogs(self): 516 | self.scene_file_to_load,_ = imgui_ext.FileDialog('Load...') 517 | self.scene_file_to_save,_ = imgui_ext.FileDialog('Save scene') 518 | self.object_file_to_save,_ = imgui_ext.FileDialog('Save object...') 519 | 520 | def handle_queued_command(self): 521 | """ 522 | @brief Executes the Graphite command if it was marked for execution 523 | @details Graphite command is not directly called when once pushes the 524 | button, because it needs to be called outside the PolyScope frame 525 | for the terminal and progress bars to work, since they trigger 526 | additional PolyScope frames (nesting PolyScope frames is forbidden) 527 | """ 528 | if self.queued_execute_command: 529 | self.scene_graph_view.commit_transform() # Commit all xform guizmos 530 | self.invoke_command() 531 | self.queued_execute_command = False 532 | 533 | if self.queued_close_command: 534 | self.reset_command() 535 | self.queued_close_command = False 536 | 537 | if self.scene_file_to_load != '': 538 | self.scene_graph.load_object(self.scene_file_to_load) 539 | ps.reset_camera_to_home_view() 540 | self.scene_file_to_load = '' 541 | 542 | if self.scene_file_to_save != '': 543 | self.scene_graph_view.copy_polyscope_params_to_grob() 544 | self.scene_graph.save(self.scene_file_to_save) 545 | self.scene_file_to_save = '' 546 | 547 | if self.object_file_to_save != '' and self.object_to_save != None: 548 | view = self.scene_graph_view.get_view(self.object_to_save) 549 | view.copy_polyscope_params_to_grob() 550 | self.object_to_save.save(self.object_file_to_save) 551 | self.object_file_to_save = '' 552 | self.object_to_save = None 553 | 554 | self.terminal.handle_queued_command() 555 | 556 | def get_grob(self,request: OGF.Request) -> OGF.Grob: 557 | """ 558 | @brief Gets the Graphite object from a Request 559 | @details The Request passed to set_command() may be in the 560 | form grob.interface.method or simply grob.method. 561 | This function gets the grob in both cases. 562 | @return the Grob associated with the Request 563 | """ 564 | object = request.object() 565 | if(hasattr(object,'grob')): 566 | return object.grob 567 | else: 568 | return object 569 | 570 | def get_menu_map(self, grob: OGF.Grob): 571 | """ 572 | @brief Gets the MenuMap associated with a grob 573 | @param grob the Graphite object 574 | @details The MenuMap is constructed the first time the function is 575 | called for a Grob class, then stored in a dictionary for the next 576 | times 577 | @return the MenuMap associated with the graphite object 578 | """ 579 | if not grob.name in self.menu_maps: 580 | self.menu_maps[grob.name] = MenuMap(grob.meta_class) 581 | return self.menu_maps[grob.name] 582 | 583 | #===== Commands management ============================================== 584 | 585 | def set_command(self, request): 586 | """ Sets current Graphite command, edited in the GUI """ 587 | self.request = request 588 | self.args = AutoGUI.init_command_args(request) 589 | 590 | def reset_command(self): 591 | """ Resets current Graphite command """ 592 | self.request = None 593 | self.args = None 594 | 595 | def invoke_command(self): 596 | """ Invokes current Graphite command with the args from the GUI """ 597 | self.request(**self.args) #**: expand dict as keywords func call 598 | 599 | 600 | #============================================================================ 601 | 602 | class SceneGraphGraphiteCommands: 603 | """ The commands in the second section of the File menu """ 604 | 605 | def create_object( 606 | interface : OGF.Interface, 607 | method : str, 608 | type : OGF.GrobClassName, 609 | name : str 610 | ): 611 | """ 612 | @brief creates a new object 613 | @param[in] type = OGF::MeshGrob type of the object to create 614 | @param[in] name = new_object name of the object to create 615 | """ 616 | interface.grob.create_object(type, name) 617 | 618 | def clear_scenegraph( 619 | interface : OGF.Interface, 620 | method : str 621 | ): 622 | """ @brief deletes all objects in the scene-graph """ 623 | interface.grob.clear() 624 | ps.reset_camera_to_home_view() 625 | 626 | #============================================================================ 627 | -------------------------------------------------------------------------------- /PyGraphite/imgui_ext.py: -------------------------------------------------------------------------------- 1 | import polyscope.imgui as imgui 2 | import os,string 3 | 4 | 5 | class FileDialogImpl: 6 | """ 7 | @brief Internal implementation of file dialog 8 | @details Do not use directly, use API functions instead 9 | imgui_ext.OpenFileDialog() and imgui_ext.FileDialog() 10 | """ 11 | def __init__( 12 | self, 13 | save_mode: bool = False, 14 | default_filename: str = '' 15 | ): 16 | self.visible = False 17 | self.save_mode = save_mode 18 | self.path = os.getcwd() 19 | self.directories = [] 20 | self.files = [] 21 | self.extensions = [] 22 | self.show_hidden = False 23 | self.current_file = default_filename 24 | self.selected_file = '' 25 | self.scaling = 1.0 # scaling factor for GUI (depends on font size) 26 | self.footer_size = 35.0 * self.scaling 27 | self.pinned = False 28 | self.are_you_sure = False 29 | 30 | def draw(self): 31 | """ @brief Draws and handles the GUI """ 32 | if not self.visible: 33 | return 34 | 35 | label = ('Save as...##' if self.save_mode else 'Load...##') 36 | label = label + str(hash(self)) # make sure label is unique 37 | 38 | imgui.SetNextWindowPos([700,10],imgui.ImGuiCond_Once) 39 | imgui.SetNextWindowSize([400,415],imgui.ImGuiCond_Once) 40 | _,self.visible = imgui.Begin(label, self.visible) 41 | self.draw_header() 42 | imgui.Separator() 43 | self.draw_files_and_directories() 44 | self.draw_footer() 45 | imgui.End() 46 | self.draw_are_you_sure() 47 | 48 | def draw_header(self): 49 | """ 50 | @brief Draws the top part of the window, with 51 | the Parent/Home/Refresh/pin buttons and the 52 | clickable current path 53 | """ 54 | if imgui.Button('Parent'): 55 | self.set_path('..') 56 | imgui.SameLine() 57 | if imgui.Button('Home'): 58 | self.set_path(os.path.expanduser('~')) 59 | imgui.SameLine() 60 | if imgui.Button('Refresh'): 61 | self.update_files() 62 | s = imgui.CalcTextSize('(-') 63 | imgui.SameLine() 64 | w = imgui.GetContentRegionAvail()[0] - s[0]*2.5 65 | imgui.Dummy([w, 1.0]) 66 | imgui.SameLine() 67 | if not self.save_mode: 68 | if SimpleButton('O' if self.pinned else '(-'): 69 | self.pinned = not self.pinned 70 | if imgui.IsItemHovered(): 71 | imgui.SetTooltip('pin dialog') 72 | self.draw_disk_drives() 73 | imgui.Separator() 74 | path = self.path.split(os.sep) 75 | for i,d in enumerate(self.path.split(os.sep)): 76 | if d == '': 77 | continue 78 | if (imgui.GetContentRegionAvail()[0] < 79 | imgui.CalcTextSize(d)[0] + 10.0 * self.scaling): 80 | imgui.NewLine() 81 | if SimpleButton( d + '##path' + str(i)): 82 | new_path = os.sep.join(path[0:i+1]) 83 | if len(path[0]) < 2 or path[0][1] != ':': 84 | new_path = os.sep + new_path 85 | self.set_path(new_path) 86 | imgui.SameLine() 87 | imgui.Text(os.sep) 88 | imgui.SameLine() 89 | 90 | def draw_disk_drives(self): 91 | """ @brief Draws buttons to select disk drives under Windows """ 92 | if os.name != 'nt': 93 | return 94 | for drive in string.ascii_uppercase: 95 | if os.path.exists(os.path.join(drive, 'File.ID')): 96 | if imgui.Button(drive + ':'): 97 | self.set_path(drive + ':') 98 | imgui.SameLine() 99 | if (imgui.GetContentRegionAvail()[0] < 100 | imgui.CalcTextSize('X:')[0] + 10.0 * self.scaling): 101 | imgui.NewLine() 102 | imgui.Text('') 103 | 104 | def draw_files_and_directories(self): 105 | """ @brief Draws two panels with the list of files and directories """ 106 | panelsize = [ 107 | imgui.GetWindowWidth()*0.5-10.0*self.scaling, 108 | -self.footer_size 109 | ] 110 | imgui.BeginChild('##directories', panelsize, True) 111 | for d in sorted(self.directories): 112 | _,sel = imgui.Selectable(d) 113 | if sel: 114 | self.set_path(d) 115 | break 116 | imgui.EndChild() 117 | imgui.SameLine() 118 | imgui.BeginChild('##files', panelsize, True) 119 | for f in sorted(self.files): 120 | sel,_ = imgui.Selectable( 121 | f, self.current_file == f, 122 | imgui.ImGuiSelectableFlags_AllowDoubleClick 123 | ) 124 | if sel: 125 | self.current_file = f 126 | if imgui.IsMouseDoubleClicked(0): 127 | self.file_selected() 128 | imgui.EndChild() 129 | 130 | def draw_footer(self): 131 | """ @brief draws footer with save/load btn and filename text entry """ 132 | save_btn_label = 'Save' if self.save_mode else 'Load' 133 | save_btn_label = save_btn_label + '##' + str(hash(self)) 134 | if imgui.Button(save_btn_label): 135 | self.file_selected() 136 | imgui.SameLine() 137 | imgui.PushItemWidth( 138 | -80.0*self.scaling if self.save_mode else -5.0 * self.scaling 139 | ) 140 | sel,self.current_file = imgui.InputText( 141 | '##filename##' + str(hash(self)), 142 | self.current_file, 143 | imgui.ImGuiInputTextFlags_EnterReturnsTrue 144 | ) 145 | if sel: 146 | self.file_selected() 147 | imgui.PopItemWidth() 148 | # Keep auto focus on the input box 149 | if imgui.IsItemHovered(): 150 | # Auto focus previous widget 151 | imgui.SetKeyboardFocusHere(-1) 152 | 153 | def draw_are_you_sure(self): 154 | """ @brief draws a modal popup if file to save already exists """ 155 | if self.are_you_sure: 156 | imgui.OpenPopup('File exists') 157 | if imgui.BeginPopupModal( 158 | "File exists", True, imgui.ImGuiWindowFlags_AlwaysAutoResize 159 | ): 160 | imgui.Text( 161 | 'File ' + self.current_file + 162 | ' already exists\nDo you want to overwrite it ?' 163 | ) 164 | imgui.Separator() 165 | if imgui.Button( 166 | 'Overwrite', 167 | [-imgui.GetContentRegionAvail()[0]/2.0, 0.0] 168 | ): 169 | self.are_you_sure = False 170 | imgui.CloseCurrentPopup() 171 | self.file_selected(True) 172 | imgui.SameLine() 173 | if imgui.Button('Cancel', [-1.0, 0.0]): 174 | self.are_you_sure = False 175 | imgui.CloseCurrentPopup(); 176 | imgui.EndPopup() 177 | 178 | def show(self): 179 | self.update_files() 180 | self.visible = True 181 | 182 | def hide(self): 183 | self.visible = False 184 | 185 | def set_path(self, path : str): 186 | self.path = os.path.normpath(os.path.join(self.path, path)) 187 | self.update_files() 188 | 189 | def set_default_filename(self, filename : str): 190 | self.current_file = filename 191 | 192 | def get_and_reset_selected_file(self): 193 | """ 194 | @brief Gets the selected file if any and resets it to the empty string 195 | @return the selected file if there is any or empty string otherwise. 196 | """ 197 | result = self.selected_file 198 | self.selected_file = '' 199 | return result 200 | 201 | def set_extensions(self, extensions : list): 202 | """ 203 | @brief Defines the file extensions managed by this FileDialogImpl. 204 | @param[in] extensions a list of extensions, without the dot '.'. 205 | """ 206 | self.extensions = extensions 207 | 208 | def set_save_mode(self,save_mode : bool): 209 | """ 210 | @brief Sets whether this file dialog is for 211 | saving file. 212 | @details If this file dialog is for saving file, 213 | then the user can enter the name of a non-existing 214 | file, else he can only select existing files. 215 | @param[in] x True if this file dialog is for 216 | saving files. 217 | """ 218 | self.save_mode = save_mode 219 | 220 | def file_selected(self, overwrite=False): 221 | """ Called whenever a file is selected """ 222 | path_file = os.path.join(self.path, self.current_file) 223 | if self.save_mode: 224 | if not overwrite and os.path.isfile(path_file): 225 | self.are_you_sure = True 226 | return 227 | else: 228 | self.selected_file = path_file 229 | else: 230 | self.selected_file = path_file 231 | if not self.pinned: 232 | self.hide() 233 | 234 | def update_files(self): 235 | self.files = [] 236 | self.directories = ['..'] 237 | for f in os.listdir(self.path): 238 | path_f = os.path.join(self.path, f) 239 | if os.path.isfile(path_f): 240 | if self.show_file(f): 241 | self.files.append(f) 242 | elif os.path.isdir(path_f): 243 | if self.show_directory(f): 244 | self.directories.append(f) 245 | 246 | def show_file(self, f : str) -> bool: 247 | """ Tests whether a give file should be shown in the dialog """ 248 | if not self.show_hidden and f.startswith('.'): 249 | return False 250 | if len(self.extensions) == 0: 251 | return True 252 | _,ext = os.path.splitext(f) 253 | ext = ext.removeprefix('.') 254 | return ext.lower() in self.extensions 255 | 256 | def show_directory(self, d : str) -> bool: 257 | """ Tests whether a give directory should be shown in the dialog """ 258 | return (self.show_hidden or not d.startswith('.')) 259 | 260 | 261 | file_dialogs = dict() 262 | ImGuiExtFileDialogFlags_Load = 1 263 | ImGuiExtFileDialogFlags_Save = 2 264 | 265 | def OpenFileDialog( 266 | label : str, 267 | extensions : list, 268 | filename : str, 269 | flags : int 270 | ): 271 | """ 272 | @brief Create or opens a file dialog 273 | @details One needs to call @see FileDialog() to display it afterwards 274 | @param[in] label a unique label associated with the dialog 275 | @param[in] extensions the list of valid file extensions, without '.' 276 | @param[in] filename default filename used for Save dialogs, or '' 277 | @param[in] flags one of ImGuiExtFileDialogFlags_Load, 278 | ImGuiExtFileDialogFlags_Save 279 | """ 280 | if label in file_dialogs: 281 | dlg = file_dialogs[label] 282 | else: 283 | dlg = FileDialogImpl() 284 | file_dialogs[label] = dlg 285 | dlg.set_extensions(extensions) 286 | if flags == ImGuiExtFileDialogFlags_Save: 287 | dlg.set_save_mode(True) 288 | dlg.set_default_filename(filename) 289 | else: 290 | dlg.set_save_mode(False) 291 | dlg.show() 292 | 293 | def FileDialog(label : str) -> (str, bool): 294 | """ 295 | @brief Draws and handles a file dialog 296 | @param[in] label the unique label associated with the dialog. If 297 | OpenFileDialog() was called before in the same frame, 298 | it will be displayed and handled, else it is ignored. 299 | @retval (selected filename,True) if a file was selected 300 | @retval ('', False) otherwise 301 | """ 302 | if not label in file_dialogs: 303 | return ('',False) 304 | dlg = file_dialogs[label] 305 | dlg.draw() 306 | result = dlg.get_and_reset_selected_file() 307 | return result, (result != '') 308 | 309 | def SimpleButton(label: str) -> bool: 310 | """ 311 | @brief Draws a button without any frame 312 | @param[in] label the text drawn of the button, what follows '##' is not 313 | displayed and used to have a unique ID 314 | @retval True if the button was pushed 315 | @retval False otherwise 316 | """ 317 | txt = label 318 | off = label.find('##') 319 | if off != -1: 320 | txt = txt[0:off] 321 | label_size = imgui.CalcTextSize(txt, None, True) 322 | _,sel=imgui.Selectable(label, False, 0, label_size) 323 | return sel 324 | -------------------------------------------------------------------------------- /PyGraphite/mesh_grob_ops.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import gompy 3 | gom = gompy.interpreter() 4 | OGF = gom.meta_types.OGF 5 | 6 | class MeshGrobOps: 7 | def get_object_bbox(o: OGF.MeshGrob) -> tuple: 8 | """ 9 | @brief gets the bounding-box of a MeshGrob 10 | @param[in] o: the MeshGrob 11 | @return pmin,pmax the bounds, as numpy arrays 12 | """ 13 | vertices = np.asarray(o.I.Editor.get_points()) 14 | return np.min(vertices,0), np.max(vertices,0) 15 | 16 | def get_object_center(o: OGF.MeshGrob) -> np.ndarray: 17 | """ 18 | @brief gets the center of a MeshGrob 19 | @param[in] o: the MeshGrob 20 | @return the center of the bounding-box of o, as a numpy array 21 | """ 22 | pmin,pmax = MeshGrobOps.get_object_bbox(o) 23 | return 0.5*(pmin+pmax) 24 | 25 | def get_object_bbox_diagonal(o: OGF.MeshGrob) -> float: 26 | """ 27 | @brief gets the bounding-box diagonal of a MeshGrob 28 | @param[in] o: the MeshGrob 29 | @return the length of the diagonal of the bounding box of o 30 | """ 31 | pmin,pmax = MeshGrobOps.get_object_bbox(o) 32 | return np.linalg.norm(pmax-pmin) 33 | 34 | def translate_object(o: OGF.MeshGrob, T: np.ndarray): 35 | """ 36 | @brief Applies a translation to object's vertices 37 | @details Does not call o.update(), it is caller's responsibility 38 | @param[in,out] o the MeshGrob to be transformed 39 | @param[in] T the translation vector as a numpy array 40 | """ 41 | vertices = np.asarray(o.I.Editor.get_points()) 42 | vertices += T 43 | 44 | def transform_object(o: OGF.MeshGrob, xform: np.ndarray): 45 | """ 46 | @brief Applies a 4x4 homogeneous coord transform to object's vertices 47 | @details Does not call o.update(), it is caller's responsibility 48 | @param[in,out] o the MeshGrob to be transformed 49 | @param[in] xform the 4x4 homogeneous coordinates transform 50 | as a numpy array 51 | """ 52 | # if xform is identity, nothing to do 53 | if np.allclose(xform,np.eye(4)): 54 | return 55 | object_vertices = np.asarray(o.I.Editor.get_points()) 56 | vertices = np.c_[ # add a column of 1 57 | object_vertices, np.ones(object_vertices.shape[0]) 58 | ] 59 | # transform all the vertices 60 | vertices = np.matmul(vertices,np.transpose(xform)) 61 | weights = vertices[:,-1] # get 4th column 62 | weights = weights[:,np.newaxis] # make it a Nx1 matrix 63 | vertices = vertices[:,:-1] # get the x,y,z coords 64 | vertices = vertices/weights # divide the x,y,z coords by w 65 | # Could be written also in 1 line only (but less legible I think): 66 | # vertices = vertices[:,:-1] / vertices[:,-1][:,np.newaxis] 67 | np.copyto(object_vertices,vertices) # inject into graphite object 68 | 69 | def set_triangle_mesh(o: OGF.MeshGrob, vrtx: np.ndarray, T: np.ndarray): 70 | """ 71 | @brief sets a mesh from a vertices array and a triangle array 72 | @param[out] o: the target mesh 73 | @param[in] vrtx: an nv*3 array of vertices coordinates 74 | @param[in] T: an nt*3 array of vertices indices (starting from 0) 75 | """ 76 | o.clear() 77 | E = o.I.Editor 78 | E.create_vertices(vrtx.shape[0]) 79 | np.copyto(np.asarray(E.get_points()), vrtx) 80 | if T.shape[0] != 0: 81 | E.create_triangles(T.shape[0]) 82 | np.copyto(np.asarray(E.get_triangles()), T) 83 | E.connect_facets() 84 | o.update() 85 | 86 | 87 | def set_parametric_surface( 88 | o: OGF.MeshGrob, 89 | F: callable, 90 | nu: int = 10, nv: int = 10, 91 | umin: float = 0.0, umax: float = 1.0, 92 | vmin: float = 0.0, vmax: float = 1.0 93 | ): 94 | """ 95 | @brief sets a mesh from a parametric function 96 | @param[in] F: equation, as a function taking 97 | two numpy arrays U and V and returning three 98 | numpy arrays X,Y and Z 99 | @param[in] nu , nv: number of subdivisions 100 | @param[in] umin , umax , vmin , vmax: domain bounds 101 | """ 102 | U = np.linspace(umin, umax, nu) 103 | V = np.linspace(vmin, vmax, nv) 104 | V,U = np.meshgrid(V,U) 105 | X,Y,Z = F(U,V) 106 | XYZ = np.column_stack((X.flatten(),Y.flatten(),Z.flatten())) 107 | 108 | # create triangles grid 109 | 110 | # https://stackoverflow.com/questions/44934631/ 111 | # making-grid-triangular-mesh-quickly-with-numpy 112 | # 113 | # nu-1 * nv-1 squares 114 | # | two triangles per square 115 | # | | 116 | # | | three vertices per triangle 117 | # | | / 118 | # /-----\ | | 119 | T = np.empty((nu-1,nv-1,2,3),dtype=np.uint32) 120 | 121 | # 2D vertices indices array 122 | r = np.arange(nu*nv).reshape(nu,nv) 123 | 124 | # the six vertices of the two triangles 125 | T[:,:, 0,0] = r[:-1,:-1] # T0.i = (u,v) 126 | T[:,:, 1,0] = r[:-1,1:] # T1.i = (u,v+1) 127 | T[:,:, 0,1] = r[:-1,1:] # T0.j = (u,v+1) 128 | T[:,:, 1,1] = r[1:,1:] # T1.j = (u+1,v+1) 129 | T[:,:, :,2] = r[1:,:-1,None] # T0.k = T1.k = (u+1,j) 130 | 131 | # reshape triangles array 132 | T.shape =(-1,3) 133 | 134 | MeshGrobOps.set_triangle_mesh(o, XYZ, T) 135 | 136 | # if the parameterization winds around (sphere, torus...), 137 | # we need to glue the vertices 138 | o.I.Surface.repair_surface() 139 | -------------------------------------------------------------------------------- /PyGraphite/polyscope_views.py: -------------------------------------------------------------------------------- 1 | import polyscope as ps 2 | import numpy as np 3 | import time 4 | 5 | import gompy 6 | gom = gompy.interpreter() 7 | OGF = gom.meta_types.OGF 8 | 9 | from mesh_grob_ops import MeshGrobOps 10 | 11 | #==== PolyScope display for Graphite objects ============================== 12 | 13 | class GrobView: 14 | """ @brief Manages PolyScope structures associated with Graphite objects """ 15 | 16 | def __init__(self, grob : OGF.Grob): 17 | """ 18 | @brief GrobView constructor 19 | @param[in] grob the Grob this GrobView is associated with 20 | """ 21 | self.grob = grob 22 | self.connection = gom.connect(grob.value_changed,self.update) 23 | self.visible = True 24 | 25 | def __del__(self): 26 | """ 27 | @brief GrobView destructor 28 | @details removes PolyScope structures associated with this view 29 | """ 30 | self.remove() 31 | 32 | def show(self): 33 | """ 34 | @brief Shows this view 35 | """ 36 | self.visible = True 37 | 38 | def hide(self): 39 | """ 40 | @brief Hides this view 41 | """ 42 | self.visible = False 43 | 44 | def update(self,grob): 45 | """ 46 | @brief Reconstructs PolyScope structures 47 | @brief Called whenever the associated Grob changes 48 | """ 49 | None 50 | 51 | def remove(self): 52 | """ 53 | @brief Removes this View 54 | @details Called whenever the associated Grob no longer exists 55 | """ 56 | if self.connection != None: 57 | self.connection.remove() # Important! don't leave pending connections 58 | self.connection = None 59 | 60 | def commit_transform(self): 61 | """ 62 | @brief Applies transforms in PolyScope guizmos to Graphite objects 63 | """ 64 | None 65 | 66 | def highlight(self): 67 | """ 68 | @brief Briefly change object colors to highlight it 69 | """ 70 | None 71 | 72 | def unhighlight(self): 73 | """ 74 | @brief Restore previous colors of highlighted object 75 | """ 76 | None 77 | 78 | def get_structure_params(self, structure = None) -> dict: 79 | """ 80 | @brief Gets parameters of a PolyScope structure 81 | @details Parameters are what have a get_xxx and set_xxx functions 82 | @param[in] structure an optional PolyScope structure, if unspecified 83 | it is taken from self.structure 84 | @return a dictionary with the parameters 85 | """ 86 | if structure == None: 87 | structure = self.structure 88 | params = {} 89 | for getter_name in dir(structure): 90 | if ( 91 | getter_name.find('buffer') == -1 and 92 | getter_name != 'get_ignore_slice_plane' and 93 | getter_name.startswith('get_') 94 | ): 95 | param_name = getter_name.removeprefix('get_') 96 | setter_name = 'set_'+param_name 97 | if hasattr(structure, setter_name): 98 | param_val = getattr(structure, getter_name)() 99 | params[param_name] = param_val 100 | return params 101 | 102 | def set_structure_params(self, params, structure=None): 103 | """ 104 | @brief Sets parameters of a PolyScope structure 105 | @details Parameters are what have a get_xxx and set_xxx functions 106 | @param[in] params a dictionary with the parameters 107 | @param[in] structure an optional PolyScope structure, if unspecified 108 | it is taken from self.structure 109 | """ 110 | if structure == None: 111 | structure = self.structure 112 | for k,v in params.items(): 113 | setter_name = 'set_' + k 114 | if hasattr(self.structure, setter_name): 115 | getattr(structure,setter_name)(v) 116 | 117 | def copy_polyscope_params_to_grob(self): 118 | """ 119 | @brief copies polyscope parameters from structure to graphite Grob 120 | """ 121 | params = self.get_structure_params() 122 | for k,v in params.items(): 123 | self.grob.set_grob_attribute('polyscope.'+k, str(v)) 124 | 125 | def copy_grob_params_to_polyscope(self): 126 | """ 127 | @brief copies polyscope parameters from graphite Grob to structure 128 | and resets Grob attributes 129 | """ 130 | if self.structure == None: 131 | return 132 | for i in range(self.grob.nb_grob_attributes()): 133 | k = self.grob.ith_grob_attribute_name(i) 134 | v = self.grob.ith_grob_attribute_value(i) 135 | if not k.startswith('polyscope.') or v == '': 136 | continue 137 | 138 | # transform value from string to native type 139 | if v == 'True': # convert boolean 140 | v = True 141 | elif v == 'False': 142 | v = False 143 | elif v[0] == '(' and v[-1] == ')': # convert vector 144 | v = [ float(x) for x in v[1:-1].split(',') ] 145 | elif ( 146 | len(v) > 4 and 147 | v[0:2] == '[[' and 148 | v[-2:] == ']]' 149 | ): # convert matrix 150 | v = v.replace('[',' ').replace(']',' ') 151 | v = [ float(x) for x in v.split() ] 152 | v = np.asarray(v).reshape(4,4) 153 | else: 154 | try: 155 | v = float(v) # convert float 156 | except: 157 | None 158 | 159 | setter_name = 'set_'+k.removeprefix('polyscope.') 160 | try: 161 | if hasattr(self.structure,setter_name): 162 | getattr(self.structure,setter_name)(v) 163 | self.grob.set_grob_attribute(k,'') # reset grob attribute 164 | except: 165 | None 166 | 167 | class MeshGrobView(GrobView): 168 | """ PolyScope view for MeshGrob """ 169 | 170 | def __init__(self, o: OGF.MeshGrob): 171 | """ 172 | @brief GrobView constructor 173 | @param[in] grob the MeshGrob this GrobView is associated with 174 | """ 175 | super().__init__(o) 176 | self.structure = None 177 | self.old_attributes = [] 178 | self.shown_attribute = '' 179 | self.component_attributes = [] 180 | self.create_structures() 181 | 182 | def create_structures(self): 183 | """ 184 | @brief Creates PolyScope structures 185 | """ 186 | o = self.grob 187 | E = o.I.Editor 188 | pts = np.asarray(E.get_points())[:,0:3] # some meshes are in nD. 189 | 190 | if E.nb_facets == 0 and E.nb_cells == 0: 191 | if E.nb_vertices != 0: 192 | self.structure = ps.register_point_cloud(o.name,pts) 193 | elif E.nb_cells == 0: 194 | self.structure = ps.register_surface_mesh( 195 | o.name, pts, 196 | np.asarray(o.I.Editor.get_triangles()) 197 | ) 198 | else: 199 | self.structure = ps.register_volume_mesh( 200 | o.name, pts, 201 | np.asarray(o.I.Editor.get_tetrahedra()) 202 | ) 203 | 204 | if self.structure == None: 205 | return 206 | 207 | self.structure.set_enabled(self.visible) 208 | 209 | # Display scalar attributes 210 | new_attributes = self.grob.list_attributes('vertices','double',1) 211 | new_attributes = ( 212 | [] if new_attributes == '' else new_attributes.split(';') 213 | ) 214 | # If there is a new attribute, show it 215 | # (else keep shown attribute if any) 216 | for attr in new_attributes: 217 | if attr not in self.old_attributes: 218 | self.shown_attribute = attr 219 | for attr in new_attributes: 220 | attrarray = np.asarray(E.find_attribute(attr)) 221 | self.structure.add_scalar_quantity( 222 | attr.removeprefix('vertices.'), 223 | attrarray, enabled = (attr == self.shown_attribute) 224 | ) 225 | self.old_attributes = new_attributes 226 | 227 | # Display component attributes 228 | for (attr, component) in self.component_attributes: 229 | attrarray = np.asarray(E.find_attribute('vertices.'+attr)) 230 | attrname = attr + '[' + str(component) + ']' 231 | self.structure.add_scalar_quantity( 232 | attrname, attrarray[:,component], 233 | enabled = (attrname == self.shown_attribute) 234 | ) 235 | 236 | def remove_structures(self): 237 | """ 238 | @brief Removes PolyScope structures 239 | """ 240 | if self.structure != None: 241 | self.structure.remove() 242 | self.structure = None 243 | 244 | def remove(self): 245 | self.remove_structures() 246 | super().remove() 247 | 248 | def show(self): 249 | super().show() 250 | if self.structure == None: 251 | return 252 | self.structure.set_enabled(True) 253 | 254 | def hide(self): 255 | super().hide() 256 | if self.structure == None: 257 | return 258 | self.structure.set_enabled(False) 259 | 260 | def update(self,grob): 261 | super().update(grob) 262 | self.remove_structures() 263 | self.create_structures() 264 | 265 | def commit_transform(self): 266 | super().commit_transform() 267 | if self.structure == None: 268 | return 269 | xform = self.structure.get_transform() 270 | if not np.allclose(xform,np.eye(4)): 271 | MeshGrobOps.transform_object(self.grob,xform) 272 | self.structure.reset_transform() 273 | self.grob.update() 274 | 275 | def highlight(self): 276 | try: 277 | self.prev_color = self.structure.get_color() 278 | self.prev_edge_color = self.structure.get_edge_color() 279 | self.prev_edge_width = self.structure.get_edge_width() 280 | self.prev_material = self.structure.get_material() 281 | self.structure.set_color([0.1,0.1,0.1]) 282 | self.structure.set_edge_color([1,1,0]) 283 | self.structure.set_edge_width(1) 284 | self.structure.set_material('flat') 285 | self.structure.set_enabled(True) 286 | except: 287 | None 288 | 289 | def unhighlight(self): 290 | try: 291 | self.structure.set_color(self.prev_color) 292 | self.structure.set_edge_color(self.prev_edge_color) 293 | self.structure.set_edge_width(self.prev_edge_width) 294 | self.structure.set_material(self.prev_material) 295 | self.structure.set_enabled(self.visible) 296 | except: 297 | None 298 | 299 | def show_component_attribute(self, attribute : str, component : int): 300 | """ 301 | @brief shows a component of a vector attribute 302 | @param[in] attribute the attribute name 303 | @param[in] component the component, in 0..dim 304 | """ 305 | if (attribute, component) in self.component_attributes: 306 | gom.err('component attribute already shown') 307 | return 308 | attr = self.grob.I.Editor.find_attribute('vertices.'+attribute,True) 309 | if attr == None: 310 | gom.err('no such attribute') 311 | return 312 | if component >= attr.dimension: 313 | gom.err('component larger than attribute dimension') 314 | return 315 | self.shown_attribute = attribute + '[' + str(component) + ']' 316 | self.component_attributes.append((attribute,component)) 317 | 318 | #================================================================================ 319 | 320 | class VoxelGrobView(GrobView): 321 | """ PolyScope view for VoxelGrob """ 322 | def __init__(self, o: OGF.VoxelGrob): 323 | super().__init__(o) 324 | self.structure = None 325 | self.create_structures() 326 | self.old_attributes = [] 327 | self.shown_attribute = '' 328 | 329 | def create_structures(self): 330 | """ 331 | @brief Creates PolyScope structures 332 | """ 333 | E = self.grob.I.Editor 334 | bound_low = [ float(x) for x in E.origin.split()] 335 | U = [ float(x) for x in E.U.split()] 336 | V = [ float(x) for x in E.V.split()] 337 | W = [ float(x) for x in E.W.split()] 338 | bound_high = [bound_low[0] + U[0], bound_low[1]+V[1], bound_low[2]+W[2]] 339 | dims = [E.nu, E.nv, E.nw] 340 | self.structure = ps.register_volume_grid( 341 | self.grob.name, dims, bound_low, bound_high 342 | ) 343 | self.structure.set_enabled(self.visible) 344 | new_attributes = self.grob.displayable_attributes 345 | new_attributes = ( 346 | [] if new_attributes == '' else new_attributes.split(';') 347 | ) 348 | # If there is a new attribute, show it 349 | # (else keep shown attribute if any) 350 | for attr in new_attributes: 351 | if attr not in self.old_attributes: 352 | self.shown_attribute = attr 353 | for attr in new_attributes: 354 | attrarray = np.asarray(E.find_attribute(attr)) 355 | attrarray = attrarray.reshape(E.nu, E.nv, E.nw).transpose() 356 | self.structure.add_scalar_quantity( 357 | attr, attrarray, enabled = (self.shown_attribute == attr) 358 | ) 359 | self.old_attributes = new_attributes 360 | 361 | def remove_structures(self): 362 | """ 363 | @brief Removes PolyScope structures 364 | """ 365 | if self.structure != None: 366 | self.structure.remove() 367 | self.structure = None 368 | 369 | def remove(self): 370 | self.remove_structures() 371 | super().remove() 372 | 373 | def show(self): 374 | super().show() 375 | if self.structure == None: 376 | return 377 | self.structure.set_enabled(True) 378 | 379 | def hide(self): 380 | super().hide() 381 | if self.structure == None: 382 | return 383 | self.structure.set_enabled(False) 384 | 385 | def update(self,grob): 386 | super().update(grob) 387 | self.remove_structures() 388 | self.create_structures() 389 | 390 | def highlight(self): 391 | try: 392 | self.prev_edge_color = self.structure.get_edge_color() 393 | self.prev_edge_width = self.structure.get_edge_width() 394 | self.structure.set_edge_color([1,1,0]) 395 | self.structure.set_edge_width(1.5) 396 | self.structure.set_enabled(True) 397 | except: 398 | None 399 | 400 | def unhighlight(self): 401 | try: 402 | self.structure.set_edge_color(self.prev_edge_color) 403 | self.structure.set_edge_width(self.prev_edge_width) 404 | self.structure.set_enabled(self.visible) 405 | except: 406 | None 407 | 408 | #=============================================================================== 409 | 410 | class SceneGraphView(GrobView): 411 | """ 412 | @brief PolyScope view for Graphite SceneGraph 413 | @details Manages a dictionary that maps Grob names to PolyScope views 414 | """ 415 | 416 | def __init__(self, grob: OGF.SceneGraph): 417 | """ 418 | @brief SceneGraphView constructor 419 | @param[in] grob the SceneGraph this SceneGraphView is associated with 420 | """ 421 | super().__init__(grob) 422 | self.view_map = {} 423 | gom.connect(grob.values_changed, self.update_objects) 424 | self.highlighted = None 425 | self.highlight_timestamp = 0.0 426 | 427 | def update_objects(self,new_list: str): 428 | """ 429 | @brief Updates the list of objects 430 | @param[in] new_list the new list of objects as a ';'-separated string 431 | @details Called whenever the list of Graphite objects changed 432 | """ 433 | 434 | old_list = list(self.view_map.keys()) 435 | new_list = [] if new_list == '' else new_list.split(';') 436 | 437 | # Remove views for objects that are no longer there 438 | for objname in old_list: 439 | if objname not in new_list: 440 | self.view_map[objname].remove() 441 | del self.view_map[objname] 442 | 443 | # Create views for new objects 444 | for objname in new_list: 445 | object = getattr(self.grob.objects, objname) 446 | if objname not in self.view_map: 447 | viewclassname = ( 448 | object.meta_class.name.removeprefix('OGF::')+'View' 449 | ) 450 | try: 451 | self.view_map[objname] = globals()[viewclassname](object) 452 | except: 453 | print('Error: ', viewclassname, ' no such view class') 454 | self.view_map[objname] = GrobView(object) # dummy view 455 | 456 | # copy viewing parameters from loaded objects to polyscope 457 | self.copy_grob_params_to_polyscope() 458 | 459 | def show_all(self): 460 | """ @brief Shows all objects """ 461 | for shd in self.view_map.values(): 462 | shd.show() 463 | 464 | def hide_all(self): 465 | """ @brief Hides all objects """ 466 | for shd in self.view_map.values(): 467 | shd.hide() 468 | 469 | def show_only(self, obj: OGF.Grob): 470 | """ 471 | @brief Shows only one object 472 | @param[in] obj the object to be shown, all other objects will be hidden 473 | """ 474 | self.hide_all() 475 | self.view_map[obj.name].show() 476 | 477 | def commit_transform(self): 478 | super().commit_transform() 479 | for shd in self.view_map.values(): 480 | shd.commit_transform() 481 | 482 | def highlight_object(self, o:OGF.Grob): 483 | if self.highlighted != None: 484 | self.view_map[self.highlighted].unhighlight() 485 | self.view_map[o.name].highlight() 486 | self.highlighted = o.name 487 | self.highlight_timestamp = time.time() 488 | 489 | def unhighlight_object(self): 490 | if self.highlighted == None: 491 | return 492 | if time.time() - self.highlight_timestamp > 0.25: 493 | self.view_map[self.highlighted].unhighlight() 494 | self.highlighted = None 495 | 496 | def copy_polyscope_params_to_grob(self): 497 | for v in self.view_map.values(): 498 | v.copy_polyscope_params_to_grob() 499 | 500 | def copy_grob_params_to_polyscope(self): 501 | for v in self.view_map.values(): 502 | v.copy_grob_params_to_polyscope() 503 | 504 | def get_view(self, o: OGF.Grob) -> GrobView: 505 | """ 506 | @brief Gets the view associated with a graphite object 507 | @param[in] o: the object 508 | @return the GrobView associated with o 509 | """ 510 | return self.view_map.get(o.name, None) 511 | 512 | def get_views(self): 513 | """ 514 | @brief Gets all the views associated with a SceneGraph 515 | @return the list of views 516 | """ 517 | return self.view_map.values() 518 | 519 | #=========================================================== 520 | -------------------------------------------------------------------------------- /PyGraphite/pygraphite.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # TODO: 4 | # - Error messages, lock graphics 5 | # - Guizmo appears at a weird location (not always visible) 6 | # - Maybe the same "projection cube" as in Graphite to choose view 7 | # - multiple PolyScope objects for each Graphite object (points, borders,...) ? 8 | # - visualize vector fields 9 | # - commands that take attributes, get list from current object, as in Graphite 10 | # (parse special attributes) 11 | # - Some messages are not displayed in the tty 12 | # - Reset view on first object 13 | # - Splitter between scenelist and command 14 | 15 | import polyscope as ps, numpy as np # of course we need these two ones 16 | import sys # to get command line args 17 | import gompy # always import gompy *after* polyscope 18 | 19 | gom = gompy.interpreter() 20 | OGF = gom.meta_types.OGF 21 | 22 | from auto_gui import PyAutoGUI # to declare new Graphite cmds in Python 23 | from graphite_app import GraphiteApp # of course we need this one 24 | from mesh_grob_ops import MeshGrobOps # some geometric xforms in Python 25 | 26 | #===================================================== 27 | # Create the graphite application 28 | 29 | graphite = GraphiteApp() 30 | 31 | #====================================================== 32 | 33 | # Extend Graphite in Python ! 34 | # Add custom commands to Graphite Object Model, so that 35 | # they appear in the menus, exactly like native Graphite 36 | # commands written in C++ 37 | 38 | # Declare a new enum type 39 | PyAutoGUI.register_enum( 40 | 'OGF::FlipAxis', 41 | ['FLIP_X','FLIP_Y','FLIP_Z','ROT_X','ROT_Y','ROT_Z','PERM_XYZ'] 42 | ) 43 | 44 | # Declare a new Commands class for MeshGrob 45 | # The name should be something like MeshGrobXXXCommands 46 | class MeshGrobPyGraphiteCommands: 47 | 48 | # You can add your own functions here, take a look at 49 | # the following ones to have an idea of how to do that. 50 | # Python functions declared to Graphite need type hints, 51 | # so that the GUI can be automatically generated. 52 | # There are always two additional arguments that appear first: 53 | # -interface: the target of the function call 54 | # -method: a string with the name of the method called. It can be used 55 | # to dispatch several slots to the same function (but we don't do that here) 56 | # Note that Python functions declared to Graphite do not take self as 57 | # argument (they are like C++ static class functions) 58 | # Note the default value for the 'axis' and 'center' args in the docstring 59 | # (it would have been better to let one put it with type hints, 60 | # but I did not figure out a way of getting it from there) 61 | def flip_or_rotate( 62 | interface : OGF.Interface, 63 | method : str, 64 | axis : OGF.FlipAxis, # the new enum created above 65 | center : bool 66 | ): 67 | # docstring is used to generate the tooltip, menu, and have additional 68 | # information attached to the "custom attributes" of the MetaMethod. 69 | """ 70 | @brief flips axes of an object or rotate around an axis 71 | @param[in] axis = PERM_XYZ rotation axis or permutation 72 | @param[in] center = True if set, xform is relative to object's center 73 | @menu /Mesh 74 | """ 75 | 76 | # the Graphite object target of the command is obtained like that: 77 | grob = interface.grob 78 | 79 | if center: 80 | C = MeshGrobOps.get_object_center(grob) 81 | # MeshGrobOps are also implemented in Python, with numpy ! 82 | # (see mesh_grob_ops.py) 83 | MeshGrobOps.translate_object(grob, -C) 84 | 85 | # points array can be modified in-place ! 86 | # (for that, note pts_array[:,[0,1,2]] instead of just pts_array) 87 | pts_array = np.asarray(grob.I.Editor.get_points()) 88 | if axis == 'FLIP_X': 89 | pts_array[:,0] = -pts_array[:,0] 90 | elif axis == 'FLIP_Y': 91 | pts_array[:,1] = -pts_array[:,1] 92 | elif axis == 'FLIP_Z': 93 | pts_array[:,2] = -pts_array[:,2] 94 | elif axis == 'ROT_X': 95 | pts_array[:,[0,1,2]] = pts_array[:,[0,2,1]] 96 | pts_array[:,1] = -pts_array[:,1] 97 | elif axis == 'ROT_Y': 98 | pts_array[:,[0,1,2]] = pts_array[:,[2,1,0]] 99 | pts_array[:,2] = -pts_array[:,2] 100 | elif axis == 'ROT_Z': 101 | pts_array[:,[0,1,2]] = pts_array[:,[1,0,2]] 102 | pts_array[:,0] = -pts_array[:,0] 103 | elif axis == 'PERM_XYZ': 104 | pts_array[:,[0,1,2]] = pts_array[:,[1,2,0]] 105 | 106 | if center: 107 | MeshGrobOps.translate_object(grob, C) 108 | 109 | grob.update() # updates the PolyScope structures in the view 110 | 111 | def randomize( 112 | interface : OGF.Interface, 113 | method : str, 114 | howmuch : float 115 | ): 116 | """ 117 | @brief Applies a random perturbation to the vertices of a mesh 118 | @param[in] howmuch = 0.01 amount of perturbation rel to bbox diag 119 | @menu /Mesh 120 | """ 121 | grob = interface.grob 122 | howmuch = howmuch * MeshGrobOps.get_object_bbox_diagonal(grob) 123 | pts = np.asarray(grob.I.Editor.get_points()) 124 | pts += howmuch * np.random.rand(*pts.shape) 125 | grob.update() 126 | 127 | def inflate( 128 | interface : OGF.Interface, 129 | method : str, 130 | howmuch : float 131 | ): 132 | """ 133 | @brief Inflates a surface by moving its vertices along the normal 134 | @param[in] howmuch = 0.01 inflating amount rel to bbox diag 135 | @menu /Surface 136 | """ 137 | grob = interface.grob 138 | howmuch = howmuch * MeshGrobOps.get_object_bbox_diagonal(grob) 139 | grob.I.Attributes.compute_vertices_normals('normal') 140 | pts = np.asarray(grob.I.Editor.get_points()) 141 | N = np.asarray(grob.I.Editor.find_attribute('vertices.normal')) 142 | pts += howmuch * N 143 | grob.update() 144 | 145 | def mesh_as_tubes( 146 | interface : OGF.Interface, 147 | method : str, 148 | new_mesh : OGF.NewMeshGrobName, 149 | cyl_radius : float, 150 | sph_radius : float, 151 | cyl_prec : int, 152 | sph_prec : int 153 | ): 154 | """ 155 | @brief Creates a surface with edges as cylinders 156 | @param[in] new_mesh = tubes new mesh name 157 | @param[in] cyl_radius = 0.002 cylinders radius rel to bbox diag or 0 158 | @param[in] sph_radius = 0.003 spheres radius rel to bbox diag or 0 159 | @advanced 160 | @param[in] cyl_prec = 10 cylinder precision 161 | @param[in] sph_prec = 2 sphere precision 162 | """ 163 | grob = interface.grob 164 | R = MeshGrobOps.get_object_bbox_diagonal(grob) 165 | cyl_radius = cyl_radius * R 166 | sph_radius = sph_radius * R 167 | if grob.scene_graph().is_bound(new_mesh): 168 | tubes = grob.scene_graph().resolve(new_mesh) 169 | tubes.I.Editor.clear() 170 | else: 171 | tubes = OGF.MeshGrob(new_mesh) 172 | 173 | tubes.disable_signals() 174 | 175 | points = np.asarray(grob.I.Editor.get_points()) 176 | triangles = np.asarray(grob.I.Editor.get_triangles()) 177 | 178 | # Get all the edges of all triangles 179 | edges = np.concatenate( 180 | (triangles[:,[0,1]], 181 | triangles[:,[1,2]], 182 | triangles[:,[2,0]]), 183 | axis=0 184 | ) 185 | 186 | # Remove duplicates 187 | keep_edges = (edges[:,0] > edges[:,1]) 188 | edges = edges[keep_edges] 189 | 190 | # Lookup vertices 191 | edges_points = np.take(points, edges, axis=0) 192 | 193 | if cyl_radius != 0.0: 194 | # Can we avoid this loop ? 195 | for i in range(edges_points.shape[0]): 196 | p1 = edges_points[i][0] 197 | p2 = edges_points[i][1] 198 | tubes.I.Shapes.create_cylinder_from_extremities( 199 | p1, p2, cyl_radius, cyl_prec 200 | ) 201 | 202 | if sph_radius != 0.0: 203 | # Can we avoid this loop ? 204 | for i in range(points.shape[0]): 205 | p = points[i] 206 | tubes.I.Shapes.create_sphere( 207 | p, sph_radius, sph_prec 208 | ) 209 | 210 | tubes.enable_signals() 211 | tubes.update() 212 | 213 | 214 | def show_component_attribute( 215 | interface : OGF.Interface, 216 | method : str, 217 | attribute : str, 218 | component : OGF.index_t 219 | ): 220 | """ 221 | @brief sends a component of a vector attribute to Polyscope 222 | @param[in] attribute name of the attribute 223 | @param[in] component index of the component to be extracted 224 | @menu /Attributes/Polyscope 225 | """ 226 | grob = interface.grob 227 | view = graphite.scene_graph_view.get_view(grob) 228 | view.show_component_attribute(attribute,component) 229 | grob.update() 230 | 231 | def create_icosahedron( 232 | interface : OGF.Interface, 233 | method : str 234 | ): 235 | """ 236 | @brief replaces the current mesh with a unit icosahedron 237 | """ 238 | pts = np.array( 239 | [[ 0. , 0. , 1.175571 ], 240 | [ 1.051462 , 0. , 0.5257311], 241 | [ 0.3249197 , 1. , 0.5257311], 242 | [-0.8506508 , 0.618034 , 0.5257311], 243 | [-0.8506508 ,-0.618034 , 0.5257311], 244 | [ 0.3249197 ,-1. , 0.5257311], 245 | [ 0.8506508 , 0.618034 ,-0.5257311], 246 | [ 0.8506508 ,-0.618034 ,-0.5257311], 247 | [-0.3249197 , 1. ,-0.5257311], 248 | [-1.051462 , 0. ,-0.5257311], 249 | [-0.3249197 ,-1. ,-0.5257311], 250 | [ 0. , 0. ,-1.175571 ]]) 251 | 252 | tri = np.array( 253 | [[ 0 , 1 , 2], 254 | [ 0 , 2 , 3], 255 | [ 0 , 3 , 4], 256 | [ 0 , 4 , 5], 257 | [ 0 , 5 , 1], 258 | [ 1 , 5 , 7], 259 | [ 1 , 7 , 6], 260 | [ 1 , 6 , 2], 261 | [ 2 , 6 , 8], 262 | [ 2 , 8 , 3], 263 | [ 3 , 8 , 9], 264 | [ 3 , 9 , 4], 265 | [ 4 , 9 ,10], 266 | [ 4 ,10 , 5], 267 | [ 5 ,10 , 7], 268 | [ 6 , 7 ,11], 269 | [ 6 ,11 , 8], 270 | [ 7 ,10 ,11], 271 | [ 8 ,11 , 9], 272 | [ 9 ,11 ,10]], dtype=np.uint32) 273 | 274 | grob = interface.grob 275 | MeshGrobOps.set_triangle_mesh(grob, pts, tri) 276 | 277 | def create_UV_sphere( 278 | interface : OGF.Interface, 279 | method : str, 280 | ntheta : int, 281 | nphi : int 282 | ): 283 | """ 284 | @brief replaces the current mesh with a sphere 285 | @param[in] ntheta = 20 number of subdivisions around equator 286 | @param[in] nphi = 10 number of subdivisions around meridian 287 | """ 288 | MeshGrobOps.set_parametric_surface( 289 | interface.grob, 290 | lambda U,V: (np.cos(U)*np.cos(V),np.sin(U)*np.cos(V),np.sin(V)), 291 | ntheta, nphi, 292 | 0.0, 2.0*np.pi, 293 | -0.5*np.pi, 0.5*np.pi 294 | ) 295 | 296 | # register our new commands so that Graphite GUI sees them 297 | PyAutoGUI.register_commands( 298 | graphite.scene_graph, OGF.MeshGrob, MeshGrobPyGraphiteCommands 299 | ) 300 | 301 | #===================================================== 302 | # Initialize Polyscope and enter app main loop 303 | ps.set_program_name('PyGraphite/PolyScope') 304 | ps.init() 305 | ps.set_up_dir('z_up') 306 | ps.set_front_dir('y_front') 307 | graphite.run(sys.argv) # Let's rock and roll ! 308 | -------------------------------------------------------------------------------- /PyGraphite/terminal.py: -------------------------------------------------------------------------------- 1 | from rlcompleter import Completer 2 | import polyscope as ps, polyscope.imgui as imgui 3 | import sys 4 | import gompy 5 | 6 | gom = gompy.interpreter() 7 | 8 | #========================================================================= 9 | 10 | class GraphiteStream: 11 | def __init__(self, func): 12 | self.func = func 13 | def write(self, string): 14 | if string != '\n': 15 | self.func(string) 16 | def flush(self): 17 | return 18 | 19 | #========================================================================= 20 | 21 | def longestCommonPrefix(a): 22 | size = len(a) 23 | if (size == 0): 24 | return '' 25 | 26 | if (size == 1): 27 | return a[0] 28 | 29 | a.sort() 30 | end = min(len(a[0]), len(a[size - 1])) 31 | i = 0 32 | while (i < end and 33 | a[0][i] == a[size - 1][i]): 34 | i += 1 35 | 36 | pre = a[0][0: i] 37 | return pre 38 | 39 | #========================================================================= 40 | 41 | class Terminal: 42 | 43 | def __init__(self, app): 44 | self.visible = True 45 | self.command = '' 46 | self.queued_execute_command = False 47 | self.command_widget_id = 0 48 | self.completer = Completer() 49 | self.message = '' 50 | self.update_frames = 0 51 | self.focus = False 52 | self.app = app 53 | self.debug_mode = self.app.debug_mode 54 | self.printing = False 55 | application = self.app.scene_graph.application 56 | gom.connect(application.out, self.out_CB) 57 | gom.connect(application.err, self.err_CB) 58 | self.sys_stdout = sys.stdout 59 | self.sys_stderr = sys.stderr 60 | self.sys_displayhook = sys.displayhook 61 | self.term_stdout = GraphiteStream(gom.out) 62 | self.term_stderr = GraphiteStream(gom.err) 63 | self.term_displayhook = gom.out 64 | self.set_term_output() 65 | 66 | def set_term_output(self): 67 | if self.debug_mode: 68 | return 69 | sys.stdout = self.term_stdout 70 | sys.stderr = self.term_stderr 71 | sys.displayhook = self.term_displayhook 72 | 73 | def set_sys_output(self): 74 | if self.debug_mode: 75 | return 76 | sys.stdout = self.sys_stdout 77 | sys.stderr = self.sys_stderr 78 | sys.displayhook = self.sys_displayhook 79 | 80 | def draw(self): 81 | if not self.visible: 82 | return 83 | imgui.SetNextWindowPos( 84 | [660,ps.get_window_size()[1]-260],imgui.ImGuiCond_Once 85 | ) 86 | imgui.SetNextWindowSize([600,200],imgui.ImGuiCond_Once) 87 | _,self.visible = imgui.Begin('Terminal', self.visible) 88 | imgui.BeginChild('scrolling',[0.0,-25.0]) 89 | imgui.Text(self.message) 90 | if self.update_frames > 0: 91 | imgui.SetScrollY(imgui.GetScrollMaxY()) 92 | self.update_frames = self.update_frames - 1 93 | imgui.EndChild() 94 | imgui.Text('>') 95 | imgui.SameLine() 96 | imgui.PushItemWidth(-1) 97 | if self.focus: 98 | imgui.SetKeyboardFocusHere(0) 99 | self.focus = False 100 | sel,self.command = imgui.InputText( 101 | '##terminal##command##' + str(self.command_widget_id), 102 | self.command, 103 | imgui.ImGuiInputTextFlags_EnterReturnsTrue 104 | ) 105 | if imgui.IsItemActive(): 106 | if imgui.IsKeyPressed(imgui.ImGuiKey_Tab): 107 | self.shell_completion() 108 | if imgui.IsKeyPressed(imgui.ImGuiKey_UpArrow): 109 | self.shell_history_up() 110 | if imgui.IsKeyPressed(imgui.ImGuiKey_DownArrow): 111 | self.shell_history_down() 112 | if sel: 113 | self.queued_execute_command = True 114 | imgui.PopItemWidth() 115 | imgui.End() 116 | 117 | def print(self, msg: str): 118 | """ 119 | @brief Prints a message to the terminal window in Graphite 120 | @details Triggers graphics update for 3 frames, for leaving the 121 | slider enough time to reach the last line in the terminal 122 | @param[in] msg the message to be printed 123 | """ 124 | if self.printing or self.app.drawing: 125 | self.printing = True 126 | self.set_sys_output() 127 | self.update_frames = 0 128 | print(msg) 129 | self.set_term_output() 130 | self.printing = False 131 | else: 132 | self.printing = True 133 | self.message = self.message + msg 134 | self.update_frames = 3 # needs three frames for SetScrollY() 135 | # to do the job 136 | self.printing = False 137 | 138 | def handle_queued_command(self): 139 | if self.queued_execute_command: 140 | try: 141 | exec( 142 | self.command, 143 | {'graphite' : self.app, 'imgui' : imgui} 144 | ) 145 | except Exception as e: 146 | self.print('Error: ' + str(e) + '\n') 147 | self.queued_execute_command = False 148 | self.command = '' 149 | self.focus = True 150 | 151 | #============================================================== 152 | 153 | def shell_completion(self): 154 | """ 155 | @brief autocompletion 156 | @details called whenever TAB is pressed 157 | """ 158 | completions = [ ] 159 | i = 0 160 | while True: 161 | cmp = self.completer.complete(self.command,i) 162 | if cmp == None: 163 | break 164 | completions.append(cmp) 165 | i = i + 1 166 | if(len(completions) > 0): 167 | if(len(completions) > 1): 168 | self.print('\n') 169 | for i,comp in enumerate(completions): 170 | self.print('['+str(i)+'] ' + comp + '\n') 171 | self.command = longestCommonPrefix(completions) 172 | self.command_widget_id += 1 173 | self.focus = True 174 | 175 | def shell_history_up(self): 176 | """ 177 | @brief previous command in history 178 | @details called whenever UP key is pressed 179 | """ 180 | None 181 | 182 | def shell_history_down(self): 183 | """ 184 | @brief next command in history 185 | @details called whenever DOWN key is pressed 186 | """ 187 | None 188 | 189 | #============================================================== 190 | 191 | def out_CB(self,msg:str): 192 | """ 193 | @brief Message display callback 194 | @details Called whenever Graphite wants to display something. 195 | It generates a new PolyScope frame. Since commands are invoked outside 196 | of a PolyScope frame, and since messages are triggered by commands 197 | only, (normally) there can't be nested PolyScope frames. 198 | @param[in] msg the message to be displayed 199 | """ 200 | self.print(msg) 201 | if not self.printing and not self.app.drawing: 202 | while self.app.running and self.update_frames > 0: 203 | self.app.redraw() 204 | 205 | def err_CB(self,msg:str): 206 | """ 207 | @brief Error display callback 208 | @details Called whenever Graphite wants to display something. 209 | It generates a new PolyScope frame. Since commands are invoked outside 210 | of a PolyScope frame, and since messages are triggered by commands 211 | only, (normally) there can't be nested PolyScope frames. 212 | @param[in] msg the error message to be displayed 213 | """ 214 | self.visible=True # make terminal appear if it was hidden 215 | self.print(msg) 216 | if not self.printing and not self.app.drawing: 217 | while self.app.running and self.update_frames > 0: 218 | self.app.redraw() 219 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pygeogram 2 | Python bindings for [geogram](https://github.com/BrunoLevy/geogram) 3 | 4 | # Note 5 | This is work in progress, be patient, I first need to understand how wheels work (that is, precompiled Python packages), how to generate them, how to publish them so that one can "pip install" them... 6 | 7 | If you need to use geogram in Python **now** (I don't know how much time it will take me to learn all these things), there is a possibility: you can use [gompy](https://github.com/BrunoLevy/GraphiteThree/wiki/python), that is, Graphite's object model projected into Python. 8 | 9 | You may also be interesting in [pygraphite](https://github.com/BrunoLevy/pygeogram/tree/main/PyGraphite), 10 | a version of [Graphite](https://github.com/BrunoLevy/GraphiteThree) that runs 11 | in [PolyScope](https://polyscope.run/) using [PolyScope Python bindings](https://polyscope.run/py/) 12 | 13 | For now this repository mainly collects notes and links to documents and projects (thank you Twitter friends). 14 | 15 | # Links, resources 16 | - How to build wheels [here](https://gertjanvandenburg.com/blog/wheels/) 17 | - Github action to build a wheel [here](https://github.com/pypa/cibuildwheel) 18 | - [PyBind11](https://github.com/pybind/pybind11) 19 | - [NanoBind](https://github.com/wjakob/nanobind) 20 | 21 | # Examples of projects with actions that build wheels and that publish on PyPI 22 | - [Rhino3Dm](https://github.com/mcneel/rhino3dm) 23 | - [Nick Sharp's potpourri3d](https://github.com/nmwsharp/potpourri3d/blob/master/.github/workflows/build.yml) 24 | - [Lagrange](https://github.com/adobe/lagrange) 25 | --------------------------------------------------------------------------------