├── .gitmodules ├── screenshots └── blender-verse-screenshot.png ├── .gitignore ├── io_verse ├── user.py ├── draw3d.py ├── mesh_tools │ ├── test_tess_face.py │ └── tess_faces.py ├── __init__.py ├── connection.py ├── session.py ├── ui_scene.py ├── ui.py ├── scene.py ├── ui_avatar_view.py ├── object3d.py ├── ui_object3d.py ├── avatar_view.py └── mesh.py ├── README.md └── LICENSE /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "io_verse/vrsent"] 2 | path = io_verse/vrsent 3 | url = git@github.com:jirihnidek/verse-entities.git 4 | -------------------------------------------------------------------------------- /screenshots/blender-verse-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verse/verse-blender/HEAD/screenshots/blender-verse-screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | 29 | #SublimeText 30 | *.sublime-project 31 | *.sublime-workspace 32 | 33 | # PyCharm 34 | .idea 35 | -------------------------------------------------------------------------------- /io_verse/user.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | 20 | """ 21 | This module contains classes and methods for used vor visualization 22 | valid user at Verse server 23 | """ 24 | 25 | import bpy 26 | from .vrsent import vrsent 27 | from . import ui 28 | 29 | 30 | class BlenderUser(vrsent.VerseUser): 31 | """ 32 | This class represent Verse user account at Verse server 33 | """ 34 | 35 | def __init__(self, *args, **kwargs): 36 | """ 37 | Constructor of AvatarView node 38 | """ 39 | 40 | super(BlenderUser, self).__init__(*args, **kwargs) 41 | 42 | wm = bpy.context.window_manager 43 | wm.verse_users.add() 44 | wm.verse_users[-1].node_id = self.id 45 | 46 | # Force redraw of properties 47 | ui.update_all_views(('PROPERTIES',)) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Blender Add-on with Verse Integration 2 | ===================================== 3 | 4 | This is alpha version of Blender Python Add-on with Verse integration. It is 5 | possible to share Mesh objects at Verse server now. 6 | 7 | ![Blender Verse Add-on screenshot](/screenshots/blender-verse-screenshot.png "Verse Blender Add-on screenshot") 8 | 9 | ### Requirements ### 10 | 11 | This Add-on requires Blender compiled from source code with following patch: https://developer.blender.org/T42865 and Python module called Verse that could be found here: 12 | 13 | http://verse.github.com/verse/ 14 | 15 | Verse project contains compiled Python module and only Linux OS is supported now. 16 | 17 | > Note: This Add-on is very WIP and it is not intended for production. 18 | 19 | ### Installation ### 20 | 21 | To install this Add-on download file: 22 | 23 | https://github.com/verse/verse-blender/archive/master.zip 24 | 25 | Unzip this archive and move directory io_verse to directory with your add-ons. 26 | Typically ~/.config/blender/2.68a/scripts/addons/ 27 | 28 | You can also download current version using git: 29 | 30 | git clone git@github.com:verse/verse-blender.git # (requires SSH) 31 | # git clone https://github.com/verse/verse-blender.git # (only HTTPS) 32 | cd verse-blender 33 | git submodule update --init --recursive 34 | git submodule foreach --recursive git checkout master 35 | git submodule foreach --recursive git pull --rebase origin master 36 | 37 | To update verse-blender to current version run following commands: 38 | 39 | git pull --rebase 40 | git submodule foreach --recursive git pull --rebase origin master 41 | 42 | ### License ### 43 | 44 | The source code of this Blender Add-on is available under GNU GPL 2.0. For details 45 | look at LICENSE file. 46 | 47 | -------------------------------------------------------------------------------- /io_verse/draw3d.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | 20 | """ 21 | This file contains callback method used for drawing into the 3D view. 22 | """ 23 | 24 | 25 | import bpy 26 | from . import avatar_view 27 | from . import session 28 | from . import object3d 29 | 30 | # TODO: this should be in some class 31 | HANDLER = None 32 | 33 | 34 | def draw3d_cb(context): 35 | """ 36 | This draw callback for io_verse Add-on is called, when view to 3D is 37 | changed. 38 | """ 39 | 40 | # This callback should affect only for 3D View and should be executed, 41 | # when Blender is connected and subscribed to shared scene. 42 | if session.VerseSession.instance() is None or \ 43 | bpy.context.scene.subscribed is False or \ 44 | context.area.type != 'VIEW_3D': 45 | return 46 | 47 | # Draw all shared objects first 48 | for obj in object3d.VerseObject.objects.values(): 49 | obj.draw(context) 50 | 51 | # If avatar view of this client doesn't exist yet, then try to 52 | # get it 53 | my_avatar_view = avatar_view.AvatarView.my_view() 54 | if my_avatar_view is not None: 55 | # Update information about avatar's view, when needed 56 | my_avatar_view.update(context) 57 | 58 | # Draw other avatars, when there is any 59 | for avatar in avatar_view.AvatarView.other_views().values(): 60 | if avatar.visualized is True and \ 61 | context.scene.verse_node_id != -1 and \ 62 | context.scene.subscribed is True and \ 63 | context.scene.verse_node_id == avatar.scene_node_id.value[0]: 64 | avatar.draw(context) 65 | -------------------------------------------------------------------------------- /io_verse/mesh_tools/test_tess_face.py: -------------------------------------------------------------------------------- 1 | #!/bin/usr/env python 2 | 3 | # ##### BEGIN GPL LICENSE BLOCK ##### 4 | # 5 | # This program is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either version 2 8 | # of the License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software Foundation, 17 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | # 19 | # ##### END GPL LICENSE BLOCK ##### 20 | 21 | """ 22 | Module with unit tests for tess_face module 23 | """ 24 | 25 | import tess_faces as tf 26 | 27 | VERTICES = ((0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)) 28 | EDGES = ((0, 1), (1, 2), (2, 3), (3, 0)) 29 | TESS_FACES = ((0, 1, 2), (0, 2, 3)) 30 | POLYGONS = ((0, 1, 2, 3),) 31 | INNER_EDGES = ((0, 2),) 32 | 33 | 34 | def test_mesh_constructor(): 35 | """ 36 | Test creating new mesh 37 | """ 38 | mesh = tf.Mesh() 39 | assert len(mesh.vertices) == 0 40 | assert len(mesh.edges) == 0 41 | assert len(mesh.polygons) == 0 42 | assert len(mesh.tess_faces) == 0 43 | 44 | 45 | def test_add_vertices(): 46 | """ 47 | Test adding vertices to the mesh 48 | """ 49 | mesh = tf.Mesh() 50 | mesh.add_vertices(VERTICES) 51 | assert list(VERTICES) == list(mesh.vertices.values()) 52 | 53 | 54 | def test_add_edges(): 55 | """ 56 | Test adding edges to the mesh 57 | """ 58 | mesh = tf.Mesh() 59 | mesh.add_vertices(VERTICES) 60 | mesh.add_edges(EDGES) 61 | edges = [tuple(sorted(edge)) for edge in EDGES] 62 | assert edges == mesh.edges 63 | 64 | 65 | def test_add_tess_face(): 66 | """ 67 | Test adding tessellated edges 68 | """ 69 | mesh = tf.Mesh() 70 | mesh.add_vertices(VERTICES) 71 | mesh.add_edges(EDGES) 72 | for tess_face in TESS_FACES: 73 | mesh.add_tess_face(tess_face) 74 | for inner_edge in mesh.tess_faces.keys(): 75 | assert inner_edge in INNER_EDGES 76 | for polygon in mesh.polygons: 77 | assert tuple(polygon) in POLYGONS 78 | -------------------------------------------------------------------------------- /io_verse/__init__.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | """ 20 | Blender Add-on with Verse integration 21 | """ 22 | 23 | bl_info = { 24 | "name": "Verse Client", 25 | "author": "Jiri Hnidek", 26 | "version": (0, 1), 27 | "blender": (2, 6, 5), 28 | "location": "File > Verse", 29 | "description": "Adds integration of Verse protocol", 30 | "warning": "Alpha quality, Works only at Linux OS, Requires verse module", 31 | "wiki_url": "", 32 | "tracker_url": "", 33 | "category": "System"} 34 | 35 | 36 | import bpy 37 | import verse as vrs 38 | from . import session 39 | from . import connection 40 | from . import scene 41 | from . import ui_scene 42 | from . import avatar_view 43 | from . import ui_avatar_view 44 | from . import object3d 45 | from . import ui_object3d 46 | from . import mesh 47 | from . import ui 48 | from . import user 49 | 50 | 51 | def register(): 52 | """ 53 | Call register methods in submodules 54 | """ 55 | ui.register() 56 | session.register() 57 | connection.register() 58 | ui_scene.register() 59 | ui_avatar_view.register() 60 | ui_object3d.register() 61 | mesh.register() 62 | 63 | 64 | def unregister(): 65 | """ 66 | Call unregister methods in submodules 67 | """ 68 | ui.unregister() 69 | session.unregister() 70 | connection.unregister() 71 | ui_scene.unregister() 72 | ui_avatar_view.unregister() 73 | ui_object3d.unregister() 74 | mesh.unregister() 75 | 76 | 77 | # Print all debug messages 78 | vrs.set_debug_level(vrs.PRINT_DEBUG_MSG) 79 | vrs.set_client_info("Blender", bpy.app.version_string) 80 | 81 | 82 | if __name__ == "__main__": 83 | # Register all modules 84 | register() 85 | -------------------------------------------------------------------------------- /io_verse/mesh_tools/tess_faces.py: -------------------------------------------------------------------------------- 1 | #!/bin/usr/env python 2 | 3 | # ##### BEGIN GPL LICENSE BLOCK ##### 4 | # 5 | # This program is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either version 2 8 | # of the License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software Foundation, 17 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | # 19 | # ##### END GPL LICENSE BLOCK ##### 20 | 21 | 22 | """ 23 | This module tries to reconstruct tessellated faces 24 | """ 25 | 26 | 27 | def sorted_edge(edge): 28 | """ 29 | This function returns sorted edge; e.g.: (4, 1) -> (1, 4). 30 | """ 31 | return (edge[0], edge[1]) if edge[0] < edge[1] else (edge[1], edge[0]) 32 | 33 | 34 | def extend_polygon(polygon, face, inner_edge): 35 | """ 36 | This function extend polygon by unique vertices from face that 37 | are not part of shared inner edge 38 | """ 39 | polygon.extend([vert for vert in face if vert not in inner_edge]) 40 | 41 | 42 | class EdgeLooper(object): 43 | """ 44 | Iterator of Face edges 45 | """ 46 | 47 | def __init__(self, face): 48 | """ 49 | Constructor of EdgeLooper 50 | """ 51 | self.face = face 52 | self.edge_index = 0 53 | 54 | def __iter__(self): 55 | """ 56 | This method returns iterator 57 | """ 58 | return self 59 | 60 | def __getitem__(self, index): 61 | """ 62 | This method returns edge at given index 63 | """ 64 | if index < 0 or index >= len(self.face): 65 | raise IndexError 66 | else: 67 | return (self.face[index], self.face[index + 1]) \ 68 | if (index + 1) < len(self.face) else \ 69 | (self.face[index], self.face[0]) 70 | 71 | def __next__(self): 72 | """ 73 | This method returns next face of face 74 | """ 75 | try: 76 | edge = self[self.edge_index] 77 | except IndexError: 78 | raise StopIteration 79 | else: 80 | self.edge_index += 1 81 | return sorted_edge(edge) 82 | 83 | def next(self): 84 | """ 85 | Backward compatibility for Python 2.x 86 | """ 87 | return self.__next__() 88 | 89 | 90 | class Mesh(object): 91 | """ 92 | Class representing Mesh 93 | """ 94 | 95 | def __init__(self): 96 | """ 97 | Constructor 98 | """ 99 | self.vertices = {} 100 | self.edges = [] 101 | self.polygons = [] 102 | self.tess_faces = {} 103 | 104 | def add_vertices(self, verts): 105 | """ 106 | This method adds vertices to mesh 107 | """ 108 | for index, vert in enumerate(verts): 109 | self.vertices[index] = vert 110 | 111 | def add_edges(self, edges): 112 | """ 113 | This method adds edges to the mesh 114 | """ 115 | self.edges.extend([sorted_edge(edge) for edge in edges]) 116 | 117 | def add_tess_face(self, face): 118 | """ 119 | This method adds tessellated face to mesh 120 | """ 121 | # Create iterator of all face edges 122 | edge_looper = EdgeLooper(face) 123 | # Create list of inner edges of polygon from face 124 | inner_edges = [edge for edge in edge_looper if edge not in self.edges] 125 | # Add tesselated face to list of faces 126 | for inner_edge in inner_edges: 127 | # When there is already tesselated face with same inner edge, then 128 | # extend this tesselated face with unique vertices from current face 129 | if inner_edge in self.tess_faces.keys(): 130 | polygon = self.tess_faces[inner_edge] 131 | extend_polygon(polygon, face, inner_edge) 132 | # The inner_edge is no longer inner edge, then pop this 133 | # key from dictionary 134 | self.tess_faces.pop(inner_edge) 135 | # When polygon is not included in dictionary of tesselated faces, 136 | # then it means, that polygon doesn't contain any inner edges. 137 | # The polygon is complete and can be added to the list of polygons. 138 | if polygon not in self.tess_faces.values(): 139 | self.polygons.append(polygon) 140 | else: 141 | self.tess_faces[inner_edge] = list(face) 142 | 143 | 144 | def main(): 145 | """ 146 | Main function for testing 147 | """ 148 | mesh = Mesh() 149 | mesh.add_vertices(((0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0))) 150 | mesh.add_edges(((0, 1), (1, 2), (2, 3), (3, 0))) 151 | mesh.add_tess_face((0, 1, 2)) 152 | mesh.add_tess_face((0, 2, 3)) 153 | print(mesh.vertices) 154 | print(mesh.edges) 155 | print(mesh.tess_faces) 156 | print(mesh.polygons) 157 | 158 | 159 | if __name__ == '__main__': 160 | main() 161 | -------------------------------------------------------------------------------- /io_verse/connection.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | 20 | """ 21 | This module is used for connecting to Verse server. It adds some 22 | menu items and there are class operator definition for connect 23 | dialogs. 24 | """ 25 | 26 | 27 | import bpy 28 | import verse as vrs 29 | from .vrsent import vrsent 30 | from . import session 31 | from . import draw3d 32 | 33 | 34 | class VerseAuthDialogOperator(bpy.types.Operator): 35 | """ 36 | Class with user authenticate dialog (username and password) 37 | """ 38 | bl_idname = "scene.verse_auth_dialog_operator" 39 | bl_label = "User Authenticate dialog" 40 | 41 | dialog_username = bpy.props.StringProperty(name="Username") 42 | dialog_password = bpy.props.StringProperty(name="Password", subtype='PASSWORD') 43 | 44 | def __init__(self): 45 | pass 46 | 47 | def execute(self, context): 48 | vrs_session = session.VerseSession.instance() 49 | if vrs_session is not None: 50 | vrs_session.my_username = self.dialog_username 51 | vrs_session.my_password = self.dialog_password 52 | vrs_session.send_user_authenticate(self.dialog_username, vrs.UA_METHOD_NONE, "") 53 | return {'FINISHED'} 54 | 55 | def invoke(self, context, event): 56 | wm = context.window_manager 57 | return wm.invoke_props_dialog(self) 58 | 59 | 60 | class VerseConnectDialogOperator(bpy.types.Operator): 61 | """ 62 | Class with connect dialog, where user can choose URL of 63 | Verse server 64 | """ 65 | bl_idname = "scene.verse_connect_dialog_operator" 66 | bl_label = "Connect Dialog" 67 | bl_description = "Dialog for setting verse server and port" 68 | 69 | vrs_server_name = bpy.props.StringProperty(name="Verse Server") 70 | vrs_server_port = bpy.props.StringProperty(name="Port") 71 | 72 | def execute(self, context): 73 | # Connect to Verse server 74 | session.VerseSession(self.vrs_server_name, self.vrs_server_port, vrs.DGRAM_SEC_NONE) 75 | # Start timer and callback function 76 | bpy.ops.wm.modal_timer_operator() 77 | # Add draw callback 78 | draw3d.HANDLER = bpy.types.SpaceView3D.draw_handler_add(draw3d.draw3d_cb, (context,), 'WINDOW', 'POST_PIXEL') 79 | return {'FINISHED'} 80 | 81 | def invoke(self, context, event): 82 | wm = context.window_manager 83 | return wm.invoke_props_dialog(self) 84 | 85 | 86 | class VerseClientDisconnect(bpy.types.Operator): 87 | """ 88 | This class will try to disconnect Blender from Verse server 89 | """ 90 | bl_idname = "scene.verse_client_disconnect" 91 | bl_label = "Disconnect" 92 | bl_description = "Disconnect from Verse server" 93 | 94 | @classmethod 95 | def poll(cls, context): 96 | if session.VerseSession.instance() is not None: 97 | state = session.VerseSession.instance().state 98 | else: 99 | return False 100 | if state == 'CONNECTING' or state == 'CONNECTED': 101 | return True 102 | else: 103 | return False 104 | 105 | def execute(self, context): 106 | vrs_session = session.VerseSession.instance() 107 | # Send disconnect request to verse server 108 | vrs_session.send_connect_terminate() 109 | # Remove callback for 3d view 110 | bpy.types.SpaceView3D.draw_handler_remove(draw3d.HANDLER, 'WINDOW') 111 | return {'FINISHED'} 112 | 113 | 114 | class VerseClientConnect(bpy.types.Operator): 115 | """ 116 | This class will try to connect Blender to Verse server 117 | """ 118 | bl_idname = "scene.verse_client_connect" 119 | bl_label = "Connect ..." 120 | bl_description = "Connect to Verse server" 121 | 122 | @classmethod 123 | def poll(cls, context): 124 | if session.VerseSession.instance() is not None: 125 | state = session.VerseSession.instance().state 126 | else: 127 | return True 128 | if state == 'DISCONNECTED': 129 | return True 130 | else: 131 | return False 132 | 133 | def execute(self, context): 134 | bpy.ops.scene.verse_connect_dialog_operator( 135 | 'INVOKE_DEFAULT', 136 | vrs_server_name='localhost', 137 | vrs_server_port='12345' 138 | ) 139 | return {'FINISHED'} 140 | 141 | 142 | class VerseMenu(bpy.types.Menu): 143 | """ 144 | Main Verse menu (it contains Connect... and Disconnect...) 145 | """ 146 | bl_label = "Verse Menu" 147 | bl_idname = "INFO_MT_verse" 148 | 149 | def draw(self, context): 150 | layout = self.layout 151 | 152 | layout.operator("scene.verse_client_connect") 153 | layout.operator("scene.verse_client_disconnect") 154 | 155 | 156 | def draw_item(self, context): 157 | """ 158 | This function draw item with Verse submenu 159 | """ 160 | layout = self.layout 161 | layout.menu(VerseMenu.bl_idname) 162 | 163 | 164 | def init_connection_properties(): 165 | """ 166 | This method initialize properties related to connection 167 | """ 168 | bpy.types.WindowManager.verse_connected = bpy.props.BoolProperty( 169 | name="Connected to Server", 170 | default=False, 171 | description="Is Blender connected to Verse server" 172 | ) 173 | 174 | 175 | # List of Blender classes in this submodule 176 | classes = ( 177 | VerseAuthDialogOperator, 178 | VerseConnectDialogOperator, 179 | VerseClientConnect, 180 | VerseClientDisconnect, 181 | VerseMenu 182 | ) 183 | 184 | 185 | def register(): 186 | """ 187 | This method register all methods of this submodule and 188 | adds Verse submenu to the File menu 189 | """ 190 | 191 | for c in classes: 192 | bpy.utils.register_class(c) 193 | 194 | bpy.types.INFO_MT_file.append(draw_item) 195 | 196 | init_connection_properties() 197 | 198 | 199 | def unregister(): 200 | """ 201 | This method unregister all methods of this submodule and 202 | removes Verse submenu from File menu 203 | """ 204 | 205 | for c in classes: 206 | bpy.utils.unregister_class(c) 207 | 208 | bpy.types.INFO_MT_file.remove(draw_item) 209 | -------------------------------------------------------------------------------- /io_verse/session.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | 20 | """ 21 | This module is used for handling session with Verse server. Blender 22 | can be connected only to one Verse server. Thus there could be only 23 | one session in one Blender instance. 24 | """ 25 | 26 | # Default FPS for timer operator 27 | FPS = 15 28 | 29 | 30 | import bpy 31 | import verse as vrs 32 | from .vrsent import vrsent 33 | from . import ui 34 | 35 | 36 | # VerseSession class 37 | class VerseSession(vrsent.VerseSession): 38 | """ 39 | Class Session for this Python client 40 | """ 41 | 42 | # Blender could be connected only to one Verse server 43 | __instance = None 44 | 45 | @classmethod 46 | def instance(cls): 47 | """ 48 | instance() -> object 49 | Class getter of instance 50 | """ 51 | return cls.__instance 52 | 53 | def __init__(self, hostname, service, flag): 54 | """ 55 | __init__(hostname, service, flag) -> None 56 | """ 57 | # Call __init__ from parent class to connect to Verse server 58 | super(VerseSession, self).__init__(hostname, service, flag) 59 | self.__class__.__instance = self 60 | self.debug_print = True 61 | 62 | def __del__(self): 63 | """ 64 | __del__() -> None 65 | """ 66 | self.__class__.__instance = None 67 | 68 | def cb_receive_connect_terminate(self, error): 69 | """ 70 | receive_connect_terminate(error) -> none 71 | """ 72 | # Call parent method to print debug information 73 | super(VerseSession, self).cb_receive_connect_terminate(error) 74 | self.__class__.__instance = None 75 | # Clear dictionary of nodes 76 | self.nodes.clear() 77 | 78 | # Stop capturing of current view to 3D View 79 | # Save current context to 3d view, start capturing and 80 | # then restore original context 81 | if bpy.context.window_manager.verse_connected is True: 82 | original_type = bpy.context.area.type 83 | bpy.context.area.type = 'VIEW_3D' 84 | bpy.ops.view3d.verse_avatar() 85 | bpy.context.area.type = original_type 86 | 87 | # Reset all properties 88 | ui.reset_avatar_properties() 89 | ui.reset_scene_properties() 90 | ui.reset_object_properties() 91 | 92 | # Set Blender property 93 | bpy.context.window_manager.verse_connected = False 94 | 95 | ui.update_all_views(('PROPERTIES', 'VIEW_3D')) 96 | 97 | def cb_receive_connect_accept(self, user_id, avatar_id): 98 | """ 99 | _receive_connect_accept(self, user_id, avatar_id) -> None 100 | """ 101 | super(VerseSession, self).cb_receive_connect_accept(user_id, avatar_id) 102 | 103 | # Set Blender property 104 | bpy.context.window_manager.verse_connected = True 105 | 106 | ui.update_all_views(('PROPERTIES', 'VIEW_3D')) 107 | 108 | def cb_receive_user_authenticate(self, username, methods): 109 | """ 110 | _receive_user_authenticate(self, username, methods) -> None 111 | """ 112 | if username == '': 113 | bpy.ops.scene.verse_auth_dialog_operator('INVOKE_DEFAULT') 114 | else: 115 | if username == self.my_username: 116 | self.send_user_authenticate(self.my_username, vrs.UA_METHOD_PASSWORD, self.my_password) 117 | 118 | def cb_receive_node_create(self, node_id, parent_id, user_id, custom_type): 119 | """ 120 | _receive_node_create(self, node_id, parent_id, user_id, type) -> None 121 | """ 122 | return super(VerseSession, self).cb_receive_node_create(node_id, parent_id, user_id, custom_type) 123 | 124 | def cb_receive_node_destroy(self, node_id): 125 | """ 126 | _receive_node_destroy(self, node_id) -> None 127 | """ 128 | # Call parent method to print debug information 129 | return super(VerseSession, self).cb_receive_node_destroy(node_id) 130 | 131 | def cb_receive_node_link(self, parent_node_id, child_node_id): 132 | """ 133 | _receive_node_link(self, parent_node_id, child_node_id) -> None 134 | """ 135 | # Call parent method to print debug information 136 | return super(VerseSession, self).cb_receive_node_link(parent_node_id, child_node_id) 137 | 138 | def cb_receive_node_perm(self, node_id, user_id, perm): 139 | """ 140 | _receive_node_perm(self, node_id, user_id, perm) -> None 141 | """ 142 | # Call parent method to print debug information 143 | return super(VerseSession, self).cb_receive_node_perm(node_id, user_id, perm) 144 | 145 | def cb_receive_taggroup_create(self, node_id, taggroup_id, custom_type): 146 | """ 147 | _receive_taggroup_create(self, node_id, taggroup_id, custom_type) -> None 148 | """ 149 | # Call parent method to print debug information 150 | return super(VerseSession, self).cb_receive_taggroup_create(node_id, taggroup_id, custom_type) 151 | 152 | def cb_receive_taggroup_destroy(self, node_id, taggroup_id): 153 | """ 154 | _receive_taggroup_destroy(self, node_id, taggroup_id) -> None 155 | """ 156 | # Call parent method to print debug information 157 | return super(VerseSession, self).cb_receive_taggroup_destroy(node_id, taggroup_id) 158 | 159 | def cb_receive_tag_create(self, node_id, taggroup_id, tag_id, data_type, count, custom_type): 160 | """ 161 | _receive_tag_create(self, node_id, taggroup_id, tag_id, data_type, count, custom_type) -> None 162 | """ 163 | # Call parent method to print debug information 164 | return super(VerseSession, self).cb_receive_tag_create(node_id, taggroup_id, tag_id, 165 | data_type, count, custom_type) 166 | 167 | def cb_receive_tag_destroy(self, node_id, taggroup_id, tag_id): 168 | """ 169 | _receive_tag_destroy(self, node_id, taggroup_id, tag_id) -> None 170 | """ 171 | # Call parent method to print debug information 172 | return super(VerseSession, self).cb_receive_tag_destroy(node_id, taggroup_id, tag_id) 173 | 174 | def cb_receive_tag_set_values(self, node_id, taggroup_id, tag_id, value): 175 | """ 176 | Custom callback method that is called, when client received command tag set value 177 | """ 178 | # Call parent method to print debug information and get modified tag 179 | return super(VerseSession, self).cb_receive_tag_set_values(node_id, taggroup_id, tag_id, value) 180 | 181 | 182 | class ModalTimerOperator(bpy.types.Operator): 183 | """ 184 | Operator which runs its self from a timer 185 | """ 186 | bl_idname = "wm.modal_timer_operator" 187 | bl_label = "Modal Timer Operator" 188 | 189 | _timer = None 190 | 191 | def modal(self, context, event): 192 | """ 193 | This method is called periodically and it is used to call callback 194 | methods, when appropriate command is received. 195 | """ 196 | if event.type == 'TIMER': 197 | vrs_session = VerseSession.instance() 198 | if vrs_session is not None: 199 | try: 200 | vrs_session.callback_update() 201 | except vrs.VerseError: 202 | del vrs_session 203 | return {'CANCELLED'} 204 | return {'PASS_THROUGH'} 205 | 206 | def execute(self, context): 207 | """ 208 | This method add timer 209 | """ 210 | self._timer = context.window_manager.event_timer_add(1.0 / FPS, context.window) 211 | context.window_manager.modal_handler_add(self) 212 | return {'RUNNING_MODAL'} 213 | 214 | def cancel(self, context): 215 | """ 216 | This method remove timer 217 | """ 218 | context.window_manager.event_timer_remove(self._timer) 219 | return None 220 | 221 | 222 | # List of Blender classes in this submodule 223 | classes = ( 224 | ModalTimerOperator, 225 | ) 226 | 227 | 228 | def register(): 229 | """ 230 | This method register all methods of this submodule 231 | """ 232 | for c in classes: 233 | bpy.utils.register_class(c) 234 | 235 | 236 | def unregister(): 237 | """ 238 | This method unregister all methods of this submodule 239 | """ 240 | for c in classes: 241 | bpy.utils.unregister_class(c) 242 | -------------------------------------------------------------------------------- /io_verse/ui_scene.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | """ 20 | This module implements sharing Blender scenes at Verse server 21 | """ 22 | 23 | import bpy 24 | from . import session 25 | from . import ui 26 | from . import scene 27 | 28 | 29 | class VERSE_SCENE_OT_share(bpy.types.Operator): 30 | """ 31 | This operator starts to share current Blender scene at Verse server. 32 | """ 33 | bl_idname = 'scene.blender_scene_share' 34 | bl_label = "Share at Verse" 35 | bl_description = "Share current Blender scene at Verse scene as new Verse scene node" 36 | 37 | def invoke(self, context, event): 38 | """ 39 | Operator for subscribing to Verse scene node 40 | """ 41 | vrs_session = session.VerseSession.instance() 42 | scene.VerseScene(session=vrs_session, name=(context.scene.name,)) 43 | return {'FINISHED'} 44 | 45 | @classmethod 46 | def poll(cls, context): 47 | """ 48 | This class method is used, when Blender check, if this operator can be 49 | executed 50 | """ 51 | # Return true only in situation, when client is connected to Verse server 52 | wm = context.window_manager 53 | if wm.verse_connected == True and context.scene.verse_node_id == -1: 54 | return True 55 | else: 56 | return False 57 | 58 | 59 | class VERSE_SCENE_OT_subscribe(bpy.types.Operator): 60 | """ 61 | This operator subscribes to existing scene shared at Verse server. 62 | It will create new Blender scene in current .blend file. 63 | """ 64 | bl_idname = 'scene.verse_scene_node_subscribe' 65 | bl_label = "Subscribe to Scene" 66 | bl_description = "Subscribe to verse scene node" 67 | 68 | def invoke(self, context, event): 69 | """ 70 | Operator for subscribing to Verse scene node 71 | """ 72 | vrs_session = session.VerseSession.instance() 73 | scene = context.scene 74 | scene_item = scene.verse_scenes[scene.cur_verse_scene_index] 75 | try: 76 | verse_scene_data = vrs_session.nodes[scene_item.data_node_id] 77 | except KeyError: 78 | return {'CANCELLED'} 79 | else: 80 | # Send node subscribe to the selected scene data node 81 | verse_scene_data.subscribe() 82 | return {'FINISHED'} 83 | 84 | @classmethod 85 | def poll(cls, context): 86 | """ 87 | This class method is used, when Blender check, if this operator can be 88 | executed 89 | """ 90 | # Allow this operator only in situation, when Blender is not subscribed 91 | # to any scene node 92 | wm = context.window_manager 93 | scene = context.scene 94 | if wm.verse_connected == True and scene.cur_verse_scene_index != -1: 95 | vrs_session = session.VerseSession.instance() 96 | for scene_item in scene.verse_scenes: 97 | try: 98 | verse_scene_data = vrs_session.nodes[scene_item.data_node_id] 99 | except KeyError: 100 | continue 101 | if verse_scene_data.subscribed is True: 102 | return False 103 | return True 104 | else: 105 | return False 106 | 107 | 108 | class VERSE_SCENE_OT_unsubscribe(bpy.types.Operator): 109 | """ 110 | This operator unsubscribes from scene node. 111 | """ 112 | bl_idname = 'scene.verse_scene_node_unsubscribe' 113 | bl_label = "Unsubscribe from Scene" 114 | bl_description = "Unsubscribe from Verse scene node" 115 | 116 | def invoke(self, context, event): 117 | """ 118 | Operator for unsubscribing from Verse scene node 119 | """ 120 | vrs_session = session.VerseSession.instance() 121 | scene = context.scene 122 | scene_item = scene.verse_scenes[scene.cur_verse_scene_index] 123 | try: 124 | verse_scene_data = vrs_session.nodes[scene_item.data_node_id] 125 | except KeyError: 126 | return {'CANCELLED'} 127 | else: 128 | # Send node unsubscribe to the selected scene data node 129 | verse_scene_data.unsubscribe() 130 | return {'FINISHED'} 131 | 132 | @classmethod 133 | def poll(cls, context): 134 | """ 135 | This class method is used, when Blender check, if this operator can be 136 | executed 137 | """ 138 | # Allow this operator only in situation, when scene with subscribed 139 | # data node is selected 140 | wm = context.window_manager 141 | scene = context.scene 142 | if wm.verse_connected is True and scene.cur_verse_scene_index != -1: 143 | scene_item = scene.verse_scenes[scene.cur_verse_scene_index] 144 | vrs_session = session.VerseSession.instance() 145 | try: 146 | verse_scene_data = vrs_session.nodes[scene_item.data_node_id] 147 | except KeyError: 148 | return False 149 | if verse_scene_data.subscribed is True: 150 | return True 151 | else: 152 | return False 153 | else: 154 | return False 155 | 156 | 157 | class VERSE_SCENE_UL_slot(bpy.types.UIList): 158 | """ 159 | A custom slot with information about Verse scene node 160 | """ 161 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 162 | vrs_session = session.VerseSession.instance() 163 | if vrs_session is not None: 164 | try: 165 | verse_scene = vrs_session.nodes[item.node_id] 166 | except KeyError: 167 | return 168 | if self.layout_type in {'DEFAULT', 'COMPACT'}: 169 | layout.label(verse_scene.name, icon='SCENE_DATA') 170 | try: 171 | verse_scene_data = vrs_session.nodes[item.data_node_id] 172 | except KeyError: 173 | pass 174 | else: 175 | if verse_scene_data.subscribed is True: 176 | layout.label('', icon='FILE_TICK') 177 | elif self.layout_type in {'GRID'}: 178 | layout.alignment = 'CENTER' 179 | layout.label(verse_scene.name) 180 | 181 | 182 | class VERSE_SCENE_MT_menu(bpy.types.Menu): 183 | """ 184 | Menu for scene list 185 | """ 186 | bl_idname = 'scene.verse_scene_menu' 187 | bl_label = 'Verse Scene Specials' 188 | bl_description = 'Menu for list of Verse scenes' 189 | 190 | def draw(self, context): 191 | """ 192 | Draw menu 193 | """ 194 | layout = self.layout 195 | layout.operator('scene.blender_scene_share') 196 | layout.operator('scene.verse_scene_node_subscribe') 197 | layout.operator('scene.verse_scene_node_unsubscribe') 198 | 199 | @classmethod 200 | def poll(cls, context): 201 | """ 202 | This class method is used, when Blender check, if this operator can be 203 | executed 204 | """ 205 | scene = context.scene 206 | 207 | # Return true only in situation, when client is connected to Verse server 208 | if scene.cur_verse_scene_index >= 0 and \ 209 | len(scene.verse_scenes) > 0: 210 | return True 211 | else: 212 | return False 213 | 214 | 215 | class VERSE_SCENE_panel(bpy.types.Panel): 216 | """ 217 | GUI of Verse scene shared at Verse server 218 | """ 219 | bl_space_type = 'PROPERTIES' 220 | bl_region_type = 'WINDOW' 221 | bl_context = 'scene' 222 | bl_label = 'Verse Scenes' 223 | bl_description = 'Panel with Verse scenes shared at Verse server' 224 | 225 | @classmethod 226 | def poll(cls, context): 227 | """ 228 | Can be this panel visible? 229 | """ 230 | # Return true only in situation, when client is connected to Verse server 231 | wm = context.window_manager 232 | if wm.verse_connected is True: 233 | return True 234 | else: 235 | return False 236 | 237 | def draw(self, context): 238 | """ 239 | This method draw panel of Verse scenes 240 | """ 241 | scene = context.scene 242 | layout = self.layout 243 | 244 | row = layout.row() 245 | 246 | row.template_list('VERSE_SCENE_UL_slot', 247 | 'verse_scenes_widget_id', 248 | scene, 249 | 'verse_scenes', 250 | scene, 251 | 'cur_verse_scene_index', 252 | rows=3) 253 | 254 | col = row.column(align=True) 255 | col.menu('scene.verse_scene_menu', icon='DOWNARROW_HLT', text="") 256 | 257 | 258 | # List of Blender classes in this submodule 259 | classes = (VERSE_SCENE_UL_slot, 260 | VERSE_SCENE_MT_menu, 261 | VERSE_SCENE_panel, 262 | VERSE_SCENE_OT_share, 263 | VERSE_SCENE_OT_subscribe, 264 | VERSE_SCENE_OT_unsubscribe 265 | ) 266 | 267 | 268 | def register(): 269 | """ 270 | This method register all methods of this submodule 271 | """ 272 | for c in classes: 273 | bpy.utils.register_class(c) 274 | ui.init_scene_properties() 275 | 276 | 277 | def unregister(): 278 | """ 279 | This method unregister all methods of this submodule 280 | """ 281 | for c in classes: 282 | bpy.utils.unregister_class(c) 283 | ui.reset_scene_properties() 284 | 285 | 286 | if __name__ == '__main__': 287 | register() 288 | -------------------------------------------------------------------------------- /io_verse/ui.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | """ 20 | This module implements ui functions used by other modules. 21 | """ 22 | 23 | import bpy 24 | 25 | 26 | def update_all_views(area_types=None): 27 | """ 28 | This method updates all areas, when no type is specified. 29 | When area_types is specified, then this function tag to 30 | redraw only areas with type specified in area_types. 31 | """ 32 | # Force redraw of all Properties View in all screens 33 | if area_types is None: 34 | for screen in bpy.data.screens: 35 | for area in screen.areas: 36 | # Tag all area to redraw 37 | area.tag_redraw() 38 | else: 39 | for screen in bpy.data.screens: 40 | for area in screen.areas: 41 | if area.type in area_types: 42 | area.tag_redraw() 43 | 44 | 45 | class VERSE_SCENE_NODES_list_item(bpy.types.PropertyGroup): 46 | """ 47 | Group of properties with representation of Verse scene node 48 | """ 49 | node_id = bpy.props.IntProperty( 50 | name="Node ID", 51 | description="ID of scene node", 52 | default=-1) 53 | data_node_id = bpy.props.IntProperty( 54 | name="Data Node ID", 55 | description="ID of node with scene data", 56 | default=-1) 57 | 58 | 59 | def init_scene_properties(): 60 | """ 61 | Init properties in blender scene data type 62 | """ 63 | bpy.types.Scene.verse_scenes = bpy.props.CollectionProperty( 64 | type=VERSE_SCENE_NODES_list_item, 65 | name="Verse Scenes", 66 | description="The list of verse scene nodes shared at Verse server" 67 | ) 68 | bpy.types.Scene.cur_verse_scene_index = bpy.props.IntProperty( 69 | name="Index of current Verse scene", 70 | default=-1, 71 | min=-1, 72 | max=1000, 73 | description="The index of curently selected Verse scene node" 74 | ) 75 | bpy.types.Scene.subscribed = bpy.props.BoolProperty( 76 | name="Subscribed to scene node", 77 | default=False, 78 | description="Is Blender subscribed to data of shared scene" 79 | ) 80 | bpy.types.Scene.verse_node_id = bpy.props.IntProperty( 81 | name="ID of verse scene node", 82 | default=-1, 83 | description="The ID of the verse node representing current Blender scene" 84 | ) 85 | bpy.types.Scene.verse_data_node_id = bpy.props.IntProperty( 86 | name="ID of verse scene data node", 87 | default=-1, 88 | description="The ID of the verse node representing current Blender scene data" 89 | ) 90 | bpy.types.Scene.verse_server_hostname = bpy.props.StringProperty( 91 | name="Verse server hostname", 92 | default="", 93 | description="Hostname of Verse server, where this scene is shared" 94 | ) 95 | bpy.types.Scene.verse_server_service = bpy.props.StringProperty( 96 | name="Verse server port (service)", 97 | default="", 98 | description="Port (service) of Verse server" 99 | ) 100 | 101 | 102 | def reset_scene_properties(): 103 | """ 104 | This method reset all properties. It is used, when Blender is disconnected 105 | from Verse server 106 | """ 107 | scene = bpy.context.scene 108 | scene.verse_scenes.clear() 109 | scene.cur_verse_scene_index = -1 110 | scene.subscribed = False 111 | scene.verse_node_id = -1 112 | scene.verse_data_node_id = -1 113 | scene.verse_server_hostname = "" 114 | scene.verse_server_service = "" 115 | 116 | 117 | class VERSE_AVATAR_NODES_list_item(bpy.types.PropertyGroup): 118 | """ 119 | Group of properties with representation of Verse avatar node 120 | """ 121 | node_id = bpy.props.IntProperty( 122 | name="Node ID", 123 | description="Node ID of avatar node", 124 | default=-1 125 | ) 126 | 127 | 128 | class VERSE_USER_NODES_list_item(bpy.types.PropertyGroup): 129 | """ 130 | Group of properties with representation of Verse avatar node 131 | """ 132 | node_id = bpy.props.IntProperty( 133 | name="Node ID", 134 | description="Node ID of user node", 135 | default=-1 136 | ) 137 | 138 | 139 | def init_avatar_properties(): 140 | """ 141 | Initialize properties used by this module 142 | """ 143 | wm = bpy.types.WindowManager 144 | wm.verse_avatar_capture = bpy.props.BoolProperty( 145 | name="Avatar Capture", 146 | default=False, 147 | description="This is information about my view to 3D scene shared at Verse server" 148 | ) 149 | wm.verse_avatars = bpy.props.CollectionProperty( 150 | type=VERSE_AVATAR_NODES_list_item, 151 | name="Verse Avatars", 152 | description="The list of verse avatar nodes representing Blender at Verse server" 153 | ) 154 | wm.cur_verse_avatar_index = bpy.props.IntProperty( 155 | name="Index of current Verse avatar", 156 | default=-1, 157 | min=-1, 158 | max=1000, 159 | description="The index of currently selected Verse avatar node" 160 | ) 161 | 162 | 163 | def init_user_properties(): 164 | """ 165 | Initialize properties used for users 166 | """ 167 | wm = bpy.types.WindowManager 168 | wm.verse_users = bpy.props.CollectionProperty( 169 | type=VERSE_USER_NODES_list_item, 170 | name="Verse Users", 171 | description="The list of verse user nodes representing valid users at Verse server" 172 | ) 173 | wm.cur_verse_user_index = bpy.props.IntProperty( 174 | name="Index of current Verse user", 175 | default=-1, 176 | min=-1, 177 | max=1000, 178 | description="The index of currently selected Verse user node" 179 | ) 180 | 181 | 182 | def reset_avatar_properties(): 183 | """ 184 | Reset properties used by this module 185 | """ 186 | wm = bpy.context.window_manager 187 | wm.verse_avatar_capture = False 188 | wm.verse_avatars.clear() 189 | wm.cur_verse_avatar_index = -1 190 | 191 | 192 | def reset_user_properties(): 193 | """ 194 | Reset properties used by this module 195 | """ 196 | wm = bpy.context.window_manager 197 | wm.verse_users.clear() 198 | wm.cur_verse_user_index = -1 199 | 200 | 201 | class VERSE_OBJECT_NODES_list_item(bpy.types.PropertyGroup): 202 | """ 203 | Group of properties with representation of Verse scene node 204 | """ 205 | node_id = bpy.props.IntProperty( 206 | name="Node ID", 207 | description="ID of object node", 208 | default=-1 209 | ) 210 | 211 | 212 | def cb_set_obj_node_id(self, value): 213 | """ 214 | Callback function for setting property value 215 | """ 216 | self.verse_node_id_ = value 217 | return None 218 | 219 | 220 | def cb_get_obj_node_id(self): 221 | """ 222 | Callback function for getting property value. 223 | TODO: renaming object will reset verse_node_id and break sharing 224 | """ 225 | if self.name != self.name_: 226 | self.name_ = self.name 227 | self.verse_node_id_ = -1 228 | return self.verse_node_id_ 229 | 230 | 231 | def init_object_properties(): 232 | """ 233 | Init properties related to Blender objects 234 | """ 235 | bpy.types.Scene.verse_objects = bpy.props.CollectionProperty( 236 | type=VERSE_OBJECT_NODES_list_item, 237 | name="Verse Objects", 238 | description="The list of verse object nodes shared at Verse server" 239 | ) 240 | bpy.types.Scene.cur_verse_object_index = bpy.props.IntProperty( 241 | name="Index of current Verse object", 242 | default=-1, 243 | min=-1, 244 | max=1000, 245 | description="The index of currently selected Verse object node" 246 | ) 247 | bpy.types.Object.verse_node_id = bpy.props.IntProperty( 248 | name="ID of verse node", 249 | default=-1, 250 | description="The node ID representing this Object at Verse server" 251 | # TODO: use following callback function set=cb_set_obj_node_id, 252 | # TODO: use following callback function get=cb_get_obj_node_id 253 | ) 254 | bpy.types.Object.verse_node_id_ = bpy.props.IntProperty( 255 | name="Hidden ID of verse node", 256 | default=-1, 257 | description="Hidden ID of verse node", 258 | options={'HIDDEN'} 259 | ) 260 | bpy.types.Object.name_ = bpy.props.StringProperty( 261 | name="SecretName", 262 | default="", 263 | description="Expected name of object storing properties", 264 | options={'HIDDEN'} 265 | ) 266 | bpy.types.Object.subscribed = bpy.props.BoolProperty( 267 | name="Subscribed to data of object node", 268 | default=False, 269 | description="Is Blender subscribed to data of mesh object" 270 | ) 271 | 272 | 273 | def reset_object_properties(): 274 | """ 275 | Reset properties related to Blender objects 276 | """ 277 | scene = bpy.context.scene 278 | scene.verse_objects.clear() 279 | scene.cur_verse_object_index = -1 280 | # Mark all objects as unsubscribed 281 | for obj in bpy.data.objects: 282 | if hasattr(obj, 'subscribed') is True: 283 | obj.subscribed = False 284 | 285 | 286 | # List of Blender classes in this submodule 287 | classes = ( 288 | VERSE_SCENE_NODES_list_item, 289 | VERSE_AVATAR_NODES_list_item, 290 | VERSE_OBJECT_NODES_list_item, 291 | VERSE_USER_NODES_list_item 292 | ) 293 | 294 | 295 | def register(): 296 | """ 297 | This method register all methods of this submodule 298 | """ 299 | for c in classes: 300 | bpy.utils.register_class(c) 301 | 302 | 303 | def unregister(): 304 | """ 305 | This method unregister all methods of this submodule 306 | """ 307 | for c in classes: 308 | bpy.utils.unregister_class(c) -------------------------------------------------------------------------------- /io_verse/scene.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | """ 20 | This module implements sharing Blender scenes at Verse server 21 | """ 22 | 23 | import bpy 24 | import verse as vrs 25 | from .vrsent import vrsent 26 | from . import object3d 27 | from . import mesh 28 | from . import avatar_view 29 | from . import ui 30 | 31 | 32 | VERSE_SCENE_CT = 123 33 | TG_INFO_CT = 0 34 | TAG_SCENE_NAME_CT = 0 35 | 36 | VERSE_SCENE_DATA_CT = 124 37 | 38 | 39 | def cb_scene_update(context): 40 | """ 41 | This function is used as callback function. It is called, 42 | when something is changed in the scene 43 | """ 44 | 45 | wm = bpy.context.window_manager 46 | 47 | if wm.verse_connected is True: 48 | # Some following actions has to be checked regularly (selection) 49 | 50 | # TODO: check if scene was renamed and refactor following code 51 | 52 | edit_obj = bpy.context.edit_object 53 | 54 | # Is shared mesh object in edit mode? 55 | if edit_obj is not None and \ 56 | edit_obj.type == 'MESH' and \ 57 | edit_obj.verse_node_id != -1: 58 | # When shared mesh object is in edit mode, then check if there is 59 | # cached geometry 60 | vrs_obj = object3d.VerseObject.objects[edit_obj.verse_node_id] 61 | if vrs_obj.mesh_node is not None: 62 | vrs_obj.mesh_node.send_updates() 63 | else: 64 | for obj in bpy.data.objects: 65 | # Is object shared at verse server 66 | if obj.verse_node_id != -1: 67 | # Was any object updated? 68 | if obj.is_updated: 69 | object3d.object_update(obj.verse_node_id) 70 | # Check if object can be selected 71 | vrs_obj = object3d.VerseObject.objects[obj.verse_node_id] 72 | if obj.select is True: 73 | # Check if current client has permission to selection 74 | if vrs_obj.can_be_selected is False: 75 | obj.select = False 76 | obj.hide_select = True 77 | # When object is selected and it is not locked, then try to 78 | # lock this object 79 | elif vrs_obj.locked is False: 80 | vrs_obj.lock() 81 | if vrs_obj.mesh_node is not None: 82 | vrs_obj.mesh_node.lock() 83 | # When client has permission to select, then it can not be 84 | # locked by other client 85 | elif vrs_obj.locked_by_me is False: 86 | obj.select = False 87 | obj.hide_select = True 88 | # When object is not selected, but it is still locked, 89 | # then unlock this node 90 | elif vrs_obj.locked_by_me is True: 91 | vrs_obj.unlock() 92 | if vrs_obj.mesh_node is not None: 93 | vrs_obj.mesh_node.unlock() 94 | 95 | 96 | class VerseSceneData(vrsent.VerseNode): 97 | """ 98 | Custom VerseNode subclass storing Blender data 99 | """ 100 | 101 | custom_type = VERSE_SCENE_DATA_CT 102 | 103 | def __init__(self, session, node_id=None, parent=None, user_id=None, 104 | custom_type=VERSE_SCENE_DATA_CT, autosubscribe=False): 105 | """ 106 | Constructor of VerseSceneData 107 | """ 108 | super(VerseSceneData, self).__init__(session, node_id, parent, user_id, custom_type) 109 | self.objects = {} 110 | self.meshes = {} 111 | self._autosubscribe = autosubscribe 112 | 113 | def _auto_subscribe(self): 114 | """ 115 | User has to subscribe to this node manually, when it is node created by 116 | other Blender. 117 | """ 118 | try: 119 | auto_subscribe = self._autosubscribe 120 | except AttributeError: 121 | auto_subscribe = False 122 | return auto_subscribe 123 | 124 | def subscribe(self): 125 | """ 126 | This method is called, when Blender user wants to subscribe to the 127 | scene data shared at Verse server. 128 | """ 129 | # Send subscribe command to Verse server 130 | subscribed = super(VerseSceneData, self).subscribe() 131 | if subscribed is True: 132 | # Save information about subscription to Blender scene too 133 | bpy.context.scene.subscribed = True 134 | # Save ID of scene node in current scene 135 | bpy.context.scene.verse_node_id = self.parent.id 136 | # Save node ID of data node in current scene 137 | bpy.context.scene.verse_data_node_id = self.id 138 | # Save hostname of server in current scene 139 | bpy.context.scene.verse_server_hostname = self.session.hostname 140 | # Save port (service) of server in current scene 141 | bpy.context.scene.verse_server_service = self.session.service 142 | # Store/share id of the verse_scene in the AvatarView 143 | avatar = avatar_view.AvatarView.my_view() 144 | avatar.scene_node_id.value = (self.parent.id,) 145 | # Add Blender callback function that sends scene updates to Verse server 146 | bpy.app.handlers.scene_update_post.append(cb_scene_update) 147 | # Force redraw of 3D view 148 | ui.update_all_views(('VIEW_3D',)) 149 | return subscribed 150 | 151 | def unsubscribe(self): 152 | """ 153 | This method is called, when Blender user wants to unsubscribe 154 | from scene data. 155 | """ 156 | # Send unsubscribe command to Verse server 157 | subscribed = super(VerseSceneData, self).unsubscribe() 158 | if subscribed is False: 159 | # Save information about subscription to Blender scene too 160 | bpy.context.scene.subscribed = False 161 | # Reset id of the verse_scene in the AvatarView 162 | avatar = avatar_view.AvatarView.my_view() 163 | avatar.scene_node_id.value = (0,) 164 | # Remove Blender callback function 165 | bpy.app.handlers.scene_update_post.remove(cb_scene_update) 166 | # TODO: switch all shared data to right state 167 | # (nodes of objects, nodes of meshes, etc.) or destroy them 168 | # Force redraw of 3D view 169 | ui.update_all_views(('VIEW_3D',)) 170 | return subscribed 171 | 172 | def __update_item_slot(self): 173 | """ 174 | This method tries to update properties in slot of scene list 175 | """ 176 | try: 177 | scene_node_id = self.parent.id 178 | except AttributeError: 179 | pass 180 | else: 181 | scene_item = None 182 | for _scene_item in bpy.context.scene.verse_scenes: 183 | if _scene_item.node_id == scene_node_id: 184 | scene_item = _scene_item 185 | break 186 | if scene_item is not None: 187 | # Add ID of this node to the corresponding group of properties 188 | scene_item.data_node_id = self.id 189 | 190 | @classmethod 191 | def cb_receive_node_create(cls, session, node_id, parent_id, user_id, custom_type): 192 | """ 193 | When new node is created or verse server confirms creating of data node 194 | for current scene, than this callback method is called. 195 | """ 196 | # Call parent class 197 | scene_data_node = super(VerseSceneData, cls).cb_receive_node_create( 198 | session=session, 199 | node_id=node_id, 200 | parent_id=parent_id, 201 | user_id=user_id, 202 | custom_type=custom_type) 203 | scene_data_node.__update_item_slot() 204 | return scene_data_node 205 | 206 | @classmethod 207 | def cb_receive_node_link(cls, session, parent_node_id, child_node_id): 208 | """ 209 | When parent node of this type of node is changed at Verse server, then 210 | this callback method is called, when corresponding command is received. 211 | """ 212 | # Call parent class 213 | scene_data_node = super(VerseSceneData, cls).cb_receive_node_link( 214 | session=session, 215 | parent_node_id=parent_node_id, 216 | child_node_id=child_node_id) 217 | scene_data_node.__update_item_slot() 218 | return scene_data_node 219 | 220 | 221 | class VerseSceneName(vrsent.VerseTag): 222 | """ 223 | Custom subclass of VerseTag representing name of scene 224 | """ 225 | 226 | node_custom_type = VERSE_SCENE_CT 227 | tg_custom_type = TG_INFO_CT 228 | custom_type = TAG_SCENE_NAME_CT 229 | 230 | def __init__(self, tg, tag_id=None, data_type=vrs.VALUE_TYPE_STRING8, count=1, 231 | custom_type=TAG_SCENE_NAME_CT, value=None): 232 | """ 233 | Constructor of VerseSceneName 234 | """ 235 | super(VerseSceneName, self).__init__(tg=tg, tag_id=tag_id, data_type=data_type, 236 | count=count, custom_type=custom_type, value=value) 237 | 238 | @classmethod 239 | def cb_receive_tag_set_values(cls, session, node_id, tg_id, tag_id, value): 240 | """ 241 | This method is called, when name of scene is set 242 | """ 243 | tag = super(VerseSceneName, cls).cb_receive_tag_set_values(session, node_id, tg_id, tag_id, value) 244 | # Update name of scene, when name of current scene was changed by other Blender 245 | if node_id == bpy.context.scene.verse_node_id: 246 | try: 247 | verse_scene = session.nodes[node_id] 248 | except KeyError: 249 | pass 250 | else: 251 | bpy.context.scene.name = verse_scene.name 252 | # Update list of scenes shared at Verse server 253 | ui.update_all_views(('PROPERTIES',)) 254 | return tag 255 | 256 | 257 | class VerseScene(vrsent.VerseNode): 258 | """ 259 | Custom subclass of VerseNode representing Blender scene 260 | """ 261 | 262 | custom_type = VERSE_SCENE_CT 263 | 264 | scenes = {} 265 | 266 | def __init__(self, session, node_id=None, parent=None, user_id=None, custom_type=VERSE_SCENE_CT, name=None): 267 | """ 268 | Constructor of VerseScene 269 | """ 270 | # When parent is not specified, then set parent node as parent of scene nodes 271 | if parent is None: 272 | parent = session.nodes[vrs.SCENE_PARENT_NODE_ID] 273 | # Call parent init method 274 | super(VerseScene, self).__init__(session, node_id, parent, user_id, custom_type) 275 | 276 | # Create tag group and tag with name of scene 277 | self.tg_info = vrsent.VerseTagGroup(node=self, custom_type=TG_INFO_CT) 278 | if name is not None: 279 | self.tg_info.tag_name = VerseSceneName(tg=self.tg_info, value=name) 280 | else: 281 | self.tg_info.tag_name = VerseSceneName(tg=self.tg_info) 282 | 283 | if node_id is None: 284 | # Create node with data, when this node was created by this Blender 285 | self.data_node = VerseSceneData(session=session, parent=self, autosubscribe=True) 286 | else: 287 | self.data_node = None 288 | 289 | @classmethod 290 | def cb_receive_node_create(cls, session, node_id, parent_id, user_id, custom_type): 291 | """ 292 | When new node is created or verse server confirms creating of node for current 293 | scene, than this callback method is called. 294 | """ 295 | 296 | # Call parent class 297 | scene_node = super(VerseScene, cls).cb_receive_node_create( 298 | session=session, 299 | node_id=node_id, 300 | parent_id=parent_id, 301 | user_id=user_id, 302 | custom_type=custom_type) 303 | 304 | # Add the scene to the dictionary of scenes 305 | cls.scenes[node_id] = scene_node 306 | 307 | # Add this node to the list of scenes visualized in scene panel 308 | bpy.context.scene.verse_scenes.add() 309 | bpy.context.scene.verse_scenes[-1].node_id = node_id 310 | 311 | return scene_node 312 | 313 | @property 314 | def name(self): 315 | """ 316 | Property of scene name 317 | """ 318 | try: 319 | name = self.tg_info.tag_name.value 320 | except AttributeError: 321 | return "" 322 | else: 323 | try: 324 | return name[0] 325 | except TypeError: 326 | return "" 327 | -------------------------------------------------------------------------------- /io_verse/ui_avatar_view.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | 20 | """ 21 | This file contains classes for visualization of other user connected 22 | to verse server in Blender UI (not 3D view). Visualization of other 23 | avatars is implemented in file avatar_view.py. 24 | """ 25 | 26 | import bpy 27 | from . import session 28 | from . import ui 29 | from . import avatar_view 30 | 31 | 32 | class VerseAvatarStatus(bpy.types.Operator): 33 | """ 34 | Status operator of Verse avatar 35 | """ 36 | bl_idname = "view3d.verse_avatar" 37 | bl_label = "Capture" 38 | bl_description = "Capture camera position" 39 | last_activity = 'NONE' 40 | 41 | def __init__(self): 42 | """ 43 | Constructor of this operator 44 | """ 45 | self.avatar_view = None 46 | 47 | def modal(self, context, event): 48 | """ 49 | This method is executed on events 50 | """ 51 | return {'PASS_THROUGH'} 52 | 53 | @classmethod 54 | def poll(cls, context): 55 | """ 56 | This class method is used, when Blender check, if this operator can be 57 | executed 58 | """ 59 | # Return true only in situation, when client is connected to Verse server 60 | wm = context.window_manager 61 | if wm.verse_connected is True and avatar_view.AvatarView.my_view() is not None: 62 | return True 63 | else: 64 | return False 65 | 66 | def execute(self, context): 67 | """ 68 | This method is used, when this operator is executed 69 | """ 70 | if context.area.type == 'VIEW_3D': 71 | if context.window_manager.verse_avatar_capture is False: 72 | context.window_manager.verse_avatar_capture = True 73 | # Force redraw (display bgl stuff) 74 | ui.update_all_views(('VIEW_3D',)) 75 | return {'RUNNING_MODAL'} 76 | else: 77 | context.window_manager.verse_avatar_capture = False 78 | # Force redraw (not display bgl stuff) 79 | ui.update_all_views(('VIEW_3D',)) 80 | return {'CANCELLED'} 81 | else: 82 | self.report({'WARNING'}, "3D View not found, can't run Camera Capture") 83 | return {'CANCELLED'} 84 | 85 | def cancel(self, context): 86 | """ 87 | This method is called, when operator is cancelled. 88 | """ 89 | if context.window_manager.verse_avatar_capture is True: 90 | context.window_manager.verse_avatar_capture = False 91 | return {'CANCELLED'} 92 | 93 | 94 | class VERSE_AVATAR_OT_show(bpy.types.Operator): 95 | """ 96 | This operator show selected avatar 97 | """ 98 | bl_idname = 'view3d.verse_avatar_show' 99 | bl_label = 'Show Avatar' 100 | 101 | def invoke(self, context, event): 102 | """ 103 | Show avatar selected in list of avatars 104 | """ 105 | wm = context.window_manager 106 | vrs_session = session.VerseSession.instance() 107 | avatar_item = wm.verse_avatars[wm.cur_verse_avatar_index] 108 | verse_avatar = vrs_session.avatars[avatar_item.node_id] 109 | verse_avatar.visualized = True 110 | avatar_view.update_3dview(verse_avatar) 111 | # TODO: subscribe to tag group 112 | return {'FINISHED'} 113 | 114 | @classmethod 115 | def poll(cls, context): 116 | """ 117 | This class method is used, when Blender check, if this operator can be 118 | executed 119 | """ 120 | # Return true only in situation, when client is connected to Verse server 121 | wm = context.window_manager 122 | if wm.verse_connected is True and wm.cur_verse_avatar_index != -1: 123 | avatar_item = wm.verse_avatars[wm.cur_verse_avatar_index] 124 | vrs_session = session.VerseSession.instance() 125 | verse_avatar = vrs_session.avatars[avatar_item.node_id] 126 | if verse_avatar.visualized is False and verse_avatar.id != vrs_session.avatar_id: 127 | return True 128 | else: 129 | return False 130 | else: 131 | return False 132 | 133 | 134 | class VERSE_AVATAR_OT_show_all(bpy.types.Operator): 135 | """ 136 | This operator show all avatars 137 | """ 138 | bl_idname = 'view3d.verse_avatar_show_all' 139 | bl_label = 'Show All Avatars' 140 | 141 | def invoke(self, context, event): 142 | """ 143 | Show all avatars in list of avatars 144 | """ 145 | wm = context.window_manager 146 | vrs_session = session.VerseSession.instance() 147 | for avatar_item in wm.verse_avatars: 148 | verse_avatar = vrs_session.avatars[avatar_item.node_id] 149 | verse_avatar.visualized = True 150 | # TODO: subscribe to unsubscribed tag groups 151 | ui.update_all_views(('VIEW_3D',)) 152 | return {'FINISHED'} 153 | 154 | @classmethod 155 | def poll(cls, context): 156 | """ 157 | This class method is used, when Blender check, if this operator can be 158 | executed 159 | """ 160 | # Return true only in situation, when client is connected to Verse server 161 | wm = context.window_manager 162 | if wm.verse_connected is True: 163 | return True 164 | else: 165 | return False 166 | 167 | 168 | class VERSE_AVATAR_OT_hide(bpy.types.Operator): 169 | """ 170 | This operator hide selected avatar 171 | """ 172 | bl_idname = 'view3d.verse_avatar_hide' 173 | bl_label = 'Hide Avatar' 174 | 175 | def invoke(self, context, event): 176 | """ 177 | Hide avatar selected in list of avatars 178 | """ 179 | wm = context.window_manager 180 | vrs_session = session.VerseSession.instance() 181 | avatar_item = wm.verse_avatars[wm.cur_verse_avatar_index] 182 | verse_avatar = vrs_session.avatars[avatar_item.node_id] 183 | verse_avatar.visualized = False 184 | avatar_view.update_3dview(verse_avatar) 185 | # TODO: unsubscribe from tag group 186 | return {'FINISHED'} 187 | 188 | @classmethod 189 | def poll(cls, context): 190 | """ 191 | This class method is used, when Blender check, if this operator can be 192 | executed 193 | """ 194 | # Return true only in situation, when client is connected to Verse server 195 | wm = context.window_manager 196 | if wm.verse_connected is True and wm.cur_verse_avatar_index != -1: 197 | avatar_item = wm.verse_avatars[wm.cur_verse_avatar_index] 198 | vrs_session = session.VerseSession.instance() 199 | verse_avatar = vrs_session.avatars[avatar_item.node_id] 200 | if verse_avatar.visualized is True and verse_avatar.id != vrs_session.avatar_id: 201 | return True 202 | else: 203 | return False 204 | else: 205 | return False 206 | 207 | 208 | class VERSE_AVATAR_OT_hide_all(bpy.types.Operator): 209 | """ 210 | This operator hide all avatars 211 | """ 212 | bl_idname = 'view3d.verse_avatar_hide_all' 213 | bl_label = 'Hide All Avatars' 214 | 215 | def invoke(self, context, event): 216 | """ 217 | Hide all avatars in list of avatars 218 | """ 219 | wm = context.window_manager 220 | vrs_session = session.VerseSession.instance() 221 | for avatar_item in wm.verse_avatars: 222 | verse_avatar = vrs_session.avatars[avatar_item.node_id] 223 | verse_avatar.visualized = False 224 | # TODO: unsubscribe from all tag groups 225 | ui.update_all_views(('VIEW_3D',)) 226 | return {'FINISHED'} 227 | 228 | @classmethod 229 | def poll(cls, context): 230 | """ 231 | This class method is used, when Blender check, if this operator can be 232 | executed 233 | """ 234 | # Return true only in situation, when client is connected to Verse server 235 | wm = context.window_manager 236 | if wm.verse_connected is True: 237 | return True 238 | else: 239 | return False 240 | 241 | 242 | class VERSE_AVATAR_MT_menu(bpy.types.Menu): 243 | """ 244 | Menu for verse avatar list 245 | """ 246 | bl_idname = 'view3d.verse_avatar_menu' 247 | bl_label = 'Verse Avatar Specials' 248 | 249 | def draw(self, context): 250 | """ 251 | Draw menu 252 | """ 253 | layout = self.layout 254 | layout.operator('view3d.verse_avatar_hide') 255 | layout.operator('view3d.verse_avatar_hide_all') 256 | layout.operator('view3d.verse_avatar_show') 257 | layout.operator('view3d.verse_avatar_show_all') 258 | 259 | @classmethod 260 | def poll(cls, context): 261 | """ 262 | This class method is used, when Blender check, if this operator can be 263 | executed 264 | """ 265 | # Return true only in situation, when client is connected to Verse server 266 | if avatar_view.AvatarView.my_view() is not None: 267 | return True 268 | else: 269 | return False 270 | 271 | 272 | class VERSE_AVATAR_UL_slot(bpy.types.UIList): 273 | """ 274 | A custom slot with information about Verse avatar node 275 | """ 276 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 277 | vrs_session = session.VerseSession.instance() 278 | if vrs_session is not None: 279 | try: 280 | verse_avatar = vrs_session.avatars[item.node_id] 281 | except KeyError: 282 | return 283 | if self.layout_type in {'DEFAULT', 'COMPACT'}: 284 | if verse_avatar.id == vrs_session.avatar_id: 285 | layout.label('Me@' + verse_avatar.hostname, icon='ARMATURE_DATA') 286 | else: 287 | layout.label(str(verse_avatar.username) + '@' + str(verse_avatar.hostname), icon='ARMATURE_DATA') 288 | if context.scene.verse_node_id != -1 and \ 289 | context.scene.subscribed is True and \ 290 | context.scene.verse_node_id == verse_avatar.scene_node_id.value[0]: 291 | if verse_avatar.visualized is True: 292 | layout.operator('view3d.verse_avatar_hide', text='', icon='RESTRICT_VIEW_OFF') 293 | else: 294 | layout.operator('view3d.verse_avatar_show', text='', icon='RESTRICT_VIEW_ON') 295 | elif self.layout_type in {'GRID'}: 296 | layout.alignment = 'CENTER' 297 | layout.label(verse_avatar.name) 298 | 299 | 300 | class VerseAvatarPanel(bpy.types.Panel): 301 | """ 302 | Panel with widgets 303 | """ 304 | bl_idname = "view3d.verse_avatar_panel" 305 | bl_label = "Verse Avatar" 306 | bl_space_type = "VIEW_3D" 307 | bl_region_type = "UI" 308 | 309 | @classmethod 310 | def poll(cls, context): 311 | """ 312 | Can be this panel visible 313 | """ 314 | # Return true only in situation, when client is connected 315 | # to Verse server and it is subscribed to data of some scene 316 | wm = context.window_manager 317 | if wm.verse_connected is True and \ 318 | context.scene.subscribed is not False: 319 | return True 320 | else: 321 | return False 322 | 323 | def draw(self, context): 324 | """ 325 | Define drawing of widgets 326 | """ 327 | wm = context.window_manager 328 | layout = self.layout 329 | 330 | # Display connected avatars in current scene and 331 | # display menu to hide/display them in 3d 332 | row = layout.row() 333 | row.template_list( 334 | 'VERSE_AVATAR_UL_slot', 335 | 'verse_avatars_widget_id', 336 | wm, 337 | 'verse_avatars', 338 | wm, 339 | 'cur_verse_avatar_index', 340 | rows=3 341 | ) 342 | 343 | col = row.column(align=True) 344 | col.menu('view3d.verse_avatar_menu', icon='DOWNARROW_HLT', text="") 345 | 346 | 347 | classes = ( 348 | VERSE_AVATAR_UL_slot, 349 | VerseAvatarPanel, 350 | VerseAvatarStatus, 351 | VERSE_AVATAR_OT_hide, 352 | VERSE_AVATAR_OT_hide_all, 353 | VERSE_AVATAR_OT_show, 354 | VERSE_AVATAR_OT_show_all, 355 | VERSE_AVATAR_MT_menu 356 | ) 357 | 358 | 359 | def register(): 360 | """ 361 | Register classes with panel and initialize properties 362 | """ 363 | for c in classes: 364 | bpy.utils.register_class(c) 365 | ui.init_avatar_properties() 366 | 367 | 368 | def unregister(): 369 | """ 370 | Unregister classes with panel and reset properties 371 | """ 372 | for c in classes: 373 | bpy.utils.unregister_class(c) 374 | ui.reset_avatar_properties() 375 | 376 | 377 | if __name__ == '__main__': 378 | register() 379 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 309 | 310 | 311 | Also add information on how to contact you by electronic and paper mail. 312 | 313 | If the program is interactive, make it output a short notice like this 314 | when it starts in an interactive mode: 315 | 316 | Gnomovision version 69, Copyright (C) year name of author 317 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 318 | This is free software, and you are welcome to redistribute it 319 | under certain conditions; type `show c' for details. 320 | 321 | The hypothetical commands `show w' and `show c' should show the appropriate 322 | parts of the General Public License. Of course, the commands you use may 323 | be called something other than `show w' and `show c'; they could even be 324 | mouse-clicks or menu items--whatever suits your program. 325 | 326 | You should also get your employer (if you work as a programmer) or your 327 | school, if any, to sign a "copyright disclaimer" for the program, if 328 | necessary. Here is a sample; alter the names: 329 | 330 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 331 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 332 | 333 | , 1 April 1989 334 | Ty Coon, President of Vice 335 | 336 | This General Public License does not permit incorporating your program into 337 | proprietary programs. If your program is a subroutine library, you may 338 | consider it more useful to permit linking proprietary applications with the 339 | library. If this is what you want to do, use the GNU Library General 340 | Public License instead of this License. 341 | -------------------------------------------------------------------------------- /io_verse/object3d.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | 20 | """ 21 | This module implements sharing Blender objects at Verse server 22 | """ 23 | 24 | 25 | import bpy 26 | import bgl 27 | import mathutils 28 | import verse as vrs 29 | from .vrsent import vrsent 30 | from . import session as vrs_session 31 | from . import ui 32 | from bpy_extras.view3d_utils import location_3d_to_region_2d 33 | 34 | 35 | VERSE_OBJECT_CT = 125 36 | # Transformation 37 | TG_TRANSFORM_CT = 0 38 | TAG_POSITION_CT = 0 39 | TAG_ROTATION_CT = 1 40 | TAG_SCALE_CT = 2 41 | # Info 42 | TG_INFO_CT = 1 43 | TAG_NAME_CT = 0 44 | LAYER_BB_CT = 0 45 | 46 | 47 | def update_3dview(node): 48 | """ 49 | This method updates all 3D View but not in case, when object is selected/locked 50 | """ 51 | # 3DView should be updated only in situation, when position/rotation/etc 52 | # of other objects is changed 53 | if node.obj.select is False: 54 | ui.update_all_views(('VIEW_3D',)) 55 | 56 | 57 | def object_update(node_id): 58 | """ 59 | This function is called by Blender callback function, when 60 | shared object is changed by user. 61 | """ 62 | # Send changed properties to Verse server 63 | session = vrs_session.VerseSession.instance() 64 | try: 65 | object_node = session.nodes[node_id] 66 | except KeyError: 67 | pass 68 | else: 69 | object_node.update() 70 | 71 | 72 | class VerseObjectPosition(vrsent.VerseTag): 73 | """ 74 | Custom VerseTag subclass representing Blender object position 75 | """ 76 | 77 | node_custom_type = VERSE_OBJECT_CT 78 | tg_custom_type = TG_TRANSFORM_CT 79 | custom_type = TAG_POSITION_CT 80 | 81 | def __init__(self, tg, tag_id=None, data_type=vrs.VALUE_TYPE_REAL32, count=3, 82 | custom_type=TAG_POSITION_CT, value=None): 83 | """ 84 | Constructor of VerseObjectPosition 85 | """ 86 | super(VerseObjectPosition, self).__init__(tg, tag_id, data_type, count, custom_type, value) 87 | 88 | @classmethod 89 | def cb_receive_tag_set_values(cls, session, node_id, tg_id, tag_id, value): 90 | """ 91 | This method is called, when new value of verse tag was set 92 | """ 93 | tag = super(VerseObjectPosition, cls).cb_receive_tag_set_values(session, node_id, tg_id, tag_id, value) 94 | # Update position of Blender object that are not locked by this client 95 | if tag.tg.node.locked_by_me is False: 96 | tag.tg.node.obj.location = mathutils.Vector(value) 97 | # Redraw all 3D views 98 | update_3dview(tag.tg.node) 99 | return tag 100 | 101 | 102 | class VerseObjectRotation(vrsent.VerseTag): 103 | """ 104 | Custom VerseTag subclass representing Blender object rotation 105 | """ 106 | 107 | node_custom_type = VERSE_OBJECT_CT 108 | tg_custom_type = TG_TRANSFORM_CT 109 | custom_type = TAG_ROTATION_CT 110 | 111 | def __init__(self, tg, tag_id=None, data_type=vrs.VALUE_TYPE_REAL32, 112 | count=4, custom_type=TAG_ROTATION_CT, value=None): 113 | """ 114 | Constructor of VerseObjectRotation 115 | """ 116 | super(VerseObjectRotation, self).__init__(tg, tag_id, data_type, count, custom_type, value) 117 | 118 | @classmethod 119 | def cb_receive_tag_set_values(cls, session, node_id, tg_id, tag_id, value): 120 | """ 121 | This method is called, when new value of verse tag was set 122 | """ 123 | tag = super(VerseObjectRotation, cls).cb_receive_tag_set_values(session, node_id, tg_id, tag_id, value) 124 | # Update rotation of Blender object that are not locked by this client 125 | if tag.tg.node.locked_by_me is False: 126 | # It is necessary to have right rotation_mode to set rotation using quaternion 127 | prev_rot_mode = tag.tg.node.obj.rotation_mode 128 | tag.tg.node.obj.rotation_mode = 'QUATERNION' 129 | tag.tg.node.obj.rotation_quaternion = mathutils.Quaternion(value) 130 | tag.tg.node.obj.rotation_mode = prev_rot_mode 131 | update_3dview(tag.tg.node) 132 | return tag 133 | 134 | 135 | class VerseObjectScale(vrsent.VerseTag): 136 | """ 137 | Custom VerseTag subclass representing Blender object scale 138 | """ 139 | 140 | node_custom_type = VERSE_OBJECT_CT 141 | tg_custom_type = TG_TRANSFORM_CT 142 | custom_type = TAG_SCALE_CT 143 | 144 | def __init__(self, tg, tag_id=None, data_type=vrs.VALUE_TYPE_REAL32, 145 | count=3, custom_type=TAG_SCALE_CT, value=None): 146 | """ 147 | Constructor of VerseObjectScale 148 | """ 149 | super(VerseObjectScale, self).__init__(tg, tag_id, data_type, count, custom_type, value) 150 | 151 | @classmethod 152 | def cb_receive_tag_set_values(cls, session, node_id, tg_id, tag_id, value): 153 | """ 154 | This method is called, when new value of verse tag was set 155 | """ 156 | tag = super(VerseObjectScale, cls).cb_receive_tag_set_values(session, node_id, tg_id, tag_id, value) 157 | # Update scale of Blender object that are not locked by this client 158 | if tag.tg.node.locked_by_me is False: 159 | tag.tg.node.obj.scale = mathutils.Vector(value) 160 | update_3dview(tag.tg.node) 161 | return tag 162 | 163 | 164 | class VerseObjectBoundingBox(vrsent.VerseLayer): 165 | """ 166 | Custom VerseLayer subclass representing Blender object bounding box 167 | """ 168 | 169 | node_custom_type = VERSE_OBJECT_CT 170 | custom_type = LAYER_BB_CT 171 | 172 | def __init__(self, node, parent_layer=None, layer_id=None, data_type=vrs.VALUE_TYPE_REAL32, 173 | count=3, custom_type=LAYER_BB_CT): 174 | """ 175 | Constructor of VerseObjectBoundingBox 176 | """ 177 | super(VerseObjectBoundingBox, self).__init__(node, parent_layer, layer_id, data_type, count, custom_type) 178 | 179 | @classmethod 180 | def cb_receive_layer_set_value(cls, session, node_id, layer_id, item_id, value): 181 | """ 182 | This method is called, when new value of verse layer was set 183 | """ 184 | layer = super(VerseObjectBoundingBox, cls).cb_receive_layer_set_value( 185 | session, node_id, layer_id, item_id, value) 186 | update_3dview(layer.node) 187 | return layer 188 | 189 | 190 | class VerseObjectName(vrsent.VerseTag): 191 | """ 192 | Custom VerseTag subclass representing name of Blender object name 193 | """ 194 | 195 | node_custom_type = VERSE_OBJECT_CT 196 | tg_custom_type = TG_INFO_CT 197 | custom_type = TAG_NAME_CT 198 | 199 | def __init__(self, tg, tag_id=None, data_type=vrs.VALUE_TYPE_STRING8, 200 | count=1, custom_type=TAG_NAME_CT, value=None): 201 | """ 202 | Constructor of VerseObjectName 203 | """ 204 | super(VerseObjectName, self).__init__(tg, tag_id, data_type, count, custom_type, value) 205 | 206 | @classmethod 207 | def cb_receive_tag_set_values(cls, session, node_id, tg_id, tag_id, value): 208 | """ 209 | This method is called, when name of object is set 210 | """ 211 | tag = super(VerseObjectName, cls).cb_receive_tag_set_values(session, node_id, tg_id, tag_id, value) 212 | # Update name of object, when name of the object was changed by other Blender 213 | try: 214 | node = session.nodes[node_id] 215 | except KeyError: 216 | pass 217 | else: 218 | obj = node.obj 219 | if obj.name != value[0]: 220 | obj.name = value[0] 221 | # Update list of scenes shared at Verse server 222 | ui.update_all_views(('PROPERTIES',)) 223 | return tag 224 | 225 | 226 | class VerseObject(vrsent.VerseNode): 227 | """ 228 | Custom VerseNode subclass representing Blender object 229 | """ 230 | 231 | node_custom_type = VERSE_OBJECT_CT 232 | tg_custom_type = TG_TRANSFORM_CT 233 | custom_type = VERSE_OBJECT_CT 234 | 235 | objects = {} 236 | 237 | def __init__(self, session, node_id=None, parent=None, user_id=None, custom_type=VERSE_OBJECT_CT, obj=None): 238 | """ 239 | Constructor of VerseObject 240 | """ 241 | super(VerseObject, self).__init__(session, node_id, parent, user_id, custom_type) 242 | self.obj = obj 243 | self.transform = vrsent.VerseTagGroup(node=self, custom_type=TG_TRANSFORM_CT) 244 | self.info = vrsent.VerseTagGroup(node=self, custom_type=TG_INFO_CT) 245 | self.bb = VerseObjectBoundingBox(node=self) 246 | self.mesh_node = None 247 | self.icon_angle = 0.0 248 | if obj is not None: 249 | # Transformation 250 | self.transform.pos = VerseObjectPosition( 251 | tg=self.transform, 252 | value=tuple(obj.location)) 253 | self.transform.rot = VerseObjectRotation( 254 | tg=self.transform, 255 | value=tuple(obj.matrix_local.to_quaternion().normalized())) 256 | self.transform.scale = VerseObjectScale( 257 | tg=self.transform, 258 | value=tuple(obj.scale)) 259 | # Information 260 | self.info.name = VerseObjectName( 261 | tg=self.info, 262 | value=(str(obj.name),)) 263 | # Bounding Box 264 | item_id = 0 265 | for bb_point in obj.bound_box: 266 | self.bb.items[item_id] = (bb_point[0], bb_point[1], bb_point[2]) 267 | item_id += 1 268 | # Scene 269 | self.parent = session.nodes[bpy.context.scene.verse_data_node_id] 270 | else: 271 | self.transform.pos = VerseObjectPosition(tg=self.transform) 272 | self.transform.rot = VerseObjectRotation(tg=self.transform) 273 | self.transform.scale = VerseObjectScale(tg=self.transform) 274 | self.info.name = VerseObjectName(tg=self.info) 275 | 276 | @property 277 | def name(self): 278 | """ 279 | Property of object name 280 | """ 281 | try: 282 | name = self.info.name.value 283 | except AttributeError: 284 | return "" 285 | else: 286 | try: 287 | return name[0] 288 | except TypeError: 289 | return "" 290 | 291 | @property 292 | def can_be_selected(self): 293 | """ 294 | :return: True, when current client can select object 295 | """ 296 | if self.can_write(self.session.user_id) is True: 297 | return True 298 | else: 299 | return False 300 | 301 | @classmethod 302 | def cb_receive_node_create(cls, session, node_id, parent_id, user_id, custom_type): 303 | """ 304 | When new object node is created or verse server, then this callback method is called. 305 | """ 306 | # Call parent class 307 | object_node = super(VerseObject, cls).cb_receive_node_create( 308 | session=session, 309 | node_id=node_id, 310 | parent_id=parent_id, 311 | user_id=user_id, 312 | custom_type=custom_type) 313 | # Create binding between Blender object and node 314 | if object_node.obj is None: 315 | # Create Blender mesh 316 | mesh = bpy.data.meshes.new('Verse') 317 | # Create Blender object 318 | obj = bpy.data.objects.new('Verse', mesh) 319 | # Link Blender object to Blender scene 320 | bpy.context.scene.objects.link(obj) 321 | object_node.obj = obj 322 | object_node.obj.verse_node_id = node_id 323 | cls.objects[node_id] = object_node 324 | bpy.context.scene.verse_objects.add() 325 | bpy.context.scene.verse_objects[-1].node_id = node_id 326 | ui.update_all_views(('VIEW_3D',)) 327 | return object_node 328 | 329 | @classmethod 330 | def cb_receive_node_lock(cls, session, node_id, avatar_id): 331 | """ 332 | When some object is locked by other user, then it should not 333 | be selectable 334 | """ 335 | object_node = super(VerseObject, cls).cb_receive_node_lock(session, node_id, avatar_id) 336 | if object_node.session.avatar_id != avatar_id: 337 | # Only in case, that two clients tried to select one object 338 | # at the same time and this client didn't win 339 | object_node.obj.select = False 340 | if object_node.obj == bpy.context.active_object: 341 | bpy.context.scene.objects.active = None 342 | object_node.obj.hide_select = True 343 | ui.update_all_views(('PROPERTIES', 'VIEW_3D')) 344 | return object_node 345 | 346 | @classmethod 347 | def cb_receive_node_unlock(cls, session, node_id, avatar_id): 348 | """ 349 | When some object was unlocked, then it should be able to select it again 350 | """ 351 | object_node = super(VerseObject, cls).cb_receive_node_unlock(session, node_id, avatar_id) 352 | if object_node.session.avatar_id != avatar_id: 353 | object_node.obj.hide_select = False 354 | ui.update_all_views(('PROPERTIES', 'VIEW_3D')) 355 | return object_node 356 | 357 | @classmethod 358 | def cb_receive_node_owner(cls, session, node_id, user_id): 359 | """ 360 | """ 361 | object_node = super(VerseObject, cls).cb_receive_node_owner(session, node_id, user_id) 362 | if object_node.can_read() is True: 363 | object_node.obj.hide_select = False 364 | else: 365 | object_node.obj.hide_select = True 366 | ui.update_all_views(('PROPERTIES', 'VIEW_3D')) 367 | return object_node 368 | 369 | @classmethod 370 | def cb_receive_node_perm(cls, session, node_id, user_id, perm): 371 | """ 372 | """ 373 | object_node = super(VerseObject, cls).cb_receive_node_perm(session, node_id, user_id, perm) 374 | if object_node.can_write() is True: 375 | object_node.obj.hide_select = False 376 | else: 377 | object_node.obj.hide_select = True 378 | ui.update_all_views(('PROPERTIES', 'VIEW_3D')) 379 | return object_node 380 | 381 | def update(self): 382 | """ 383 | This method tries to send fresh properties of mesh object to Verse server 384 | """ 385 | 386 | # Position 387 | if self.transform.pos.value is not None and \ 388 | self.transform.pos.value != tuple(self.obj.location): 389 | self.transform.pos.value = tuple(self.obj.location) 390 | 391 | # Rotation 392 | if self.transform.rot.value is not None and \ 393 | self.transform.rot.value != tuple(self.obj.matrix_local.to_quaternion().normalized()): 394 | self.transform.rot.value = tuple(self.obj.matrix_local.to_quaternion().normalized()) 395 | 396 | # Scale 397 | if self.transform.scale.value is not None and \ 398 | self.transform.scale.value != tuple(self.obj.scale): 399 | self.transform.scale.value = tuple(self.obj.scale) 400 | 401 | # Bounding box 402 | item_id = 0 403 | for bb_point in self.obj.bound_box: 404 | try: 405 | if self.bb.items[item_id] != (bb_point[0], bb_point[1], bb_point[2]): 406 | self.bb.items[item_id] = (bb_point[0], bb_point[1], bb_point[2]) 407 | except KeyError: 408 | # Bounding box was not received yet 409 | break 410 | item_id += 1 411 | 412 | def draw(self, context): 413 | """ 414 | Draw vector icon on position of shared object 415 | """ 416 | 417 | if self.locked is True: 418 | # When object is locked by current client, then visualize it by green color. 419 | # Otherwise visualize it by red color 420 | if self.locked_by_me is True: 421 | color = (0.0, 1.0, 0.0, 1.0) 422 | else: 423 | color = (1.0, 0.0, 0.0, 1.0) 424 | else: 425 | color = (0.0, 1.0, 1.0, 1.0) 426 | 427 | # Store Line width 428 | line_width_prev = bgl.Buffer(bgl.GL_FLOAT, [1]) 429 | bgl.glGetFloatv(bgl.GL_LINE_WIDTH, line_width_prev) 430 | line_width_prev = line_width_prev[0] 431 | 432 | # Store glColor4f 433 | col_prev = bgl.Buffer(bgl.GL_FLOAT, [4]) 434 | bgl.glGetFloatv(bgl.GL_COLOR, col_prev) 435 | 436 | pos = self.transform.pos.value 437 | if pos is not None: 438 | new_pos = location_3d_to_region_2d( 439 | context.region, 440 | context.space_data.region_3d, 441 | pos) 442 | else: 443 | # When position of object is not set atm, then draw 444 | # icon with stipple line 445 | new_pos = mathutils.Vector((0.0, 0.0, 0.0, 1.0)) 446 | bgl.glEnable(bgl.GL_LINE_STIPPLE) 447 | 448 | verts = ( 449 | (0.20000000298023224, 0.0), 450 | (0.19318519532680511, 0.051763709634542465), 451 | (0.17320513725280762, 0.09999989718198776), 452 | (0.14142143726348877, 0.14142127335071564), 453 | (0.10000012069940567, 0.17320501804351807), 454 | (0.13000015914440155, 0.22516652941703796), 455 | (0.06729313731193542, 0.25114068388938904), 456 | (0.0, 0.2600000202655792), 457 | (-0.0672929584980011, 0.2511407434940338), 458 | (-0.1300000101327896, 0.22516663372516632), 459 | (-0.1000000014901161, 0.17320509254932404), 460 | (-0.1414213627576828, 0.1414213627576828), 461 | (-0.1732050925493240, 0.09999999403953552), 462 | (-0.1931851655244827, 0.05176381394267082), 463 | (-0.2000000029802322, 0.0), 464 | (-0.2600000202655792, 0.0), 465 | (-0.2511407434940338, -0.06729292124509811), 466 | (-0.2251666486263275, -0.12999996542930603), 467 | (-0.1838478147983551, -0.18384772539138794), 468 | (-0.1300000697374344, -0.22516658902168274), 469 | (-0.1000000461935997, -0.17320506274700165), 470 | (-0.0517638735473156, -0.19318515062332153), 471 | (0.0, -0.20000000298023224), 472 | (0.05176372453570366, -0.19318519532680511), 473 | (0.09999991953372955, -0.17320513725280762), 474 | (0.12999990582466125, -0.2251666933298111), 475 | (0.18384768068790436, -0.18384787440299988), 476 | (0.22516657412052155, -0.13000008463859558), 477 | (0.25114068388938904, -0.06729305535554886), 478 | (0.26000002026557920, 0.0) 479 | ) 480 | 481 | bgl.glLineWidth(1) 482 | bgl.glColor4f(color[0], color[1], color[2], color[3]) 483 | 484 | bgl.glPushMatrix() 485 | 486 | bgl.glTranslatef(new_pos[0], new_pos[1], 0) 487 | 488 | # TODO: Rotate this icon, when some other user change something (tranformation, mesh) 489 | # bgl.glRotatef(self.icon_angle, 0, 0, 1) 490 | 491 | # Draw icon 492 | bgl.glBegin(bgl.GL_LINE_LOOP) 493 | for vert in verts: 494 | bgl.glVertex2f(100.0 * vert[0], 100.0 * vert[1]) 495 | bgl.glEnd() 496 | 497 | # When object is locked by someone else or it can not be selected, then draw cross over icon 498 | if self.locked is True and self.locked_by_me is False or \ 499 | self.can_be_selected is False: 500 | bgl.glBegin(bgl.GL_LINES) 501 | bgl.glVertex2f(100.0 * verts[3][0], 100.0 * verts[3][1]) 502 | bgl.glVertex2f(100.0 * verts[18][0], 100.0 * verts[18][1]) 503 | bgl.glVertex2f(100.0 * verts[11][0], 100.0 * verts[11][1]) 504 | bgl.glVertex2f(100.0 * verts[27][0], 100.0 * verts[27][1]) 505 | bgl.glEnd() 506 | 507 | bgl.glPopMatrix() 508 | 509 | bgl.glDisable(bgl.GL_LINE_STIPPLE) 510 | bgl.glLineWidth(line_width_prev) 511 | bgl.glColor4f(col_prev[0], col_prev[1], col_prev[2], col_prev[3]) 512 | 513 | # # Try to draw mesh IDs 514 | # if self.mesh_node is not None: 515 | # self.mesh_node.draw_IDs(context, self.obj) -------------------------------------------------------------------------------- /io_verse/ui_object3d.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | 20 | """ 21 | This module implements sharing Blender objects at Verse server 22 | """ 23 | 24 | 25 | import bpy 26 | import verse as vrs 27 | from . import session 28 | from . import object3d 29 | from . import mesh 30 | from . import ui 31 | 32 | 33 | class VerseObjectOtSubscribe(bpy.types.Operator): 34 | """ 35 | This operator tries to subscribe to Blender Mesh object at Verse server. 36 | """ 37 | bl_idname = 'object.mesh_object_subscribe' 38 | bl_label = "Subscribe" 39 | bl_description = "Subscribe to data of active Mesh Object at Verse server" 40 | 41 | def invoke(self, context, event): 42 | """ 43 | This method will try to create new node representing Mesh Object 44 | at Verse server 45 | """ 46 | # TODO: add something here 47 | return {'FINISHED'} 48 | 49 | @classmethod 50 | def poll(cls, context): 51 | """ 52 | This class method is used, when Blender check, if this operator can be 53 | executed 54 | """ 55 | # Return true only in situation, when client is connected to Verse server 56 | wm = context.window_manager 57 | if wm.verse_connected is True and \ 58 | context.scene.subscribed is not False and \ 59 | context.active_object is not None and \ 60 | context.active_object.verse_node_id != -1: 61 | vrs_session = session.VerseSession.instance() 62 | try: 63 | node = vrs_session.nodes[context.active_object.verse_node_id] 64 | except KeyError: 65 | return False 66 | else: 67 | if node.subscribed is not True: 68 | return True 69 | else: 70 | return False 71 | else: 72 | return False 73 | 74 | 75 | class VerseObjectOtShare(bpy.types.Operator): 76 | """ 77 | This operator tries to share Blender Mesh object at Verse server. 78 | """ 79 | bl_idname = 'object.mesh_object_share' 80 | bl_label = "Share at Verse" 81 | bl_description = "Share active Mesh Object at Verse server" 82 | 83 | def invoke(self, context, event): 84 | """ 85 | This method will try to create new node representing Mesh Object 86 | at Verse server 87 | """ 88 | vrs_session = session.VerseSession.instance() 89 | # Get node with scene data 90 | try: 91 | scene_data_node = vrs_session.nodes[context.scene.verse_data_node_id] 92 | except KeyError: 93 | return {'CANCELLED'} 94 | else: 95 | # Share active mesh object at Verse server 96 | object_node = object3d.VerseObject( 97 | session=vrs_session, 98 | parent=scene_data_node, 99 | obj=context.active_object 100 | ) 101 | object_node.mesh_node = mesh.VerseMesh( 102 | session=vrs_session, 103 | parent=object_node, 104 | mesh=context.active_object.data, 105 | autosubscribe=True 106 | ) 107 | object_node.lock() 108 | # TODO: lock mesh_node too 109 | return {'FINISHED'} 110 | 111 | @classmethod 112 | def poll(cls, context): 113 | """ 114 | This class method is used, when Blender check, if this operator can be 115 | executed 116 | """ 117 | # Return true only in situation, when client is connected to Verse server 118 | wm = context.window_manager 119 | if wm.verse_connected is True and \ 120 | context.scene.subscribed is not False and \ 121 | context.active_object is not None and \ 122 | context.active_object.type == 'MESH' and \ 123 | context.active_object.verse_node_id == -1: 124 | return True 125 | else: 126 | return False 127 | 128 | 129 | class VerseObjectMtMenu(bpy.types.Menu): 130 | """ 131 | Menu for object list 132 | """ 133 | bl_idname = 'object.verse_object_menu' 134 | bl_label = 'Verse Object Specials' 135 | bl_description = 'Menu for list of Verse objects' 136 | 137 | def draw(self, context): 138 | """ 139 | Draw menu 140 | """ 141 | layout = self.layout 142 | layout.operator('object.mesh_object_subscribe') 143 | 144 | @classmethod 145 | def poll(cls, context): 146 | """ 147 | This class method is used, when Blender check, if this operator can be 148 | executed 149 | """ 150 | scene = context.scene 151 | 152 | # Return true only in situation, when client is connected to Verse server 153 | if scene.cur_verse_object_index >= 0 and \ 154 | len(scene.verse_objects) > 0: 155 | return True 156 | else: 157 | return False 158 | 159 | 160 | class VerseObjectUlSlot(bpy.types.UIList): 161 | """ 162 | A custom slot with information about Verse object node 163 | """ 164 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 165 | """ 166 | This method draw one list item representing node 167 | """ 168 | vrs_session = session.VerseSession.instance() 169 | if vrs_session is not None: 170 | try: 171 | verse_object = vrs_session.nodes[item.node_id] 172 | except KeyError: 173 | return 174 | if self.layout_type in {'DEFAULT', 'COMPACT'}: 175 | layout.label(verse_object.name, icon='OBJECT_DATA') 176 | # Owner 177 | if verse_object.user_id == vrs_session.user_id: 178 | layout.label('Me') 179 | else: 180 | layout.label(str(verse_object.owner.name)) 181 | # Read permissions 182 | perm_str = '' 183 | if verse_object.can_read(vrs_session.user_id): 184 | perm_str += 'r' 185 | else: 186 | perm_str += '-' 187 | # Write permissions 188 | if verse_object.can_write(vrs_session.user_id): 189 | perm_str += 'w' 190 | else: 191 | perm_str += '-' 192 | # Locked/unlocked? 193 | if verse_object.locked is True: 194 | layout.label(perm_str, icon='LOCKED') 195 | else: 196 | layout.label(perm_str, icon='UNLOCKED') 197 | # Subscribed? 198 | if verse_object.mesh_node is not None: 199 | layout.label('', icon='FILE_TICK') 200 | elif self.layout_type in {'GRID'}: 201 | layout.alignment = 'CENTER' 202 | layout.label(verse_object.name) 203 | 204 | 205 | class View3DPanelToolsVerseObject(bpy.types.Panel): 206 | """ 207 | Panel with Verse tools for Mesh Object 208 | """ 209 | bl_category = "Relations" 210 | bl_context = 'objectmode' 211 | bl_space_type = 'VIEW_3D' 212 | bl_region_type = 'TOOLS' 213 | bl_label = 'Verse' 214 | 215 | @classmethod 216 | def poll(cls, context): 217 | """ 218 | Can this panel visible 219 | """ 220 | # Return true only in situation, when client is connected to Verse server 221 | wm = context.window_manager 222 | if wm.verse_connected is True and \ 223 | context.scene.subscribed is not False and \ 224 | context.active_object is not None and \ 225 | context.active_object.type == 'MESH': 226 | return True 227 | else: 228 | return False 229 | 230 | def draw(self, context): 231 | """ 232 | Definition of panel layout 233 | """ 234 | layout = self.layout 235 | 236 | col = layout.column(align=True) 237 | col.operator("object.mesh_object_share") 238 | col.operator("object.mesh_object_subscribe") 239 | 240 | 241 | class VerseObjectPanel(bpy.types.Panel): 242 | """ 243 | GUI of Blender objects shared at Verse server 244 | """ 245 | bl_space_type = 'PROPERTIES' 246 | bl_region_type = 'WINDOW' 247 | bl_context = 'scene' 248 | bl_label = 'Verse Objects' 249 | bl_description = 'Panel with Blender objects shared at Verse server' 250 | 251 | @classmethod 252 | def poll(cls, context): 253 | """ 254 | Can be this panel visible 255 | """ 256 | # Return true only in situation, when client is connected 257 | # to Verse server and it is subscribed to data of some scene 258 | wm = context.window_manager 259 | if wm.verse_connected is True and \ 260 | context.scene.subscribed is not False: 261 | return True 262 | else: 263 | return False 264 | 265 | def draw(self, context): 266 | """ 267 | This method draw panel of Verse scenes 268 | """ 269 | scene = context.scene 270 | layout = self.layout 271 | 272 | row = layout.row() 273 | 274 | row.template_list( 275 | 'VerseObjectUlSlot', 276 | 'verse_objects_widget_id', 277 | scene, 278 | 'verse_objects', 279 | scene, 280 | 'cur_verse_object_index', 281 | rows=3 282 | ) 283 | 284 | col = row.column(align=True) 285 | col.menu('object.verse_object_menu', icon='DOWNARROW_HLT', text="") 286 | 287 | 288 | class VerseObjectOtAddWritePerm(bpy.types.Operator): 289 | """ 290 | This operator tries to subscribe to Blender Mesh object at Verse server. 291 | """ 292 | bl_idname = 'object.object_add_write_perm' 293 | bl_label = "Add Write Permission" 294 | bl_description = "Adds write permission to the user" 295 | 296 | def invoke(self, context, event): 297 | """ 298 | This method will try to create new node representing Mesh Object 299 | at Verse server 300 | """ 301 | wm = context.window_manager 302 | vrs_session = session.VerseSession.instance() 303 | user_item = wm.verse_users[wm.cur_verse_user_index] 304 | user_id = user_item.node_id 305 | obj_node = vrs_session.nodes[context.active_object.verse_node_id] 306 | vrs_session.send_node_perm(obj_node.prio, obj_node.id, user_id, vrs.PERM_NODE_WRITE | vrs.PERM_NODE_READ) 307 | mesh_node = obj_node.mesh_node 308 | vrs_session.send_node_perm(mesh_node.prio, mesh_node.id, user_id, vrs.PERM_NODE_WRITE | vrs.PERM_NODE_READ) 309 | return {'FINISHED'} 310 | 311 | @classmethod 312 | def poll(cls, context): 313 | """ 314 | This class method is used, when Blender check, if this operator can be 315 | executed 316 | """ 317 | # Return true only in situation, when client is owner of node representing object 318 | wm = context.window_manager 319 | if wm.verse_connected is True and \ 320 | context.scene.subscribed is not False and \ 321 | context.active_object is not None and \ 322 | context.active_object.verse_node_id != -1: 323 | vrs_session = session.VerseSession.instance() 324 | try: 325 | node = vrs_session.nodes[context.active_object.verse_node_id] 326 | except KeyError: 327 | return False 328 | else: 329 | return node.owned_by_me 330 | else: 331 | return False 332 | 333 | 334 | class VerseObjectOtRemWritePerm(bpy.types.Operator): 335 | """ 336 | This operator tries to subscribe to Blender Mesh object at Verse server. 337 | """ 338 | bl_idname = 'object.object_rem_write_perm' 339 | bl_label = "Remove Write Permission" 340 | bl_description = "Removes write permission to the user" 341 | 342 | def invoke(self, context, event): 343 | """ 344 | This method will try to create new node representing Mesh Object 345 | at Verse server 346 | """ 347 | wm = context.window_manager 348 | vrs_session = session.VerseSession.instance() 349 | user_item = wm.verse_users[wm.cur_verse_user_index] 350 | user_id = user_item.node_id 351 | obj_node = vrs_session.nodes[context.active_object.verse_node_id] 352 | vrs_session.send_node_perm(obj_node.prio, obj_node.id, user_id, vrs.PERM_NODE_READ) 353 | mesh_node = obj_node.mesh_node 354 | vrs_session.send_node_perm(mesh_node.prio, mesh_node.id, user_id, vrs.PERM_NODE_READ) 355 | return {'FINISHED'} 356 | 357 | @classmethod 358 | def poll(cls, context): 359 | """ 360 | This class method is used, when Blender check, if this operator can be 361 | executed 362 | """ 363 | # Return true only in situation, when client is owner of node representing object 364 | wm = context.window_manager 365 | if wm.verse_connected is True and \ 366 | context.scene.subscribed is not False and \ 367 | context.active_object is not None and \ 368 | context.active_object.verse_node_id != -1: 369 | vrs_session = session.VerseSession.instance() 370 | try: 371 | node = vrs_session.nodes[context.active_object.verse_node_id] 372 | except KeyError: 373 | return False 374 | else: 375 | return node.owned_by_me 376 | else: 377 | return False 378 | 379 | 380 | class VerseObjectOtSetOwner(bpy.types.Operator): 381 | """ 382 | This operator tries to subscribe to Blender Mesh object at Verse server. 383 | """ 384 | bl_idname = 'object.set_owner' 385 | bl_label = "Set Owner" 386 | bl_description = "Sets new owner of object" 387 | 388 | def invoke(self, context, event): 389 | """ 390 | This method will try to create new node representing Mesh Object 391 | at Verse server 392 | """ 393 | wm = context.window_manager 394 | vrs_session = session.VerseSession.instance() 395 | user_item = wm.verse_users[wm.cur_verse_user_index] 396 | user_id = user_item.node_id 397 | node = vrs_session.nodes[context.active_object.verse_node_id] 398 | vrs_session.send_node_owner(node.prio, node.id, user_id) 399 | return {'FINISHED'} 400 | 401 | @classmethod 402 | def poll(cls, context): 403 | """ 404 | This class method is used, when Blender check, if this operator can be 405 | executed 406 | """ 407 | # Return true only in situation, when client is owner of node representing object 408 | wm = context.window_manager 409 | if wm.verse_connected is True and \ 410 | context.scene.subscribed is not False and \ 411 | context.active_object is not None and \ 412 | context.active_object.verse_node_id != -1: 413 | vrs_session = session.VerseSession.instance() 414 | try: 415 | node = vrs_session.nodes[context.active_object.verse_node_id] 416 | except KeyError: 417 | return False 418 | else: 419 | return node.owned_by_me 420 | else: 421 | return False 422 | 423 | 424 | class VerseObjectPermMtMenu(bpy.types.Menu): 425 | """ 426 | Menu for object list 427 | """ 428 | bl_idname = 'object.verse_object_perm_menu' 429 | bl_label = 'Verse Object Permission Specials' 430 | bl_description = 'Menu for Verse objects permissions' 431 | 432 | def draw(self, context): 433 | """ 434 | Draw menu 435 | """ 436 | layout = self.layout 437 | layout.operator('object.object_add_write_perm') 438 | layout.operator('object.object_rem_write_perm') 439 | layout.operator('object.set_owner') 440 | 441 | @classmethod 442 | def poll(cls, context): 443 | """ 444 | This class method is used, when Blender check, if this operator can be 445 | executed 446 | """ 447 | wm = context.window_manager 448 | 449 | # Return true only in situation, when client is connected to Verse server 450 | if wm.cur_verse_user_index >= 0 and \ 451 | len(wm.verse_user) > 0: 452 | return True 453 | else: 454 | return False 455 | 456 | 457 | class VerseObjectPermUlSlot(bpy.types.UIList): 458 | """ 459 | A custom slot with information about Verse object node 460 | """ 461 | 462 | # TODO: highlight owner of the node 463 | 464 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 465 | vrs_session = session.VerseSession.instance() 466 | if vrs_session is not None: 467 | try: 468 | verse_user = vrs_session.users[item.node_id] 469 | except KeyError: 470 | return 471 | if self.layout_type in {'DEFAULT', 'COMPACT'}: 472 | if verse_user.id == vrs_session.user_id: 473 | layout.label('Me', icon='ARMATURE_DATA') 474 | else: 475 | layout.label(str(verse_user.name), icon='ARMATURE_DATA') 476 | elif self.layout_type in {'GRID'}: 477 | layout.alignment = 'CENTER' 478 | layout.label(verse_user.name) 479 | 480 | 481 | class VerseObjectPermPanel(bpy.types.Panel): 482 | """ 483 | GUI of Blender objects shared at Verse server 484 | """ 485 | bl_space_type = 'PROPERTIES' 486 | bl_region_type = 'WINDOW' 487 | bl_context = 'object' 488 | bl_label = 'Verse Permissions' 489 | bl_description = 'Panel with Verse access permissions of the object' 490 | 491 | @classmethod 492 | def poll(cls, context): 493 | """ 494 | Can be this panel visible 495 | """ 496 | # Return true only in situation, when client is connected 497 | # to Verse server and it is subscribed to data of some scene 498 | wm = context.window_manager 499 | obj = context.active_object 500 | if wm.verse_connected is True and \ 501 | obj is not None and \ 502 | obj.type == 'MESH' and \ 503 | obj.verse_node_id != -1: 504 | return True 505 | else: 506 | return False 507 | 508 | def draw(self, context): 509 | """ 510 | This method draw panel of Verse scenes 511 | """ 512 | wm = context.window_manager 513 | layout = self.layout 514 | vrs_session = session.VerseSession.instance() 515 | node = vrs_session.nodes[context.active_object.verse_node_id] 516 | 517 | row = layout.row() 518 | row.active = node.owned_by_me 519 | 520 | row.template_list( 521 | 'VerseObjectPermUlSlot', 522 | 'verse_object_perms_widget_id', 523 | wm, 524 | 'verse_users', 525 | wm, 526 | 'cur_verse_user_index', 527 | rows=3 528 | ) 529 | 530 | col = row.column(align=True) 531 | col.menu('object.verse_object_perm_menu', icon='DOWNARROW_HLT', text="") 532 | 533 | 534 | # List of Blender classes in this submodule 535 | classes = ( 536 | VerseObjectOtShare, 537 | VerseObjectOtSubscribe, 538 | View3DPanelToolsVerseObject, 539 | VerseObjectPanel, 540 | VerseObjectUlSlot, 541 | VerseObjectMtMenu, 542 | VerseObjectPermUlSlot, 543 | VerseObjectPermPanel, 544 | VerseObjectOtAddWritePerm, 545 | VerseObjectOtRemWritePerm, 546 | VerseObjectOtSetOwner, 547 | VerseObjectPermMtMenu 548 | ) 549 | 550 | 551 | def register(): 552 | """ 553 | This method register all methods of this submodule 554 | """ 555 | for c in classes: 556 | bpy.utils.register_class(c) 557 | ui.init_object_properties() 558 | ui.init_user_properties() 559 | 560 | 561 | def unregister(): 562 | """ 563 | This method unregister all methods of this submodule 564 | """ 565 | for c in classes: 566 | bpy.utils.unregister_class(c) 567 | ui.reset_object_properties() 568 | ui.reset_user_properties() 569 | 570 | 571 | if __name__ == '__main__': 572 | register() 573 | -------------------------------------------------------------------------------- /io_verse/avatar_view.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | 20 | """ 21 | This file contains methods and classes for visualization of other 22 | users connected to Verse server. It visualize their position and 23 | current view to active 3DView. Other Blender users sharing data at 24 | Verse server can also see, where you are and what you do. 25 | """ 26 | 27 | import bpy 28 | import bgl 29 | import blf 30 | import mathutils 31 | import math 32 | import verse as vrs 33 | from .vrsent import vrsent 34 | from . import ui 35 | from bpy_extras.view3d_utils import location_3d_to_region_2d 36 | 37 | 38 | TG_INFO_CT = 0 39 | TAG_LOCATION_CT = 0 40 | TAG_ROTATION_CT = 1 41 | TAG_DISTANCE_CT = 2 42 | TAG_PERSPECTIVE_CT = 3 43 | TAG_WIDTH_CT = 4 44 | TAG_HEIGHT_CT = 5 45 | TAG_LENS_CT = 6 46 | TAG_SCENE_CT = 7 47 | 48 | 49 | def update_3dview(avatar_view): 50 | """ 51 | This method updates all 3D View but not in case, when the avatar_view is equal to current view, 52 | because it would be useless. 53 | """ 54 | # 3DView should be updated only in situation, when position/rotation/etc 55 | # of other avatar is changed 56 | if avatar_view != AvatarView.my_view(): 57 | ui.update_all_views(('VIEW_3D',)) 58 | 59 | 60 | class BlenderUserNameTag(vrsent.verse_user.UserNameTag): 61 | """ 62 | Custom VerseTag subclass for storing username 63 | """ 64 | 65 | @classmethod 66 | def cb_receive_tag_set_values(cls, session, node_id, tg_id, tag_id, value): 67 | """ 68 | This method is called, when new value of verse tag was set. 69 | It should force to redraw all visible 3D views. 70 | """ 71 | tag = super(BlenderUserNameTag, cls).cb_receive_tag_set_values(session, node_id, tg_id, tag_id, value) 72 | ui.update_all_views(('VIEW_3D',)) 73 | return tag 74 | 75 | 76 | class BlenderHostnameTag(vrsent.verse_avatar.HostnameTag): 77 | """ 78 | Custom VerseTag subclass for storing hostname 79 | """ 80 | 81 | @classmethod 82 | def cb_receive_tag_set_values(cls, session, node_id, tg_id, tag_id, value): 83 | """ 84 | This method is called, when new value of verse tag was set. 85 | It should force to redraw all visible 3D views. 86 | """ 87 | tag = super(BlenderHostnameTag, cls).cb_receive_tag_set_values(session, node_id, tg_id, tag_id, value) 88 | ui.update_all_views(('VIEW_3D',)) 89 | return tag 90 | 91 | 92 | class AvatarLocation(vrsent.VerseTag): 93 | """Class representing location of avatar""" 94 | node_custom_type = vrs.AVATAR_NODE_CT 95 | tg_custom_type = TG_INFO_CT 96 | custom_type = TAG_LOCATION_CT 97 | 98 | def __init__(self, tg, tag_id=None, data_type=vrs.VALUE_TYPE_REAL32, 99 | count=3, custom_type=TAG_LOCATION_CT, value=(0.0, 0.0, 0.0)): 100 | """Constructor of AvatarLocation""" 101 | super(AvatarLocation, self).__init__(tg=tg, tag_id=tag_id, data_type=data_type, 102 | count=count, custom_type=custom_type, value=value) 103 | 104 | @classmethod 105 | def cb_receive_tag_set_values(cls, session, node_id, tg_id, tag_id, value): 106 | """ 107 | This method is called, when new value of verse tag was set 108 | """ 109 | tag = super(AvatarLocation, cls).cb_receive_tag_set_values(session, node_id, tg_id, tag_id, value) 110 | update_3dview(tag.tg.node) 111 | return tag 112 | 113 | 114 | class AvatarRotation(vrsent.VerseTag): 115 | """Class representing rotation of avatar""" 116 | node_custom_type = vrs.AVATAR_NODE_CT 117 | tg_custom_type = TG_INFO_CT 118 | custom_type = TAG_ROTATION_CT 119 | 120 | def __init__(self, tg, tag_id=None, data_type=vrs.VALUE_TYPE_REAL32, 121 | count=4, custom_type=TAG_ROTATION_CT, value=(0.0, 0.0, 0.0, 0.0)): 122 | """Constructor of AvatarRotation""" 123 | super(AvatarRotation, self).__init__(tg=tg, tag_id=tag_id, data_type=data_type, 124 | count=count, custom_type=custom_type, value=value) 125 | 126 | @classmethod 127 | def cb_receive_tag_set_values(cls, session, node_id, tg_id, tag_id, value): 128 | """ 129 | This method is called, when new value of verse tag was set 130 | """ 131 | tag = super(AvatarRotation, cls).cb_receive_tag_set_values(session, node_id, tg_id, tag_id, value) 132 | update_3dview(tag.tg.node) 133 | return tag 134 | 135 | 136 | class AvatarDistance(vrsent.VerseTag): 137 | """Class representing distance of avatar from center of rotation""" 138 | node_custom_type = vrs.AVATAR_NODE_CT 139 | tg_custom_type = TG_INFO_CT 140 | custom_type = TAG_DISTANCE_CT 141 | 142 | def __init__(self, tg, tag_id=None, data_type=vrs.VALUE_TYPE_REAL32, 143 | count=1, custom_type=TAG_DISTANCE_CT, value=(0.0,)): 144 | """Constructor of AvatarDistance""" 145 | super(AvatarDistance, self).__init__(tg=tg, tag_id=tag_id, data_type=data_type, 146 | count=count, custom_type=custom_type, value=value) 147 | 148 | @classmethod 149 | def cb_receive_tag_set_values(cls, session, node_id, tg_id, tag_id, value): 150 | """ 151 | This method is called, when new value of verse tag was set 152 | """ 153 | tag = super(AvatarDistance, cls).cb_receive_tag_set_values(session, node_id, tg_id, tag_id, value) 154 | update_3dview(tag.tg.node) 155 | return tag 156 | 157 | 158 | class AvatarPerspective(vrsent.VerseTag): 159 | """Class representing perspective of avatar""" 160 | node_custom_type = vrs.AVATAR_NODE_CT 161 | tg_custom_type = TG_INFO_CT 162 | custom_type = TAG_PERSPECTIVE_CT 163 | 164 | def __init__(self, tg, tag_id=None, data_type=vrs.VALUE_TYPE_STRING8, 165 | count=1, custom_type=TAG_PERSPECTIVE_CT, value=('PERSP',)): 166 | """Constructor of AvatarPerspective""" 167 | super(AvatarPerspective, self).__init__(tg=tg, tag_id=tag_id, data_type=data_type, 168 | count=count, custom_type=custom_type, value=value) 169 | 170 | @classmethod 171 | def cb_receive_tag_set_values(cls, session, node_id, tg_id, tag_id, value): 172 | """ 173 | This method is called, when new value of verse tag was set 174 | """ 175 | tag = super(AvatarPerspective, cls).cb_receive_tag_set_values(session, node_id, tg_id, tag_id, value) 176 | update_3dview(tag.tg.node) 177 | return tag 178 | 179 | 180 | class AvatarWidth(vrsent.VerseTag): 181 | """Class representing width of avatar view""" 182 | node_custom_type = vrs.AVATAR_NODE_CT 183 | tg_custom_type = TG_INFO_CT 184 | custom_type = TAG_WIDTH_CT 185 | 186 | def __init__(self, tg, tag_id=None, data_type=vrs.VALUE_TYPE_UINT16, 187 | count=1, custom_type=TAG_WIDTH_CT, value=(0,)): 188 | """Constructor of AvatarWidth""" 189 | super(AvatarWidth, self).__init__(tg=tg, tag_id=tag_id, data_type=data_type, 190 | count=count, custom_type=custom_type, value=value) 191 | 192 | @classmethod 193 | def cb_receive_tag_set_values(cls, session, node_id, tg_id, tag_id, value): 194 | """ 195 | This method is called, when new value of verse tag was set 196 | """ 197 | tag = super(AvatarWidth, cls).cb_receive_tag_set_values(session, node_id, tg_id, tag_id, value) 198 | update_3dview(tag.tg.node) 199 | return tag 200 | 201 | 202 | class AvatarHeight(vrsent.VerseTag): 203 | """Class representing height of avatar view""" 204 | node_custom_type = vrs.AVATAR_NODE_CT 205 | tg_custom_type = TG_INFO_CT 206 | custom_type = TAG_HEIGHT_CT 207 | 208 | def __init__(self, tg, tag_id=None, data_type=vrs.VALUE_TYPE_UINT16, 209 | count=1, custom_type=TAG_HEIGHT_CT, value=(0,)): 210 | """Constructor of AvatarHeight""" 211 | super(AvatarHeight, self).__init__(tg=tg, tag_id=tag_id, data_type=data_type, 212 | count=count, custom_type=custom_type, value=value) 213 | 214 | @classmethod 215 | def cb_receive_tag_set_values(cls, session, node_id, tg_id, tag_id, value): 216 | """ 217 | This method is called, when new value of verse tag was set 218 | """ 219 | tag = super(AvatarHeight, cls).cb_receive_tag_set_values(session, node_id, tg_id, tag_id, value) 220 | update_3dview(tag.tg.node) 221 | return tag 222 | 223 | 224 | class AvatarLens(vrsent.VerseTag): 225 | """Class representing lens of avatar view""" 226 | node_custom_type = vrs.AVATAR_NODE_CT 227 | tg_custom_type = TG_INFO_CT 228 | custom_type = TAG_LENS_CT 229 | 230 | def __init__(self, tg, tag_id=None, data_type=vrs.VALUE_TYPE_REAL32, 231 | count=1, custom_type=TAG_LENS_CT, value=(35.0,)): 232 | """Constructor of AvatarLens""" 233 | super(AvatarLens, self).__init__(tg=tg, tag_id=tag_id, data_type=data_type, 234 | count=count, custom_type=custom_type, value=value) 235 | 236 | @classmethod 237 | def cb_receive_tag_set_values(cls, session, node_id, tg_id, tag_id, value): 238 | """ 239 | This method is called, when new value of verse tag was set 240 | """ 241 | tag = super(AvatarLens, cls).cb_receive_tag_set_values(session, node_id, tg_id, tag_id, value) 242 | update_3dview(tag.tg.node) 243 | return tag 244 | 245 | 246 | class AvatarScene(vrsent.VerseTag): 247 | """Class representing scene id of avatar view""" 248 | node_custom_type = vrs.AVATAR_NODE_CT 249 | tg_custom_type = TG_INFO_CT 250 | custom_type = TAG_SCENE_CT 251 | 252 | def __init__(self, tg, tag_id=None, data_type=vrs.VALUE_TYPE_UINT32, 253 | count=1, custom_type=TAG_SCENE_CT, value=(0,)): 254 | """Constructor of AvatarScene""" 255 | super(AvatarScene, self).__init__(tg=tg, tag_id=tag_id, data_type=data_type, 256 | count=count, custom_type=custom_type, value=value) 257 | 258 | @classmethod 259 | def cb_receive_tag_set_values(cls, session, node_id, tg_id, tag_id, value): 260 | """ 261 | This method is called, when new value of verse tag was set 262 | """ 263 | tag = super(AvatarScene, cls).cb_receive_tag_set_values(session, node_id, tg_id, tag_id, value) 264 | update_3dview(tag.tg.node) 265 | return tag 266 | 267 | 268 | class AvatarView(vrsent.VerseAvatar): 269 | """ 270 | Verse node with representation of avatar view to 3D View 271 | """ 272 | 273 | # View of own avatar 274 | __my_view = None 275 | 276 | # Dictionary of other avatar views of other users 277 | __other_views = {} 278 | 279 | # This is specific custom_type of Avatar 280 | custom_type = vrs.AVATAR_NODE_CT 281 | 282 | @classmethod 283 | def my_view(cls): 284 | """ 285 | Getter of class member __my_view 286 | """ 287 | return cls.__my_view 288 | 289 | @classmethod 290 | def other_views(cls): 291 | """ 292 | Getter of class member __other_views 293 | """ 294 | return cls.__other_views 295 | 296 | def __init__(self, *args, **kwargs): 297 | """ 298 | Constructor of AvatarView node 299 | """ 300 | 301 | super(AvatarView, self).__init__(*args, **kwargs) 302 | 303 | wm = bpy.context.window_manager 304 | wm.verse_avatars.add() 305 | wm.verse_avatars[-1].node_id = self.id 306 | 307 | # Force redraw of 3D view 308 | ui.update_all_views(('VIEW_3D',)) 309 | 310 | self.scene_node = None 311 | view_initialized = False 312 | self.visualized = True 313 | self.cur_area = None 314 | self.cur_space = None 315 | 316 | if self.id == self.session.avatar_id: 317 | # Initialize default values 318 | self.cur_screen = bpy.context.screen 319 | self.__class__.__my_view = self 320 | 321 | # Try to find current 3D view 322 | for area in bpy.context.screen.areas.values(): 323 | if area.type == 'VIEW_3D': 324 | self.cur_area = area 325 | for space in area.spaces.values(): 326 | if space.type == 'VIEW_3D': 327 | self.cur_space = space 328 | break 329 | break 330 | 331 | if self.cur_area.type == 'VIEW_3D' and self.cur_space.type == 'VIEW_3D': 332 | view_initialized = True 333 | # Create tag group containing information about view 334 | self.view_tg = vrsent.VerseTagGroup( 335 | node=self, 336 | custom_type=TG_INFO_CT) 337 | # Create tags with data of view to 3D view 338 | # Location 339 | self.location = AvatarLocation( 340 | tg=self.view_tg, 341 | value=tuple(self.cur_space.region_3d.view_location)) 342 | # Rotation 343 | self.rotation = AvatarRotation( 344 | tg=self.view_tg, 345 | value=tuple(self.cur_space.region_3d.view_rotation)) 346 | # Distance 347 | self.distance = AvatarDistance( 348 | tg=self.view_tg, 349 | value=(self.cur_space.region_3d.view_distance,)) 350 | # Perspective/Orthogonal 351 | self.perspective = AvatarPerspective( 352 | tg=self.view_tg, 353 | value=(self.cur_space.region_3d.view_perspective,)) 354 | # Width 355 | self.width = AvatarWidth( 356 | tg=self.view_tg, 357 | value=(self.cur_area.width,)) 358 | # Height 359 | self.height = AvatarHeight( 360 | tg=self.view_tg, 361 | value=(self.cur_area.height,)) 362 | # Lens 363 | self.lens = AvatarLens( 364 | tg=self.view_tg, 365 | value=(self.cur_space.lens,)) 366 | # Get current Scene ID 367 | if bpy.context.scene.verse_node_id != -1: 368 | scene_node_id = bpy.context.scene.verse_node_id 369 | else: 370 | scene_node_id = 0 371 | self.scene_node_id = AvatarScene( 372 | tg=self.view_tg, 373 | value=(scene_node_id,)) 374 | 375 | # TODO: check following code (may be not needed anymore) 376 | original_type = bpy.context.area.type 377 | bpy.context.area.type = 'VIEW_3D' 378 | bpy.ops.view3d.verse_avatar() 379 | bpy.context.area.type = original_type 380 | else: 381 | # TODO: Add some assert, because this should not happen. 382 | pass 383 | else: 384 | self.__class__.__other_views[self.id] = self 385 | 386 | if view_initialized is False: 387 | # Create tag group containing information about view 388 | self.view_tg = vrsent.VerseTagGroup( 389 | node=self, 390 | custom_type=TG_INFO_CT) 391 | # Create tags with data of view to 3D view 392 | self.location = AvatarLocation(tg=self.view_tg) 393 | self.rotation = AvatarRotation(tg=self.view_tg) 394 | self.distance = AvatarDistance(tg=self.view_tg) 395 | self.perspective = AvatarPerspective(tg=self.view_tg) 396 | self.width = AvatarWidth(tg=self.view_tg) 397 | self.height = AvatarHeight(tg=self.view_tg) 398 | self.lens = AvatarLens(tg=self.view_tg) 399 | self.scene_node_id = AvatarScene(tg=self.view_tg) 400 | 401 | @classmethod 402 | def cb_receive_node_destroy(cls, session, node_id): 403 | """ 404 | This method is called, when server destroyed avatar with node_id 405 | """ 406 | # Remove item from collection of properties 407 | wm = bpy.context.window_manager 408 | index = 0 409 | for item in wm.verse_avatars: 410 | if item.node_id == node_id: 411 | wm.verse_avatars.remove(index) 412 | if wm.cur_verse_avatar_index >= index: 413 | wm.cur_verse_avatar_index -= 1 414 | break 415 | index += 1 416 | cls.__other_views.pop(node_id) 417 | # Force redraw of 3D view 418 | ui.update_all_views(('VIEW_3D',)) 419 | return super(AvatarView, cls).cb_receive_node_destroy(session, node_id) 420 | 421 | def update(self, context): 422 | """ 423 | This method tries to update members according context 424 | """ 425 | 426 | self.cur_screen = context.screen 427 | self.cur_area = context.area 428 | 429 | # Location of avatar 430 | if tuple(context.space_data.region_3d.view_location) != self.location.value: 431 | self.location.value = tuple(context.space_data.region_3d.view_location) 432 | 433 | # Rotation around location point 434 | if tuple(context.space_data.region_3d.view_rotation) != self.rotation.value: 435 | self.rotation.value = tuple(context.space_data.region_3d.view_rotation) 436 | 437 | # Distance from location point 438 | if context.space_data.region_3d.view_distance != self.distance.value[0]: 439 | self.distance.value = (context.space_data.region_3d.view_distance,) 440 | 441 | # Perspective/Orthogonal 442 | if context.space_data.region_3d.view_perspective != self.perspective.value[0]: 443 | self.perspective.value = (context.space_data.region_3d.view_perspective,) 444 | 445 | # Lens 446 | if context.space_data.lens != self.lens.value[0]: 447 | self.lens.value = (context.space_data.lens,) 448 | 449 | # Width 450 | if context.area.width != self.width.value[0]: 451 | self.width.value = (context.area.width,) 452 | 453 | # Height 454 | if context.area.height != self.height.value[0]: 455 | self.height.value = (context.area.height,) 456 | 457 | def draw(self, context): 458 | """ 459 | Draw avatar view in given context 460 | """ 461 | # TODO: Add this color to Add-on option 462 | color = (1.0, 1.0, 0.5, 1.0) 463 | alpha = 2.0 * math.atan((18.0 / 2.0) / self.lens.value[0]) 464 | dist = 0.5 / (math.tan(alpha / 2.0)) 465 | if self.height.value[0] == 0: 466 | width = 0.7 467 | else: 468 | width = self.width.value[0] / self.height.value[0] 469 | 470 | points = dict() 471 | points['border'] = [None, None, None, None] 472 | points['center'] = [None] 473 | 474 | # Points of face 475 | points['right_eye'] = [ 476 | mathutils.Vector((0.25, 0.25, self.distance.value[0] - dist)), 477 | mathutils.Vector((0.3, 0.25, self.distance.value[0] - dist)), 478 | mathutils.Vector((0.3, 0.0, self.distance.value[0] - dist)), 479 | mathutils.Vector((0.25, 0.0, self.distance.value[0] - dist)), 480 | mathutils.Vector((0.25, 0.25, self.distance.value[0] - dist)) 481 | ] 482 | points['left_eye'] = [ 483 | mathutils.Vector((-0.25, 0.25, self.distance.value[0] - dist)), 484 | mathutils.Vector((-0.3, 0.25, self.distance.value[0] - dist)), 485 | mathutils.Vector((-0.3, 0.0, self.distance.value[0] - dist)), 486 | mathutils.Vector((-0.25, 0.0, self.distance.value[0] - dist)), 487 | mathutils.Vector((-0.25, 0.25, self.distance.value[0] - dist)) 488 | ] 489 | 490 | points['mouth'] = [ 491 | mathutils.Vector((-0.40912365913391113, -0.11777058243751526, self.distance.value[0] - dist)), 492 | mathutils.Vector((-0.3441678285598755, -0.15873458981513977, self.distance.value[0] - dist)), 493 | mathutils.Vector((-0.2563667893409729, -0.1998385488986969, self.distance.value[0] - dist)), 494 | mathutils.Vector((-0.18191590905189514, -0.22385218739509583, self.distance.value[0] - dist)), 495 | mathutils.Vector((-0.10375960171222687, -0.23957833647727966, self.distance.value[0] - dist)), 496 | mathutils.Vector((0.0, -0.2464955747127533, self.distance.value[0] - dist)), 497 | mathutils.Vector((0.10375960171222687, -0.23957833647727966, self.distance.value[0] - dist)), 498 | mathutils.Vector((0.18191590905189514, -0.22385218739509583, self.distance.value[0] - dist)), 499 | mathutils.Vector((0.2563667893409729, -0.1998385488986969, self.distance.value[0] - dist)), 500 | mathutils.Vector((0.3441678285598755, -0.15873458981513977, self.distance.value[0] - dist)), 501 | mathutils.Vector((0.40912365913391113, -0.11777058243751526, self.distance.value[0] - dist)) 502 | ] 503 | 504 | # Put border points of camera to basic position 505 | points['border'][0] = mathutils.Vector(( 506 | -width / 2.0, 507 | -0.5, 508 | self.distance.value[0] - dist, 509 | 1.0 510 | )) 511 | points['border'][1] = mathutils.Vector(( 512 | width / 2.0, 513 | -0.5, 514 | self.distance.value[0] - dist, 515 | 1.0 516 | )) 517 | points['border'][2] = mathutils.Vector(( 518 | width / 2.0, 519 | 0.5, 520 | self.distance.value[0] - dist, 521 | 1.0 522 | )) 523 | points['border'][3] = mathutils.Vector(( 524 | -width / 2.0, 525 | 0.5, 526 | self.distance.value[0] - dist, 527 | 1.0 528 | )) 529 | 530 | # Center of view 531 | points['center'][0] = mathutils.Vector(( 532 | 0.0, 533 | 0.0, 534 | self.distance.value[0], 535 | 1.0 536 | )) 537 | 538 | # Create transformation (rotation) matrix 539 | rot_matrix = mathutils.Quaternion(self.rotation.value).to_matrix().to_4x4() 540 | 541 | # Transform points in all point groups 542 | for point_group in points.values(): 543 | for index in range(len(point_group)): 544 | # Rotate points 545 | point_group[index] = (rot_matrix * point_group[index]).to_3d() 546 | # Move points 547 | point_group[index] += mathutils.Vector(self.location.value) 548 | 549 | border = points['border'] 550 | center = points['center'] 551 | 552 | # Store glColor4f 553 | col_prev = bgl.Buffer(bgl.GL_FLOAT, [4]) 554 | bgl.glGetFloatv(bgl.GL_COLOR, col_prev) 555 | 556 | bgl.glColor4f(color[0], color[1], color[2], color[3]) 557 | 558 | # Draw username 559 | coord_2d = location_3d_to_region_2d( 560 | context.region, 561 | context.space_data.region_3d, 562 | center[0]) 563 | 564 | # When coordinates are not outside window, then draw the name of avatar 565 | if coord_2d is not None: 566 | # TODO: add to Add-on options 567 | font_id, font_size, my_dpi = 0, 12, 72 568 | blf.size(font_id, font_size, my_dpi) 569 | blf.position(font_id, coord_2d[0] + 2, coord_2d[1] + 2, 0) 570 | blf.draw(font_id, str(self.username)) 571 | 572 | # Get & convert the Perspective Matrix of the current view/region. 573 | persp_matrix = context.space_data.region_3d.perspective_matrix 574 | temp_mat = [persp_matrix[j][i] for i in range(4) for j in range(4)] 575 | persp_buff = bgl.Buffer(bgl.GL_FLOAT, 16, temp_mat) 576 | 577 | # Store previous OpenGL settings. 578 | # Store MatrixMode 579 | matrix_mode_prev = bgl.Buffer(bgl.GL_INT, [1]) 580 | bgl.glGetIntegerv(bgl.GL_MATRIX_MODE, matrix_mode_prev) 581 | matrix_mode_prev = matrix_mode_prev[0] 582 | 583 | # Store projection matrix 584 | proj_matrix_prev = bgl.Buffer(bgl.GL_DOUBLE, [16]) 585 | bgl.glGetFloatv(bgl.GL_PROJECTION_MATRIX, proj_matrix_prev) 586 | 587 | # Store Line width 588 | line_width_prev = bgl.Buffer(bgl.GL_FLOAT, [1]) 589 | bgl.glGetFloatv(bgl.GL_LINE_WIDTH, line_width_prev) 590 | line_width_prev = line_width_prev[0] 591 | 592 | # Store GL_BLEND 593 | blend_prev = bgl.Buffer(bgl.GL_BYTE, [1]) 594 | bgl.glGetFloatv(bgl.GL_BLEND, blend_prev) 595 | blend_prev = blend_prev[0] 596 | 597 | # Store GL_DEPTH_TEST 598 | depth_test_prev = bgl.Buffer(bgl.GL_BYTE, [1]) 599 | bgl.glGetFloatv(bgl.GL_DEPTH_TEST, depth_test_prev) 600 | depth_test_prev = depth_test_prev[0] 601 | 602 | # Store GL_LINE_STIPPLE 603 | line_stipple_prev = bgl.Buffer(bgl.GL_BYTE, [1]) 604 | bgl.glGetFloatv(bgl.GL_LINE_STIPPLE, line_stipple_prev) 605 | line_stipple_prev = line_stipple_prev[0] 606 | 607 | # Prepare for 3D drawing 608 | bgl.glLoadIdentity() 609 | bgl.glMatrixMode(bgl.GL_PROJECTION) 610 | bgl.glLoadMatrixf(persp_buff) 611 | bgl.glEnable(bgl.GL_BLEND) 612 | bgl.glEnable(bgl.GL_DEPTH_TEST) 613 | 614 | # Draw "Look At" point 615 | bgl.glLineWidth(1) 616 | bgl.glBegin(bgl.GL_LINES) 617 | bgl.glColor4f(color[0], color[1], color[2], color[3]) 618 | 619 | bgl.glVertex3f( 620 | self.location.value[0] + 0.1, 621 | self.location.value[1], 622 | self.location.value[2] 623 | ) 624 | bgl.glVertex3f( 625 | self.location.value[0] - 0.1, 626 | self.location.value[1], 627 | self.location.value[2] 628 | ) 629 | 630 | bgl.glVertex3f( 631 | self.location.value[0], 632 | self.location.value[1] + 0.1, 633 | self.location.value[2] 634 | ) 635 | bgl.glVertex3f( 636 | self.location.value[0], 637 | self.location.value[1] - 0.1, 638 | self.location.value[2] 639 | ) 640 | 641 | bgl.glVertex3f( 642 | self.location.value[0], 643 | self.location.value[1], 644 | self.location.value[2] + 0.1 645 | ) 646 | bgl.glVertex3f( 647 | self.location.value[0], 648 | self.location.value[1], 649 | self.location.value[2] - 0.1 650 | ) 651 | 652 | bgl.glEnd() 653 | 654 | # Draw border of camera 655 | bgl.glBegin(bgl.GL_LINE_STRIP) 656 | bgl.glVertex3f(border[0][0], border[0][1], border[0][2]) 657 | bgl.glVertex3f(border[1][0], border[1][1], border[1][2]) 658 | bgl.glVertex3f(border[2][0], border[2][1], border[2][2]) 659 | bgl.glVertex3f(border[3][0], border[3][1], border[3][2]) 660 | bgl.glVertex3f(border[0][0], border[0][1], border[0][2]) 661 | bgl.glEnd() 662 | 663 | # Draw left eye 664 | bgl.glBegin(bgl.GL_LINE_STRIP) 665 | for point in points['left_eye']: 666 | bgl.glVertex3f(point[0], point[1], point[2]) 667 | bgl.glEnd() 668 | 669 | # Draw right eye 670 | bgl.glBegin(bgl.GL_LINE_STRIP) 671 | for point in points['right_eye']: 672 | bgl.glVertex3f(point[0], point[1], point[2]) 673 | bgl.glEnd() 674 | 675 | # Draw mouth 676 | bgl.glBegin(bgl.GL_LINE_STRIP) 677 | for point in points['mouth']: 678 | bgl.glVertex3f(point[0], point[1], point[2]) 679 | bgl.glEnd() 680 | 681 | # Draw dashed lines from center of "camera" to border of camera 682 | bgl.glEnable(bgl.GL_LINE_STIPPLE) 683 | bgl.glBegin(bgl.GL_LINES) 684 | bgl.glVertex3f(border[0][0], border[0][1], border[0][2]) 685 | bgl.glVertex3f(center[0][0], center[0][1], center[0][2]) 686 | bgl.glVertex3f(border[1][0], border[1][1], border[1][2]) 687 | bgl.glVertex3f(center[0][0], center[0][1], center[0][2]) 688 | bgl.glVertex3f(border[2][0], border[2][1], border[2][2]) 689 | bgl.glVertex3f(center[0][0], center[0][1], center[0][2]) 690 | bgl.glVertex3f(border[3][0], border[3][1], border[3][2]) 691 | bgl.glVertex3f(center[0][0], center[0][1], center[0][2]) 692 | bgl.glEnd() 693 | 694 | # Draw dashed line from Look At point and center of camera 695 | bgl.glBegin(bgl.GL_LINES) 696 | bgl.glVertex3f( 697 | self.location.value[0], 698 | self.location.value[1], 699 | self.location.value[2] 700 | ) 701 | bgl.glVertex3f(center[0][0], center[0][1], center[0][2]) 702 | bgl.glEnd() 703 | bgl.glDisable(bgl.GL_LINE_STIPPLE) 704 | 705 | # Restore previous OpenGL settings 706 | bgl.glLoadIdentity() 707 | bgl.glMatrixMode(matrix_mode_prev) 708 | bgl.glLoadMatrixf(proj_matrix_prev) 709 | bgl.glLineWidth(line_width_prev) 710 | if not blend_prev: 711 | bgl.glDisable(bgl.GL_BLEND) 712 | if not line_stipple_prev: 713 | bgl.glDisable(bgl.GL_LINE_STIPPLE) 714 | if not depth_test_prev: 715 | bgl.glDisable(bgl.GL_DEPTH_TEST) 716 | 717 | bgl.glColor4f(col_prev[0], col_prev[1], col_prev[2], col_prev[3]) 718 | -------------------------------------------------------------------------------- /io_verse/mesh.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | 20 | """ 21 | This module implements sharing Blender meshes at Verse server 22 | """ 23 | 24 | 25 | import bpy 26 | import blf 27 | import bgl 28 | import mathutils 29 | import bmesh 30 | from bpy_extras.view3d_utils import location_3d_to_region_2d 31 | import verse as vrs 32 | from .vrsent import vrsent 33 | from . import object3d 34 | 35 | 36 | VERSE_MESH_CT = 126 37 | LAYER_VERTEXES_CT = 0 38 | LAYER_EDGES_CT = 1 39 | LAYER_QUADS_CT = 2 40 | 41 | 42 | class VerseVertices(vrsent.VerseLayer): 43 | """ 44 | Custom VerseLayer subclass representing position of vertexes 45 | """ 46 | 47 | node_custom_type = VERSE_MESH_CT 48 | custom_type = LAYER_VERTEXES_CT 49 | 50 | def __init__(self, node, parent_layer=None, layer_id=None, data_type=vrs.VALUE_TYPE_REAL64, 51 | count=3, custom_type=LAYER_VERTEXES_CT): 52 | """ 53 | Constructor of VerseVertices 54 | """ 55 | super(VerseVertices, self).__init__(node, parent_layer, layer_id, data_type, count, custom_type) 56 | self.id_cache = {} 57 | 58 | def b3d_vertex(self, item_id): 59 | """ 60 | This method tries to find Blender vertex in bmesh and cache 61 | """ 62 | 63 | _bmesh = self.node.bmesh 64 | try: 65 | # Try to find blender vertex at cache first 66 | b3d_vert = self.id_cache[item_id] 67 | except KeyError: 68 | try: 69 | # Then try to find it in bmesh at the index==item_id 70 | b3d_vert = _bmesh.verts[item_id] 71 | except IndexError: 72 | # When vertex was not found in cache nor bmesh, then try to 73 | # find it using loop over all vertices 74 | id_layer = _bmesh.verts.layers.int.get('VertIDs') 75 | for b3d_vert in _bmesh.verts: 76 | verse_id = b3d_vert[id_layer] 77 | if verse_id != -1: 78 | self.id_cache[item_id] = b3d_vert 79 | if verse_id == item_id: 80 | return b3d_vert 81 | return None 82 | else: 83 | # Update cache 84 | self.id_cache[item_id] = b3d_vert 85 | return b3d_vert 86 | else: 87 | return b3d_vert 88 | 89 | def get_bmesh(self): 90 | """ 91 | This method tries to update reference on bmesh 92 | """ 93 | 94 | if self.node.bmesh is None: 95 | self.node.bmesh = bmesh.new() 96 | self.node.bmesh.from_mesh(self.node.mesh) 97 | self.node.bm_from_edit_mesh = False 98 | else: 99 | try: 100 | self.node.bmesh.verts 101 | except ReferenceError: 102 | self.node.bmesh = bmesh.new() 103 | self.node.bmesh.from_mesh(self.node.mesh) 104 | self.node.clear_ID_cache() 105 | return self.node.bmesh 106 | 107 | @classmethod 108 | def cb_receive_layer_set_value(cls, session, node_id, layer_id, item_id, value): 109 | """ 110 | This method is called, when new value of verse layer was set 111 | """ 112 | vert_layer = super(VerseVertices, cls).cb_receive_layer_set_value(session, node_id, layer_id, item_id, value) 113 | 114 | # Update mesh only in situation, when it was changed by someone else 115 | if vert_layer.node.locked_by_me is False: 116 | 117 | _bmesh = vert_layer.get_bmesh() 118 | 119 | b3d_vert = vert_layer.b3d_vertex(item_id) 120 | 121 | # Try to update last vertex ID 122 | if vert_layer.node.last_vert_ID is None or \ 123 | vert_layer.node.last_vert_ID < item_id: 124 | vert_layer.node.last_vert_ID = item_id 125 | 126 | if b3d_vert is not None: 127 | # Update position 128 | b3d_vert.co = mathutils.Vector(value) 129 | else: 130 | # When vertex was not found, then it is new vertex. Create it. 131 | b3d_vert = _bmesh.verts.new(value) 132 | vert_layer.id_cache[item_id] = b3d_vert 133 | id_layer = _bmesh.verts.layers.int.get('VertIDs') 134 | b3d_vert[id_layer] = item_id 135 | 136 | # Update Blender mesh 137 | _bmesh.to_mesh(vert_layer.node.mesh) 138 | vert_layer.node.mesh.update() 139 | 140 | return vert_layer 141 | 142 | @classmethod 143 | def cb_receive_layer_unset_value(cls, session, node_id, layer_id, item_id): 144 | """ 145 | This method is called, when some vertex was deleted 146 | """ 147 | vert_layer = super(VerseVertices, cls).cb_receive_layer_unset_value(session, node_id, layer_id, item_id) 148 | 149 | # Update mesh only in situation, when it was changed by someone else 150 | if vert_layer.node.locked_by_me is False: 151 | 152 | _bmesh = vert_layer.get_bmesh() 153 | 154 | b3d_vert = vert_layer.b3d_vertex(item_id) 155 | 156 | # Try to delete vertex 157 | if b3d_vert is not None: 158 | bmesh.ops.delete(_bmesh, geom=[b3d_vert], context=1) 159 | vert_layer.id_cache.pop(item_id) 160 | 161 | # Update Blender mesh 162 | _bmesh.to_mesh(vert_layer.node.mesh) 163 | vert_layer.node.mesh.update() 164 | 165 | return vert_layer 166 | 167 | 168 | class VerseEdges(vrsent.VerseLayer): 169 | """ 170 | Custom VerseLayer subclass representing edges (indexes to vertexes) 171 | """ 172 | 173 | node_custom_type = VERSE_MESH_CT 174 | custom_type = LAYER_EDGES_CT 175 | 176 | def __init__(self, node, parent_layer=None, layer_id=None, data_type=vrs.VALUE_TYPE_UINT32, 177 | count=2, custom_type=LAYER_EDGES_CT): 178 | """ 179 | Constructor of VerseEdges 180 | """ 181 | super(VerseEdges, self).__init__(node, parent_layer, layer_id, data_type, count, custom_type) 182 | self.id_cache = {} 183 | 184 | def b3d_edge(self, item_id): 185 | """ 186 | This method tries to find Blender edge in bmesh and cache 187 | """ 188 | _bmesh = self.node.bmesh 189 | try: 190 | # Try to find blender vertex at cache first 191 | b3d_edge = self.id_cache[item_id] 192 | except KeyError: 193 | try: 194 | # Then try to find it in bmesh at the index==item_id 195 | b3d_edge = _bmesh.edges[item_id] 196 | except IndexError: 197 | # When edge was not found in cache nor bmesh, then try to 198 | # find it using loop over all edges 199 | id_layer = _bmesh.edges.layers.int.get('EdgeIDs') 200 | for b3d_edge in _bmesh.edges: 201 | verse_id = b3d_edge[id_layer] 202 | if verse_id != -1: 203 | self.id_cache[item_id] = b3d_edge 204 | if verse_id == item_id: 205 | return b3d_edge 206 | return None 207 | else: 208 | # Update cache 209 | self.id_cache[item_id] = b3d_edge 210 | return b3d_edge 211 | else: 212 | return b3d_edge 213 | 214 | @classmethod 215 | def cb_receive_layer_set_value(cls, session, node_id, layer_id, item_id, value): 216 | """ 217 | This method is called, when new value of verse layer was set 218 | """ 219 | edge_layer = super(VerseEdges, cls).cb_receive_layer_set_value(session, node_id, layer_id, item_id, value) 220 | 221 | # Update mesh only in situation, when it was changed by someone else 222 | if edge_layer.node.locked_by_me is False: 223 | 224 | vert_layer = edge_layer.node.vertices 225 | face_layer = edge_layer.node.quads 226 | 227 | if edge_layer.node.bmesh is None: 228 | edge_layer.node.bmesh = bmesh.new() 229 | edge_layer.node.bmesh.from_mesh(edge_layer.node.mesh) 230 | edge_layer.node.bm_from_edit_mesh = False 231 | else: 232 | try: 233 | edge_layer.node.bmesh.edges 234 | except ReferenceError: 235 | edge_layer.node.bmesh = bmesh.new() 236 | edge_layer.node.bmesh.from_mesh(edge_layer.node.mesh) 237 | vert_layer.id_cache = {} 238 | edge_layer.id_cache = {} 239 | face_layer.id_cache = {} 240 | 241 | _bmesh = edge_layer.node.bmesh 242 | 243 | b3d_edge = edge_layer.b3d_edge(item_id) 244 | 245 | # Try to update last vertex ID 246 | if edge_layer.node.last_edge_ID is None or \ 247 | edge_layer.node.last_edge_ID < item_id: 248 | edge_layer.node.last_edge_ID = item_id 249 | 250 | # Does edge with same id exist? 251 | if b3d_edge is not None: 252 | # Delete edge 253 | try: 254 | _bmesh.edges.remove(b3d_edge) 255 | except ReferenceError: 256 | # Edge was already removed 257 | pass 258 | 259 | # Create new edge 260 | b3d_edge = _bmesh.edges.new([vert_layer.b3d_vertex(vert_id) for vert_id in value]) 261 | edge_layer.id_cache[item_id] = b3d_edge 262 | id_layer = _bmesh.edges.layers.int.get('EdgeIDs') 263 | b3d_edge[id_layer] = item_id 264 | 265 | # Update Blender mesh 266 | _bmesh.to_mesh(edge_layer.node.mesh) 267 | edge_layer.node.mesh.update() 268 | 269 | return edge_layer 270 | 271 | @classmethod 272 | def cb_receive_layer_unset_value(cls, session, node_id, layer_id, item_id): 273 | """ 274 | This method is called, when some vertex was deleted 275 | """ 276 | edge_layer = super(VerseEdges, cls).cb_receive_layer_unset_value(session, node_id, layer_id, item_id) 277 | 278 | # Update mesh only in situation, when it was changed by someone else 279 | if edge_layer.node.locked_by_me is False: 280 | 281 | vert_layer = edge_layer.node.vertices 282 | face_layer = edge_layer.node.quads 283 | 284 | if edge_layer.node.bmesh is None: 285 | edge_layer.node.bmesh = bmesh.new() 286 | edge_layer.node.bmesh.from_mesh(edge_layer.node.mesh) 287 | edge_layer.node.bm_from_edit_mesh = False 288 | else: 289 | try: 290 | edge_layer.node.bmesh.edges 291 | except ReferenceError: 292 | edge_layer.node.bmesh = bmesh.new() 293 | edge_layer.node.bmesh.from_mesh(edge_layer.node.mesh) 294 | vert_layer.id_cache = {} 295 | edge_layer.id_cache = {} 296 | face_layer.id_cache = {} 297 | 298 | _bmesh = edge_layer.node.bmesh 299 | 300 | b3d_edge = edge_layer.b3d_edge(item_id) 301 | 302 | # Try to update last vertex ID 303 | if edge_layer.node.last_vert_ID is None or \ 304 | edge_layer.node.last_edge_ID < item_id: 305 | edge_layer.node.last_edge_ID = item_id 306 | 307 | if b3d_edge is not None: 308 | # Delete edge 309 | try: 310 | _bmesh.edges.remove(b3d_edge) 311 | except ReferenceError: 312 | # Edge was already removed? 313 | edge_layer.id_cache.pop(item_id) 314 | else: 315 | # Update Blender mesh 316 | _bmesh.to_mesh(edge_layer.node.mesh) 317 | edge_layer.node.mesh.update() 318 | edge_layer.id_cache.pop(item_id) 319 | 320 | return edge_layer 321 | 322 | 323 | class VerseFaces(vrsent.VerseLayer): 324 | """ 325 | Custom VerseLayer subclass representing tessellated faces (indexes to vertexes). 326 | Tessellated mesh contains only triangles and quads. 327 | """ 328 | 329 | node_custom_type = VERSE_MESH_CT 330 | custom_type = LAYER_QUADS_CT 331 | 332 | def __init__(self, node, parent_layer=None, layer_id=None, data_type=vrs.VALUE_TYPE_UINT32, 333 | count=4, custom_type=LAYER_QUADS_CT): 334 | """ 335 | Constructor of VerseFaces 336 | """ 337 | super(VerseFaces, self).__init__(node, parent_layer, layer_id, data_type, count, custom_type) 338 | self.id_cache = {} 339 | 340 | def find_b3d_face(self, item_id): 341 | """ 342 | This method tries to find Blender vertex in bmesh and cache 343 | """ 344 | _bmesh = self.node.bmesh 345 | try: 346 | # Try to find blender face at cache first 347 | b3d_face = self.id_cache[item_id] 348 | except KeyError: 349 | try: 350 | # Then try to find it in bmesh at the index==item_id 351 | b3d_face = _bmesh.faces[item_id] 352 | except IndexError: 353 | # When face was not found in cache nor bmesh, then try to 354 | # find it using loop over all faces 355 | id_layer = _bmesh.faces.layers.int.get('FaceIDs') 356 | for b3d_face in _bmesh.faces: 357 | verse_id = b3d_face[id_layer] 358 | if verse_id != -1: 359 | self.id_cache[item_id] = b3d_face 360 | if verse_id == item_id: 361 | return b3d_face 362 | return None 363 | else: 364 | # Update cache 365 | self.id_cache[item_id] = b3d_face 366 | return b3d_face 367 | else: 368 | return b3d_face 369 | 370 | @classmethod 371 | def cb_receive_layer_set_value(cls, session, node_id, layer_id, item_id, value): 372 | """ 373 | This method is called, when new value of verse layer was set 374 | """ 375 | face_layer = super(VerseFaces, cls).cb_receive_layer_set_value(session, node_id, layer_id, item_id, value) 376 | 377 | # Update mesh only in situation, when it was changed by someone else 378 | if face_layer.node.locked_by_me is False: 379 | 380 | vert_layer = face_layer.node.vertices 381 | edge_layer = face_layer.node.edges 382 | 383 | if face_layer.node.bmesh is None: 384 | face_layer.node.bmesh = bmesh.new() 385 | face_layer.node.bmesh.from_mesh(face_layer.node.mesh) 386 | face_layer.node.bm_from_edit_mesh = False 387 | else: 388 | try: 389 | face_layer.node.bmesh.faces 390 | except ReferenceError: 391 | face_layer.node.bmesh = bmesh.new() 392 | face_layer.node.bmesh.from_mesh(face_layer.node.mesh) 393 | vert_layer.id_cache = {} 394 | edge_layer.id_cache = {} 395 | face_layer.id_cache = {} 396 | 397 | _bmesh = face_layer.node.bmesh 398 | 399 | b3d_face = face_layer.find_b3d_face(item_id) 400 | 401 | # When face already exists, then remove the face 402 | if b3d_face is not None: 403 | try: 404 | _bmesh.faces.remove(b3d_face) 405 | except ReferenceError: 406 | # Face was already removed 407 | pass 408 | 409 | # Add new one 410 | if value[3] == 0: 411 | b3d_face = _bmesh.faces.new([vert_layer.b3d_vertex(vert_id) for vert_id in value[0:3]]) 412 | else: 413 | b3d_face = _bmesh.faces.new([vert_layer.b3d_vertex(vert_id) for vert_id in value]) 414 | 415 | # Try to update last face ID 416 | if face_layer.node.last_face_ID is None or \ 417 | face_layer.node.last_face_ID < item_id: 418 | face_layer.node.last_face_ID = item_id 419 | 420 | face_layer.id_cache[item_id] = b3d_face 421 | id_layer = _bmesh.faces.layers.int.get('FaceIDs') 422 | b3d_face[id_layer] = item_id 423 | 424 | # Update Blender mesh 425 | _bmesh.to_mesh(face_layer.node.mesh) 426 | face_layer.node.mesh.update() 427 | 428 | return face_layer 429 | 430 | @classmethod 431 | def cb_receive_layer_unset_value(cls, session, node_id, layer_id, item_id): 432 | """ 433 | This method is called, when some vertex was deleted 434 | """ 435 | face_layer = super(VerseFaces, cls).cb_receive_layer_unset_value(session, node_id, layer_id, item_id) 436 | 437 | # Update mesh only in situation, when it was changed by someone else 438 | if face_layer.node.locked_by_me is False: 439 | 440 | vert_layer = face_layer.node.vertices 441 | edge_layer = face_layer.node.edges 442 | 443 | if face_layer.node.bmesh is None: 444 | face_layer.node.bmesh = bmesh.new() 445 | face_layer.node.bmesh.from_mesh(face_layer.node.mesh) 446 | face_layer.node.bm_from_edit_mesh = False 447 | else: 448 | try: 449 | face_layer.node.bmesh.faces 450 | except ReferenceError: 451 | face_layer.node.bmesh = bmesh.new() 452 | face_layer.node.bmesh.from_mesh(face_layer.node.mesh) 453 | vert_layer.id_cache = {} 454 | edge_layer.id_cache = {} 455 | face_layer.id_cache = {} 456 | 457 | _bmesh = face_layer.node.bmesh 458 | 459 | b3d_face = face_layer.find_b3d_face(item_id) 460 | 461 | # Remove face 462 | if b3d_face is not None: 463 | try: 464 | _bmesh.faces.remove(b3d_face) 465 | except ReferenceError: 466 | # Face was already removed 467 | face_layer.id_cache.pop(item_id) 468 | else: 469 | # Update Blender mesh 470 | _bmesh.to_mesh(face_layer.node.mesh) 471 | face_layer.node.mesh.update() 472 | # Update id_cache 473 | face_layer.id_cache.pop(item_id) 474 | 475 | return face_layer 476 | 477 | 478 | class VerseMesh(vrsent.VerseNode): 479 | """ 480 | Custom VerseNode subclass representing Blender mesh data structure 481 | """ 482 | 483 | custom_type = VERSE_MESH_CT 484 | 485 | def __init__(self, session, node_id=None, parent=None, user_id=None, custom_type=VERSE_MESH_CT, 486 | mesh=None, autosubscribe=False): 487 | """ 488 | Constructor of VerseMesh 489 | """ 490 | super(VerseMesh, self).__init__(session, node_id, parent, user_id, custom_type) 491 | 492 | self.mesh = mesh 493 | self.vertices = VerseVertices(node=self) 494 | self.edges = VerseEdges(node=self) 495 | self.quads = VerseFaces(node=self) 496 | self._autosubscribe = autosubscribe 497 | self.bmesh = None 498 | self.bm_from_edit_mesh = False 499 | self.cache = None 500 | self.last_vert_ID = None 501 | self.last_edge_ID = None 502 | self.last_face_ID = None 503 | 504 | if self.mesh is not None: 505 | # TODO: make following code working in edit mode too 506 | self.mesh.update(calc_tessface=True) 507 | self.bmesh = bmesh.new() 508 | self.bmesh.from_mesh(self.mesh) 509 | # TODO: do not do it in this way for huge mesh (do not send whole mesh), but use 510 | # vrs.get to get free space in outgoing queue. 511 | 512 | # Send all Vertices 513 | for vert in mesh.vertices: 514 | self.vertices.items[vert.index] = tuple(vert.co) 515 | # Send all Edges 516 | for edge in mesh.edges: 517 | self.edges.items[edge.index] = (edge.vertices[0], edge.vertices[1]) 518 | # Send all Faces 519 | for face in mesh.tessfaces: 520 | if len(face.vertices) == 3: 521 | self.quads.items[face.index] = (face.vertices[0], face.vertices[1], face.vertices[2], 0) 522 | else: 523 | self.quads.items[face.index] = tuple(vert for vert in face.vertices) 524 | 525 | # Create blender layers storing Verse IDs of vertices, edges and faces 526 | self.last_vert_ID = self.__create_bpy_layer_ids('verts', 'VertIDs') 527 | self.last_edge_ID = self.__create_bpy_layer_ids('edges', 'EdgeIDs') 528 | self.last_face_ID = self.__create_bpy_layer_ids('faces', 'FaceIDs') 529 | 530 | # Safe blender layers containing IDs to original mesh 531 | self.bmesh.to_mesh(self.mesh) 532 | self.bmesh.free() 533 | self.bmesh = None 534 | 535 | def __create_bpy_layer_ids(self, elems_name, layer_name): 536 | """ 537 | This method create Blender layer storing IDs of vertices or edges or faces 538 | :elems_name: this could be 'verts', 'edges' or 'faces' 539 | """ 540 | elems_iter = getattr(self.bmesh, elems_name) 541 | lay = elems_iter.layers.int.new(layer_name) 542 | lay.use_force_default = True 543 | lay.default_value = -1 544 | # Set values in layer 545 | last_elem_id = None 546 | for elem in elems_iter: 547 | last_elem_id = elem.index 548 | elem[lay] = elem.index 549 | 550 | return last_elem_id 551 | 552 | def get_verse_id_of_vertex(self, bpy_vert): 553 | """ 554 | Return ID of blender vertex at Verse server 555 | """ 556 | layer = self.bmesh.verts.layers.int.get('VertIDs') 557 | return bpy_vert[layer] 558 | 559 | def get_verse_id_of_edge(self, bpy_edge): 560 | """ 561 | Return ID of blender edge at Verse server 562 | """ 563 | layer = self.bmesh.edges.layers.int.get('EdgeIDs') 564 | return bpy_edge[layer] 565 | 566 | def get_verse_id_of_face(self, bpy_face): 567 | """ 568 | Return ID of blender face at Verse server 569 | """ 570 | layer = self.bmesh.faces.layers.int.get('FaceIDs') 571 | return bpy_face[layer] 572 | 573 | def __send_vertex_updates(self): 574 | """ 575 | Try to send updates of geometry and positions of vertices 576 | """ 577 | 578 | alive_verts = {} 579 | 580 | # Go through bmesh and try to detect new positions of vertices, 581 | # deleted vertices and newly created vertices 582 | for b3d_vert in self.bmesh.verts: 583 | verse_id = self.get_verse_id_of_vertex(b3d_vert) 584 | # New vertex was created. Try to send it to Verse server, store it in cache and save verse ID 585 | if verse_id == -1: 586 | # Update the last vertex ID 587 | self.last_vert_ID += 1 588 | verse_id = self.last_vert_ID 589 | # Send new vertex position to Verse server 590 | self.vertices.items[verse_id] = tuple(b3d_vert.co) 591 | # Store verse vertex ID in bmesh layer 592 | layer = self.bmesh.verts.layers.int.get('VertIDs') 593 | b3d_vert[layer] = verse_id 594 | # Position of vertex was changed? 595 | elif self.vertices.items[verse_id] != tuple(b3d_vert.co): 596 | # This will send updated position of vertex 597 | self.vertices.items[verse_id] = tuple(b3d_vert.co) 598 | 599 | # Mark vertex as alive 600 | alive_verts[verse_id] = b3d_vert.index 601 | 602 | # Try to find deleted vertices 603 | rem_verts = [vert_id for vert_id in self.vertices.items.keys() if vert_id not in alive_verts] 604 | # This will send unset commands for deleted vertices 605 | for vert_id in rem_verts: 606 | self.vertices.items.pop(vert_id) 607 | if vert_id in self.vertices.id_cache: 608 | self.vertices.id_cache.pop(vert_id) 609 | 610 | def __send_edge_updates(self): 611 | """ 612 | Try to send updates of topology (edges) 613 | """ 614 | 615 | alive_edges = {} 616 | 617 | # Go through bmesh and try to detect changes in edges (new created edges or deleted edges) 618 | for b3d_edge in self.bmesh.edges: 619 | verse_id = self.get_verse_id_of_edge(b3d_edge) 620 | # New edge was created. Try to send it to Verse server 621 | if verse_id == -1: 622 | self.last_edge_ID += 1 623 | verse_id = self.last_edge_ID 624 | # Send new edge to Verse server 625 | self.edges.items[verse_id] = ( 626 | self.get_verse_id_of_vertex(b3d_edge.verts[0]), 627 | self.get_verse_id_of_vertex(b3d_edge.verts[1]) 628 | ) 629 | # Store edge ID in bmesh layer 630 | layer = self.bmesh.edges.layers.int.get('EdgeIDs') 631 | b3d_edge[layer] = verse_id 632 | else: 633 | # Was edge changed? 634 | edge = ( 635 | self.get_verse_id_of_vertex(b3d_edge.verts[0]), 636 | self.get_verse_id_of_vertex(b3d_edge.verts[1]) 637 | ) 638 | if self.edges.items[verse_id] != edge: 639 | self.edges.items[verse_id] = edge 640 | 641 | alive_edges[verse_id] = b3d_edge.index 642 | 643 | # Try to find deleted edges 644 | rem_edges = [edge_id for edge_id in self.edges.items.keys() if edge_id not in alive_edges] 645 | # This will send unset commands for deleted edges 646 | for edge_id in rem_edges: 647 | self.edges.items.pop(edge_id) 648 | if edge_id in self.edges.id_cache: 649 | self.edges.id_cache.pop(edge_id) 650 | 651 | def __send_face_updates(self): 652 | """ 653 | Try to send updates of topology (faces) 654 | """ 655 | 656 | def b3d_face_to_tuple(_b3d_face): 657 | _face = None 658 | if len(_b3d_face.verts) == 3: 659 | _face = ( 660 | self.get_verse_id_of_vertex(_b3d_face.verts[0]), 661 | self.get_verse_id_of_vertex(_b3d_face.verts[1]), 662 | self.get_verse_id_of_vertex(_b3d_face.verts[2]), 663 | 0 664 | ) 665 | elif len(b3d_face.verts) == 4: 666 | _face = tuple(self.get_verse_id_of_vertex(vert) for vert in _b3d_face.verts) 667 | # The last item of tuple can not be zero, because it indicates triangle. 668 | if _face[3] == 0: 669 | # Rotate the face to get zero to the beginning of the tuple 670 | _face = (_face[3], _face[0], _face[1], _face[2]) 671 | else: 672 | # TODO: tesselate face 673 | print('Error: Face with more than 4 vertices is not supported') 674 | return _face 675 | 676 | alive_faces = {} 677 | 678 | # Go through bmesh faces and try to detect changes (newly created) 679 | for b3d_face in self.bmesh.faces: 680 | verse_id = self.get_verse_id_of_face(b3d_face) 681 | # New face was created. Try to send it to Verse server 682 | if verse_id == -1: 683 | self.last_face_ID += 1 684 | verse_id = self.last_face_ID 685 | self.quads.items[verse_id] = b3d_face_to_tuple(b3d_face) 686 | # Store face ID in bmesh layer 687 | layer = self.bmesh.faces.layers.int.get('FaceIDs') 688 | b3d_face[layer] = verse_id 689 | # Update id cache 690 | self.quads.id_cache[verse_id] = b3d_face 691 | else: 692 | # Was face changed? 693 | face = b3d_face_to_tuple(b3d_face) 694 | if self.quads.items[verse_id] != face: 695 | self.quads.items[verse_id] = face 696 | 697 | alive_faces[verse_id] = b3d_face.index 698 | 699 | # Try to find deleted faces 700 | rem_faces = [face_id for face_id in self.quads.items.keys() if face_id not in alive_faces] 701 | # This will send unset commands for deleted faces 702 | for face_id in rem_faces: 703 | self.quads.items.pop(face_id) 704 | if face_id in self.quads.id_cache: 705 | self.quads.id_cache.pop(face_id) 706 | 707 | def clear_ID_cache(self): 708 | """ 709 | This method clear cache with references on vertices, edges and faces 710 | """ 711 | self.vertices.id_cache = {} 712 | self.edges.id_cache = {} 713 | self.quads.id_cache = {} 714 | 715 | def update_references(self): 716 | """ 717 | This method tries to update references at bmesh, when old bmesh was removed 718 | """ 719 | 720 | if self.bmesh is None: 721 | if bpy.context.edit_object is not None and \ 722 | bpy.context.edit_object.data == self.mesh: 723 | self.bmesh = bmesh.from_edit_mesh(self.mesh) 724 | self.bm_from_edit_mesh = True 725 | else: 726 | self.bmesh = bmesh.new() 727 | self.bmesh.from_mesh(self.mesh) 728 | self.bm_from_edit_mesh = False 729 | else: 730 | try: 731 | self.bmesh.verts 732 | except ReferenceError: 733 | if bpy.context.edit_object is not None and \ 734 | bpy.context.edit_object.data == self.mesh: 735 | self.bmesh = bmesh.from_edit_mesh(self.mesh) 736 | self.bm_from_edit_mesh = True 737 | else: 738 | self.bmesh = bmesh.new() 739 | self.bmesh.from_mesh(self.mesh) 740 | self.bm_from_edit_mesh = False 741 | self.clear_ID_cache() 742 | 743 | def send_updates(self): 744 | """ 745 | Try to send update of edit mesh to Verse server 746 | """ 747 | if self.bmesh is None: 748 | self.bmesh = bmesh.from_edit_mesh(self.mesh) 749 | self.bm_from_edit_mesh = True 750 | else: 751 | if self.bm_from_edit_mesh is False: 752 | self.bmesh = bmesh.from_edit_mesh(self.mesh) 753 | self.bm_from_edit_mesh = True 754 | self.clear_ID_cache() 755 | else: 756 | # Check if bmesh is still fresh 757 | try: 758 | self.bmesh.verts 759 | except ReferenceError: 760 | self.bmesh = bmesh.from_edit_mesh(self.mesh) 761 | self.clear_ID_cache() 762 | self.__send_vertex_updates() 763 | self.__send_edge_updates() 764 | self.__send_face_updates() 765 | 766 | def create_empty_b3d_mesh(self, object_node): 767 | """ 768 | Create empty mesh and create blender layers for entity IDs 769 | """ 770 | # Mesh should be empty ATM 771 | self.mesh = object_node.obj.data 772 | self.bmesh = bmesh.new() 773 | self.bmesh.from_mesh(self.mesh) 774 | # Create layers for verse IDs 775 | vert_lay = self.bmesh.verts.layers.int.new('VertIDs') 776 | vert_lay.use_force_default = True 777 | vert_lay.default_value = -1 778 | edge_lay = self.bmesh.edges.layers.int.new('EdgeIDs') 779 | edge_lay.use_force_default = True 780 | edge_lay.default_value = -1 781 | face_lay = self.bmesh.faces.layers.int.new('FaceIDs') 782 | face_lay.use_force_default = True 783 | face_lay.default_value = -1 784 | # Safe blender layers containing IDs to original mesh 785 | self.bmesh.to_mesh(self.mesh) 786 | self.bmesh.free() 787 | self.bmesh = None 788 | 789 | @classmethod 790 | def cb_receive_node_link(cls, session, parent_node_id, child_node_id): 791 | """ 792 | When link between nodes is changed, then try to create mesh. 793 | """ 794 | mesh_node = super(VerseMesh, cls).cb_receive_node_link( 795 | session=session, 796 | parent_node_id=parent_node_id, 797 | child_node_id=child_node_id 798 | ) 799 | 800 | try: 801 | object_node = object3d.VerseObject.objects[parent_node_id] 802 | except KeyError: 803 | pass 804 | else: 805 | mesh_node.create_empty_b3d_mesh(object_node) 806 | mesh_node.mesh.verse_node_id = child_node_id 807 | object_node.mesh_node = mesh_node 808 | 809 | return mesh_node 810 | 811 | @classmethod 812 | def cb_receive_node_create(cls, session, node_id, parent_id, user_id, custom_type): 813 | """ 814 | When new mesh node is created or verse server, then this callback method is called. 815 | """ 816 | # Call parent class 817 | mesh_node = super(VerseMesh, cls).cb_receive_node_create( 818 | session=session, 819 | node_id=node_id, 820 | parent_id=parent_id, 821 | user_id=user_id, 822 | custom_type=custom_type 823 | ) 824 | 825 | # When this mesh was created at different Blender, then mesh_node does 826 | # not have valid reference at blender mesh data block 827 | if mesh_node.mesh is None: 828 | try: 829 | object_node = object3d.VerseObject.objects[parent_id] 830 | except KeyError: 831 | # The object was not created yet 832 | pass 833 | else: 834 | mesh_node.create_empty_b3d_mesh(object_node) 835 | mesh_node.mesh.verse_node_id = node_id 836 | object_node.mesh_node = mesh_node 837 | 838 | return mesh_node 839 | 840 | def draw_IDs(self, context, obj): 841 | """ 842 | This method draws Verse IDs of vertices, edges and faces 843 | """ 844 | 845 | font_id, font_size, my_dpi = 0, 12, 72 846 | 847 | self.update_references() 848 | 849 | vert_id_layer = self.bmesh.verts.layers.int.get('VertIDs') 850 | edge_id_layer = self.bmesh.edges.layers.int.get('EdgeIDs') 851 | face_id_layer = self.bmesh.faces.layers.int.get('FaceIDs') 852 | 853 | bgl.glColor3f(1.0, 1.0, 0.0) 854 | 855 | for vert_id, vert_co in self.vertices.items.items(): 856 | 857 | coord_2d = location_3d_to_region_2d( 858 | context.region, 859 | context.space_data.region_3d, 860 | obj.matrix_world * mathutils.Vector(vert_co)) 861 | 862 | b3d_vert = self.vertices.b3d_vertex(vert_id) 863 | if b3d_vert is not None: 864 | b3d_vert_id = b3d_vert[vert_id_layer] 865 | else: 866 | b3d_vert_id = None 867 | 868 | # When coordinates are not outside window, then draw the ID of vertex 869 | if coord_2d is not None: 870 | blf.size(font_id, font_size, my_dpi) 871 | blf.position(font_id, coord_2d[0] + 2, coord_2d[1] + 2, 0) 872 | blf.draw(font_id, str((vert_id, b3d_vert_id))) 873 | 874 | bgl.glColor3f(0.0, 1.0, 0.0) 875 | 876 | for edge_id, edge_verts in self.edges.items.items(): 877 | vert1 = self.vertices.items[edge_verts[0]] 878 | vert2 = self.vertices.items[edge_verts[1]] 879 | 880 | edge_co = mathutils.Vector(( 881 | (vert2[0] + vert1[0]) / 2.0, 882 | (vert2[1] + vert1[1]) / 2.0, 883 | (vert2[2] + vert1[2]) / 2.0)) 884 | 885 | b3d_edge = self.edges.b3d_edge(edge_id) 886 | if b3d_edge is not None: 887 | b3d_edge_id = b3d_edge[edge_id_layer] 888 | else: 889 | b3d_edge_id = None 890 | 891 | coord_2d = location_3d_to_region_2d( 892 | context.region, 893 | context.space_data.region_3d, 894 | obj.matrix_world * edge_co) 895 | 896 | # When coordinates are not outside window, then draw the ID of edge 897 | if coord_2d is not None: 898 | blf.size(font_id, font_size, my_dpi) 899 | blf.position(font_id, coord_2d[0] + 2, coord_2d[1] + 2, 0) 900 | blf.draw(font_id, str((edge_id, b3d_edge_id))) 901 | 902 | bgl.glColor3f(0.0, 1.0, 1.0) 903 | 904 | for face_id, face_verts in self.quads.items.items(): 905 | if face_verts[3] == 0: 906 | vert1 = self.vertices.items[face_verts[0]] 907 | vert2 = self.vertices.items[face_verts[1]] 908 | vert3 = self.vertices.items[face_verts[2]] 909 | face_co = mathutils.Vector(( 910 | (vert1[0] + vert2[0] + vert3[0]) / 3.0, 911 | (vert1[1] + vert2[1] + vert3[1]) / 3.0, 912 | (vert1[2] + vert2[2] + vert3[2]) / 3.0 913 | )) 914 | else: 915 | vert1 = self.vertices.items[face_verts[0]] 916 | vert2 = self.vertices.items[face_verts[1]] 917 | vert3 = self.vertices.items[face_verts[2]] 918 | vert4 = self.vertices.items[face_verts[3]] 919 | face_co = mathutils.Vector(( 920 | (vert1[0] + vert2[0] + vert3[0] + vert4[0]) / 4.0, 921 | (vert1[1] + vert2[1] + vert3[1] + vert4[1]) / 4.0, 922 | (vert1[2] + vert2[2] + vert3[2] + vert4[2]) / 4.0 923 | )) 924 | 925 | b3d_face = self.quads.find_b3d_face(face_id) 926 | if b3d_face is not None: 927 | b3d_face_id = b3d_face[face_id_layer] 928 | else: 929 | b3d_face_id = None 930 | 931 | coord_2d = location_3d_to_region_2d( 932 | context.region, 933 | context.space_data.region_3d, 934 | obj.matrix_world * face_co) 935 | 936 | # When coordinates are not outside window, then draw the ID of face 937 | if coord_2d is not None: 938 | blf.size(font_id, font_size, my_dpi) 939 | blf.position(font_id, coord_2d[0] + 2, coord_2d[1] + 2, 0) 940 | blf.draw(font_id, str((face_id, b3d_face_id))) 941 | 942 | # List of Blender classes in this submodule 943 | classes = () 944 | 945 | 946 | def init_properties(): 947 | """ 948 | Init properties in blender object data type 949 | """ 950 | bpy.types.Mesh.verse_node_id = bpy.props.IntProperty( 951 | name="ID of verse mesh node", 952 | default=-1, 953 | description="ID of node representing mesh at Verse server" 954 | ) 955 | 956 | 957 | def register(): 958 | """ 959 | This method register all methods of this submodule 960 | """ 961 | for c in classes: 962 | bpy.utils.register_class(c) 963 | init_properties() 964 | 965 | 966 | def unregister(): 967 | """ 968 | This method unregister all methods of this submodule 969 | """ 970 | for c in classes: 971 | bpy.utils.unregister_class(c) 972 | 973 | 974 | if __name__ == '__main__': 975 | register() 976 | --------------------------------------------------------------------------------