Created
April 17, 2025 08:12
-
-
Save unvestigate/92d99a62e3f53e751385b107e3c63455 to your computer and use it in GitHub Desktop.
The main source code files for the BlenderSync addon
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import bpy | |
| import ctypes | |
| import tempfile | |
| import pathlib | |
| import subprocess | |
| import zipfile | |
| import os | |
| import mathutils | |
| blender_sync_lib = None | |
| server_running = False | |
| server_shutdown_requested = False | |
| editing_mesh = False | |
| surroundings_obj_file_path = "" | |
| # Update the server four times per second. | |
| server_update_rate = 0.25 | |
| # These need to match the values on the C side: | |
| BLENDERSYNC_EVENT_NONE = 0 | |
| BLENDERSYNC_EVENT_CLIENT_CONNECTED = 1 | |
| BLENDERSYNC_EVENT_CLIENT_DISCONNECTED = 2 | |
| BLENDERSYNC_EVENT_TARGET_MESH_PATH_RECEIVED = 3 | |
| BLENDERSYNC_EVENT_TARGET_AND_SURROUNDINGS_MESH_PATH_RECEIVED = 4 | |
| BLENDERSYNC_TARGET_OBJECT_NAME = "BlenderSyncTarget" | |
| BLENDERSYNC_TARGET_OBJECT_DATA_NAME = "BlenderSyncTargetData" | |
| BLENDERSYNC_SURROUNDINGS_OBJECT_NAME = "BlenderSyncSurroundingsObj" | |
| BLENDERSYNC_HELPERS_COLLECTION_NAME = "BlenderSyncHelpers" | |
| BLENDERSYNC_ZIP_BLEND_ARCNAME = "blendersync_target.blend" | |
| BLENDERSYNC_ZIP_FBX_ARCNAME = "blendersync_target.fbx" | |
| #server_iteration = 0 | |
| def get_addon_name(): | |
| return __package__.split('.')[0] | |
| def update_server(): | |
| global server_running | |
| global server_shutdown_requested | |
| global editing_mesh | |
| global surroundings_obj_file_path | |
| #global server_iteration | |
| #print(f"server iteration: {server_iteration}") | |
| #server_iteration += 1 | |
| if server_shutdown_requested: | |
| print("BasisBlenderSync: Shutting down server...") | |
| server_running = False | |
| server_shutdown_requested = False | |
| editing_mesh = False | |
| blender_sync_lib.stopServer() | |
| return None | |
| evt = blender_sync_lib.service() | |
| if evt == BLENDERSYNC_EVENT_NONE: | |
| pass | |
| elif evt == BLENDERSYNC_EVENT_CLIENT_CONNECTED: | |
| print("BasisBlenderSync: client connected") | |
| elif evt == BLENDERSYNC_EVENT_CLIENT_DISCONNECTED: | |
| print("BasisBlenderSync: client disconnected") | |
| elif (evt == BLENDERSYNC_EVENT_TARGET_MESH_PATH_RECEIVED or | |
| evt == BLENDERSYNC_EVENT_TARGET_AND_SURROUNDINGS_MESH_PATH_RECEIVED): | |
| target_mesh_path = get_target_mesh_file_path() | |
| editing_mesh = True | |
| #print(f"Received target path: {target_mesh_path}") | |
| if target_mesh_path == "": | |
| print("BasisBlenderSync: Creating new scene.") | |
| create_new_mesh() | |
| else: | |
| print(f"BasisBlenderSync: Loading previous scene: {target_mesh_path}") | |
| open_mesh(target_mesh_path) | |
| if evt == BLENDERSYNC_EVENT_TARGET_AND_SURROUNDINGS_MESH_PATH_RECEIVED: | |
| surroundings_obj_file_path = get_surroundings_mesh_file_path() | |
| print(f"BasisBlenderSync: Importing surroundings from: {surroundings_obj_file_path}") | |
| import_surroundings_mesh(surroundings_obj_file_path, False) | |
| #print(f"Received target path: {target_mesh_path}, and surroundings path: {surroundings_obj_file_path}") | |
| else: | |
| surroundings_obj_file_path = "" | |
| return server_update_rate | |
| def get_target_mesh_file_path(): | |
| b = blender_sync_lib.getTargetMeshFilePath() | |
| return b.decode('UTF-8') | |
| def get_surroundings_mesh_file_path(): | |
| b = blender_sync_lib.getSurroundingsMeshFilePath() | |
| return b.decode('UTF-8') | |
| ############################# | |
| # Commented out, this is used for adding a panel to the "N-menu" on the right of the view. | |
| # class VIEW3D_PT_blendersync(bpy.types.Panel): | |
| # bl_space_type = "VIEW_3D" | |
| # bl_region_type = "UI" | |
| # bl_category = "BlenderSync" | |
| # bl_label = "Basis BlenderSync" | |
| # def draw(self, context): | |
| # global server_running | |
| # if server_running: | |
| # self.layout.label(text="Server running", icon="FUND") # FUND is a heart. | |
| # self.layout.operator("object.stop_blendersync_server") | |
| # else: | |
| # self.layout.label(text="Server stopped") | |
| # self.layout.operator("object.start_blendersync_server") | |
| # self.layout.separator() | |
| # col = self.layout.column(align=True) | |
| # col.enabled = server_running | |
| # col.operator("object.finish_editing_blendersync_object") | |
| class VIEW3D_PT_blendersync_button(bpy.types.Panel): | |
| bl_space_type = "VIEW_3D" | |
| bl_region_type = "HEADER" | |
| #bl_category = "BlenderSync" | |
| bl_label = "Basis BlenderSync" | |
| @staticmethod | |
| def draw_popover(self, context): | |
| global server_running | |
| global editing_mesh | |
| self.layout.separator() | |
| if editing_mesh: | |
| # We use the ERROR icon here since it is a warning sign. There is no error, it's just to notify the | |
| # user that we are editing the mesh at the moment. | |
| self.layout.popover('VIEW3D_PT_blendersync_button', text="Basis BlenderSync", icon="FILE_3D") | |
| else: | |
| if server_running: | |
| self.layout.popover('VIEW3D_PT_blendersync_button', text="Basis BlenderSync", icon="LINKED") | |
| else: | |
| self.layout.popover('VIEW3D_PT_blendersync_button', text="Basis BlenderSync", icon="UNLINKED") | |
| def draw(self, context): | |
| global server_running | |
| global editing_mesh | |
| layout = self.layout | |
| if server_running: | |
| layout.label(text="Server running", icon="LINKED") | |
| layout.operator("object.stop_blendersync_server", icon="SNAP_FACE") # SNAP_FACE looks like a stop sign. | |
| else: | |
| layout.label(text="Server stopped", icon="UNLINKED") | |
| layout.operator("object.start_blendersync_server", icon="PLAY") | |
| if editing_mesh: | |
| layout.separator() | |
| col = layout.column(align=True) | |
| col.label(text="Currently editing an object") | |
| col.operator(FinishEditingBlenderSyncObject.bl_idname, text="Finish editing", icon="CHECKMARK") | |
| col.operator(SendBlenderSyncDataAndKeepEditing.bl_idname, text="Update and keep editing", icon="UV_SYNC_SELECT") | |
| col.separator() | |
| col.operator(CancelBlenderSyncEdit.bl_idname, text="Cancel edit", icon="CANCEL") | |
| col.separator() | |
| col.operator(CreateHelpersCollectionOperator.bl_idname, text="Create helpers collection", icon="OUTLINER_COLLECTION") | |
| ################################################################## | |
| class AddonPreferences(bpy.types.AddonPreferences): | |
| bl_idname = get_addon_name() | |
| server_port_number: bpy.props.IntProperty(name="Port number", | |
| description="The UDP port number to start the server on.", | |
| min=0, | |
| max=65535, | |
| default=1950) | |
| basis_mesh_converter_path: bpy.props.StringProperty(name="Mesh converter exe path", | |
| description="The path to the Basis mesh converter exe.", | |
| default="") | |
| def draw(self, context): | |
| self.layout.label(text="These are the preferences for Basis BlenderSync.") | |
| self.layout.prop(self, "server_port_number") | |
| self.layout.prop(self, "basis_mesh_converter_path") | |
| def get_addon_prefs(context): | |
| preferences = context.preferences | |
| addon_prefs = preferences.addons[get_addon_name()].preferences | |
| return addon_prefs | |
| ################################################################## | |
| # Operators for starting/stopping the server. | |
| class StartServerOperator(bpy.types.Operator): | |
| bl_idname = "object.start_blendersync_server" | |
| bl_label = "Start Server" | |
| def execute(self, context): | |
| global server_running | |
| global server_shutdown_requested | |
| server_shutdown_requested = False | |
| addon_prefs = get_addon_prefs(context) | |
| if addon_prefs.basis_mesh_converter_path == "": | |
| self.report({'ERROR'}, "The Basis mesh converter exe path is not set in the BlenderSync addon preferences.") | |
| return {'FINISHED'} | |
| print(f"Starting server on port {addon_prefs.server_port_number}") | |
| s = blender_sync_lib.startServer(addon_prefs.server_port_number) | |
| if s == 0: | |
| # Kick off the server update loop. persistent=True means that | |
| # the timer keeps running even after a new blend file is loaded. | |
| bpy.app.timers.register(update_server, persistent=True) | |
| server_running = True | |
| else: | |
| self.report({'ERROR'}, "The server failed to start.") | |
| return {'FINISHED'} | |
| class StopServerOperator(bpy.types.Operator): | |
| bl_idname = "object.stop_blendersync_server" | |
| bl_label = "Stop Server" | |
| def execute(self, context): | |
| global server_running | |
| global server_shutdown_requested | |
| server_shutdown_requested = True | |
| server_running = False | |
| return {'FINISHED'} | |
| ################################################################## | |
| # Operator for sending the selected object back to Basis. | |
| class FinishEditingBlenderSyncObject(bpy.types.Operator): | |
| bl_idname = "object.finish_editing_blendersync_object" | |
| bl_label = "Finish editing object" | |
| def execute(self, context): | |
| global server_running | |
| global editing_mesh | |
| if not server_running: | |
| self.report({'ERROR'}, "Cannot send object to Basis, the server is not running.") | |
| return {'FINISHED'} | |
| keep_editing = False | |
| if send_mesh_data_to_basis(self, context, keep_editing): | |
| editing_mesh = False | |
| return {'FINISHED'} | |
| class SendBlenderSyncDataAndKeepEditing(bpy.types.Operator): | |
| bl_idname = "object.send_blendersync_data_and_keep_editing" | |
| bl_label = "Send data to Basis and keep editing" | |
| def execute(self, context): | |
| global server_running | |
| if not server_running: | |
| self.report({'ERROR'}, "Cannot send object to Basis, the server is not running.") | |
| return {'FINISHED'} | |
| keep_editing = True | |
| send_mesh_data_to_basis(self, context, keep_editing) | |
| return {'FINISHED'} | |
| import bpy | |
| class CancelBlenderSyncEdit(bpy.types.Operator): | |
| bl_idname = "object.cancel_blendersync_edit" | |
| bl_label = "Really cancel the edit?" | |
| bl_options = {'REGISTER', 'INTERNAL'} | |
| @classmethod | |
| def poll(cls, context): | |
| return True | |
| def execute(self, context): | |
| cancel_edit(self) | |
| return {'FINISHED'} | |
| def invoke(self, context, event): | |
| return context.window_manager.invoke_confirm(self, event) | |
| class CreateHelpersCollectionOperator(bpy.types.Operator): | |
| bl_idname = "object.create_blendersync_helper_collection" | |
| bl_label = "Create Helpers Collection" | |
| def execute(self, context): | |
| create_helpers_collection() | |
| return {'FINISHED'} | |
| ################################################################## | |
| # # Operator for clearing the scene. | |
| # class DeleteObjectsOperator(bpy.types.Operator): | |
| # bl_idname = "object.blendersync_clear_scene" | |
| # bl_label = "Delete Objects (BlenderSync)" | |
| # def execute(self, context): | |
| # if bpy.context.object.mode == 'EDIT': | |
| # bpy.ops.object.mode_set(mode='OBJECT') | |
| # # deselect all objects | |
| # bpy.ops.object.select_all(action='SELECT') | |
| # # delete all selected objects | |
| # bpy.ops.object.delete() | |
| # return {'FINISHED'} | |
| # class TestCreateNewMesh(bpy.types.Operator): | |
| # bl_idname = "object.test_blendersync_create_new_mesh" | |
| # bl_label = "Test Create New BlenderSync Mesh" | |
| # def execute(self, context): | |
| # #print(context.selected_objects) | |
| # create_new_mesh() | |
| # return {'FINISHED'} | |
| # class TestSaveBlenderSyncToTempFile(bpy.types.Operator): | |
| # bl_idname = "object.test_blendersync_save_to_temp_file" | |
| # bl_label = "Test Save Blender Sync To Temp File" | |
| # def execute(self, context): | |
| # finish_editing_mesh(self, context) | |
| # return {'FINISHED'} | |
| ################################################################## | |
| # Function for reading/writing BlenderSync meshes. | |
| def create_helpers_collection(): | |
| # If the helpers collection isn't already there, create and link it. | |
| if bpy.data.collections.get(BLENDERSYNC_HELPERS_COLLECTION_NAME) == None: | |
| coll = bpy.data.collections.new(BLENDERSYNC_HELPERS_COLLECTION_NAME) | |
| bpy.context.scene.collection.children.link(coll) | |
| def create_new_mesh(): | |
| bpy.ops.wm.read_homefile(use_empty=True) | |
| bpy.ops.mesh.primitive_cube_add() | |
| target_obj = bpy.context.scene.objects[0] | |
| target_obj.name = BLENDERSYNC_TARGET_OBJECT_NAME | |
| target_obj.data.name = BLENDERSYNC_TARGET_OBJECT_DATA_NAME | |
| set_target_object_rotation(target_obj) | |
| def open_mesh(zipped_work_file_path): | |
| containing_folder = str(pathlib.Path(zipped_work_file_path).parent) | |
| archive = zipfile.ZipFile(zipped_work_file_path, 'r') | |
| blend_file_path = archive.extract(BLENDERSYNC_ZIP_BLEND_ARCNAME, path=containing_folder) | |
| #print(f"Extracted blend file: {blend_file_path}") | |
| bpy.ops.wm.open_mainfile(filepath=blend_file_path) | |
| target_obj = bpy.context.scene.objects.get(BLENDERSYNC_TARGET_OBJECT_NAME) | |
| target_obj.select_set(True) | |
| bpy.context.view_layer.objects.active = target_obj | |
| set_target_object_rotation(target_obj) | |
| print(f"BasisBlenderSync: Opened blend file: {blend_file_path}") | |
| def import_surroundings_mesh(surroundings_obj_file_path, hide_surroundings): | |
| # Activate the master collection. | |
| bpy.context.view_layer.active_layer_collection = bpy.context.view_layer.layer_collection | |
| # Use the new obj importer, starting with Blender 4.0. | |
| if (bpy.app.version[0] >= 4): | |
| # We need the context.temp_override stuff, otherwise we get | |
| # 'RuntimeError: Operator bpy.ops.wm.obj_import.poll() failed, context is incorrect' | |
| with bpy.context.temp_override(window=bpy.data.window_managers[0].windows[0]): | |
| bpy.ops.wm.obj_import(filepath=surroundings_obj_file_path) | |
| else: | |
| bpy.ops.import_scene.obj(filepath=surroundings_obj_file_path) | |
| helpers_coll = bpy.data.collections.get(BLENDERSYNC_HELPERS_COLLECTION_NAME) | |
| # Any and all objects in the scene that are not the target object nor a | |
| # helper object are considered to be part of the surroundings | |
| for obj in bpy.context.scene.objects: | |
| if (helpers_coll != None) and (obj in list(helpers_coll.objects)): | |
| continue | |
| if obj.name != BLENDERSYNC_TARGET_OBJECT_NAME: | |
| obj.name = BLENDERSYNC_SURROUNDINGS_OBJECT_NAME | |
| obj.hide_set(hide_surroundings) | |
| #obj.hide_viewport = hide_surroundings | |
| # Re-select the target object. | |
| bpy.ops.object.select_all(action='DESELECT') | |
| target_obj = bpy.context.scene.objects.get(BLENDERSYNC_TARGET_OBJECT_NAME) | |
| target_obj.select_set(True) | |
| bpy.context.view_layer.objects.active = target_obj | |
| # Sends the mesh data to Basis and returns whether or not the operation succeeded. | |
| def send_mesh_data_to_basis(op, context, keep_editing): | |
| if bpy.context.object.mode != 'OBJECT': | |
| bpy.ops.object.mode_set(mode='OBJECT') | |
| surroundings_object_hidden = False | |
| target_obj = bpy.context.scene.objects.get(BLENDERSYNC_TARGET_OBJECT_NAME) | |
| surroundings_obj = bpy.context.scene.objects.get(BLENDERSYNC_SURROUNDINGS_OBJECT_NAME) | |
| if not target_obj: | |
| op.report({'ERROR'}, f"Could not find BlenderSync target object: '{BLENDERSYNC_TARGET_OBJECT_NAME}'") | |
| return False | |
| if surroundings_obj: | |
| surroundings_object_hidden = not surroundings_obj.visible_get() | |
| print(f"BasisBlenderSync: Surroundings object found, hidden: {surroundings_object_hidden}") | |
| # Unhide all objects. | |
| for obj in bpy.context.scene.objects: | |
| obj.hide_set(False) | |
| #obj.hide_viewport = False | |
| world_origin = mathutils.Vector([0, 0, 0]) | |
| distance_to_origin = (target_obj.location - world_origin).length | |
| if distance_to_origin > 0.0001: | |
| op.report({'ERROR'}, "The target object pivot is not at the origin.") | |
| return False | |
| # Should be able to use this to "apply all transforms" to the target object if we want. (untested) | |
| #bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) | |
| # Reset the rotation of the target object. | |
| target_obj.rotation_mode = 'XYZ' | |
| target_obj.rotation_euler = [0, 0, 0] | |
| # If we are in local-view (numpad '/') we need to toggle out of it before proceeding. | |
| # Otherwise the attempt to delete the surroundings object(s) fails. | |
| if context.space_data.local_view is not None: | |
| bpy.ops.view3d.localview() | |
| #print("Toggled out of local-view."); | |
| # Deselect all objects. | |
| bpy.ops.object.select_all(action='DESELECT') | |
| # If we have a helpers collection, select every object in it. | |
| helpers_coll = bpy.data.collections.get(BLENDERSYNC_HELPERS_COLLECTION_NAME) | |
| if not helpers_coll is None: | |
| for helper_obj in helpers_coll.objects: | |
| helper_obj.select_set(True) | |
| # Make the target the active object. | |
| #bpy.context.view_layer.objects.active = target_obj | |
| # Select the target object and invert the selection. | |
| target_obj.select_set(True) | |
| bpy.ops.object.select_all(action='INVERT') | |
| # Delete the selected object(s), ie. all objects except the target (and any helper objects). | |
| bpy.ops.object.delete() | |
| # Remove all unused data. | |
| for block in bpy.data.meshes: | |
| if block.users == 0: | |
| bpy.data.meshes.remove(block) | |
| for block in bpy.data.materials: | |
| if block.users == 0: | |
| bpy.data.materials.remove(block) | |
| for block in bpy.data.images: | |
| if block.users == 0: | |
| bpy.data.images.remove(block) | |
| for block in bpy.data.textures: | |
| if block.users == 0: | |
| bpy.data.textures.remove(block) | |
| # Select the target object again. | |
| target_obj.select_set(True) | |
| # Generate a writable temp file name by creating a temp file and appending the extensions as appropriate. | |
| fo = tempfile.NamedTemporaryFile() | |
| temp_file_path = fo.name | |
| fo.close() | |
| work_file_path = temp_file_path + ".blend" | |
| zipped_work_file_path = temp_file_path + ".zip" | |
| exported_file_path = temp_file_path + ".fbx" | |
| exported_file_meta_path = temp_file_path + ".fbx.meta" | |
| converted_file_path = temp_file_path + ".binmesh" | |
| converter_metadata_file_path = temp_file_path + ".meta" | |
| # Save the blender scene to a temp file. copy=True saves the file but doesn't make it the active document. | |
| bpy.ops.wm.save_as_mainfile(filepath=work_file_path, copy=True) | |
| print(f"BasisBlenderSync: Work file saved to '{work_file_path}'") | |
| # Export the target object to a temp fbx file. | |
| # bpy.ops.export_scene.fbx(filepath='', check_existing=True, filter_glob='*.fbx', use_selection=False, | |
| # use_active_collection=False, global_scale=1.0, apply_unit_scale=True, apply_scale_options='FBX_SCALE_NONE', | |
| # use_space_transform=True, bake_space_transform=False, | |
| # object_types={'ARMATURE', 'CAMERA', 'EMPTY', 'LIGHT', 'MESH', 'OTHER'}, use_mesh_modifiers=True, | |
| # use_mesh_modifiers_render=True, mesh_smooth_type='OFF', use_subsurf=False, use_mesh_edges=False, | |
| # use_tspace=False, use_custom_props=False, add_leaf_bones=True, primary_bone_axis='Y', secondary_bone_axis='X', | |
| # use_armature_deform_only=False, armature_nodetype='NULL', bake_anim=True, bake_anim_use_all_bones=True, | |
| # bake_anim_use_nla_strips=True, bake_anim_use_all_actions=True, bake_anim_force_startend_keying=True, | |
| # bake_anim_step=1.0, bake_anim_simplify_factor=1.0, path_mode='AUTO', embed_textures=False, batch_mode='OFF', | |
| # use_batch_own_dir=True, use_metadata=True, axis_forward='-Z', axis_up='Y') | |
| bpy.ops.export_scene.fbx(filepath=exported_file_path, | |
| use_selection=True, object_types={'MESH'}, axis_forward='Z') | |
| print(f"BasisBlenderSync: Object exported to '{exported_file_path}'") | |
| # Write a meta file to instruct the Basis mesh converter what to do: | |
| # Example: | |
| # { | |
| # "vertexFormat":"PositionTangentBinormalNormalTexcoord", | |
| # "generatePhysicsTriMesh":false, | |
| # "linearVertexColors":false, | |
| # "optimizeGeometry":true, | |
| # "resourceTags":[] | |
| # } | |
| exported_mesh_metadata_json = '{"generatePhysicsTriMesh":true}' | |
| with open(exported_file_meta_path, 'w') as outfile: | |
| outfile.write(exported_mesh_metadata_json) | |
| # Make sure we have the Basis mesh converter path set up. | |
| addon_prefs = get_addon_prefs(context) | |
| print(f"BasisBlenderSync: Mesh converter path: {addon_prefs.basis_mesh_converter_path}") | |
| #s = blender_sync_lib.startServer(addon_prefs.server_port_number) | |
| if addon_prefs.basis_mesh_converter_path == "": | |
| op.report({'ERROR'}, "The Basis mesh converter exe path is not set in the BlenderSync addon preferences.") | |
| return False | |
| mesh_converter_path = pathlib.Path(addon_prefs.basis_mesh_converter_path) | |
| if not mesh_converter_path.exists(): | |
| op.report({'ERROR'}, f"The Basis mesh converter exe path {addon_prefs.basis_mesh_converter_path} is not valid.") | |
| return False | |
| # Convert the fbx file to a binmesh. | |
| conversion_command = f'{addon_prefs.basis_mesh_converter_path} -i:"{exported_file_path}" -o:"{converted_file_path}" -m:"{converter_metadata_file_path}" -p:physx' | |
| conversion_result = subprocess.run(conversion_command, shell=True) | |
| print(f"BasisBlenderSync: Object converted to '{converted_file_path}'") | |
| if conversion_result.returncode != 0: | |
| op.report({'ERROR'}, f"Error converting the object to a Basis mesh. Check the Blender console for more info.") | |
| return False | |
| # Write the blend file to a zip archive. | |
| with zipfile.ZipFile(zipped_work_file_path, 'w') as zipObj: | |
| zipObj.write(work_file_path, arcname=BLENDERSYNC_ZIP_BLEND_ARCNAME, compress_type=zipfile.ZIP_DEFLATED, compresslevel=9) | |
| zipObj.write(exported_file_path, arcname=BLENDERSYNC_ZIP_FBX_ARCNAME, compress_type=zipfile.ZIP_DEFLATED, compresslevel=9) | |
| # Remove the files we don't need anymore. | |
| os.remove(work_file_path) | |
| os.remove(exported_file_path) | |
| workFileZipPathBytes = zipped_work_file_path.encode('UTF-8') | |
| binmeshFilePathBytes = converted_file_path.encode('UTF-8') | |
| if not server_running: | |
| op.report({'ERROR'}, f"The BlenderSync server is not running. Cannot send the mesh data to Basis.") | |
| return False | |
| if blender_sync_lib.sendUpdatedMeshFilePaths(workFileZipPathBytes, len(workFileZipPathBytes), | |
| binmeshFilePathBytes, len(binmeshFilePathBytes), keep_editing) == 0: | |
| op.report({'INFO'}, "Successfully sent the mesh data to the Basis editor.") | |
| else: | |
| op.report({'ERROR'}, f"Error sending the mesh data to Basis. Is the client connected?") | |
| if keep_editing: | |
| # The user wants to keep editing. | |
| open_mesh(zipped_work_file_path) | |
| if surroundings_obj_file_path != "": | |
| import_surroundings_mesh(surroundings_obj_file_path, surroundings_object_hidden) | |
| else: | |
| # The user does not want to keep editing. Clear the scene. | |
| bpy.ops.wm.read_homefile(use_empty=True) | |
| return True | |
| def set_target_object_rotation(target_obj): | |
| w = blender_sync_lib.getTargetObjectRotationW() | |
| x = blender_sync_lib.getTargetObjectRotationX() | |
| y = blender_sync_lib.getTargetObjectRotationY() | |
| z = blender_sync_lib.getTargetObjectRotationZ() | |
| print(f"BasisBlenderSync: Target rotation W: {w}") | |
| print(f"BasisBlenderSync: Target rotation X: {x}") | |
| print(f"BasisBlenderSync: Target rotation Y: {y}") | |
| print(f"BasisBlenderSync: Target rotation Z: {z}") | |
| target_obj.rotation_mode = 'QUATERNION' | |
| # This is how we convert from a Basis quaternion to a Blender quaternion: | |
| target_obj.rotation_quaternion = mathutils.Quaternion((-w, x, z, -y)) | |
| # Reset the rotation mode. | |
| target_obj.rotation_mode = 'XYZ' | |
| def cancel_edit(op): | |
| global editing_mesh | |
| editing_mesh = False | |
| if blender_sync_lib.cancelEdit() == 0: | |
| op.report({'INFO'}, "Successfully canceled the mesh editing.") | |
| else: | |
| op.report({'ERROR'}, f"Error sending the cancel message to Basis. Is the client connected?") | |
| bpy.ops.wm.read_homefile(use_empty=True) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include "BlenderSyncLibExport.h" | |
| #include <enet/enet.h> | |
| #include <stdio.h> | |
| #include <stdint.h> | |
| #include <string> | |
| #include <fstream> | |
| #include "../Basis/include/editor/BasisBlenderSync.h" | |
| #define BLENDERSYNC_MAX_PATH_LENGTH 2048 | |
| #define BLENDERSYNC_RESPONSE_BUFFER_SIZE 2 * 1024 | |
| // These are the return values of the service() function: | |
| #define BLENDERSYNC_EVENT_NONE 0 | |
| #define BLENDERSYNC_EVENT_CLIENT_CONNECTED 1 | |
| #define BLENDERSYNC_EVENT_CLIENT_DISCONNECTED 2 | |
| #define BLENDERSYNC_EVENT_TARGET_MESH_PATH_RECEIVED 3 | |
| #define BLENDERSYNC_EVENT_TARGET_AND_SURROUNDINGS_MESH_PATH_RECEIVED 4 | |
| // Longer timeouts: | |
| #define BLENDERSYNC_PEER_TIMEOUT_LIMIT 32 | |
| #define BLENDERSYNC_PEER_TIMEOUT_MINIMUM 30000 | |
| #define BLENDERSYNC_PEER_TIMEOUT_MAXIMUM 120000 | |
| // The filesystem-based communication (which admittedly is a bit of a hack) | |
| // can be completely left out by commenting out this define. | |
| //#define BLENDERSYNC_FILESYSTEM_COMMS_ENABLED | |
| // Global state: | |
| ENetHost* gEnetHost = nullptr; | |
| ENetPeer* gEnetClientPeer = nullptr; | |
| #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED | |
| bool gRunningWithFilesystemComms = false; | |
| #endif | |
| uint32_t gEditID = 0xFFFFFFFF; | |
| float gTargetObjectRotationW = 1.0f; | |
| float gTargetObjectRotationX = 0.0f; | |
| float gTargetObjectRotationY = 0.0f; | |
| float gTargetObjectRotationZ = 0.0f; | |
| char gTargetMeshFilePath[BLENDERSYNC_MAX_PATH_LENGTH + 1]; | |
| char gSurroundingsMeshFilePath[BLENDERSYNC_MAX_PATH_LENGTH + 1]; | |
| uint8_t gTempBuffer[BLENDERSYNC_RESPONSE_BUFFER_SIZE]; | |
| #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED | |
| // Returns the path to the filesystem-based communication data file. | |
| std::string getFSCommDataFilePath(bool clientToServer) | |
| { | |
| char tempFolderPath[MAX_PATH]; | |
| GetTempPathA(MAX_PATH, tempFolderPath); | |
| std::string path; | |
| path.append(tempFolderPath); | |
| path.append(clientToServer ? BLENDERSYNC_FS_CLIENT_TO_SERVER_DATA_FILE_NAME : BLENDERSYNC_FS_SERVER_TO_CLIENT_DATA_FILE_NAME); | |
| return path; | |
| } | |
| bool fileExists(const std::string& filePath) | |
| { | |
| bool found = false; | |
| DWORD dwAttrib = GetFileAttributes(filePath.c_str()); | |
| found = (dwAttrib != INVALID_FILE_ATTRIBUTES) && !(dwAttrib & FILE_ATTRIBUTE_DIRECTORY); | |
| return found; | |
| } | |
| // When using filesystem communication, this returns true if the client has sent data to the server. | |
| // After returning true, the data can be found in gTempBuffer. | |
| bool readDataFromClientFS(size_t& dataLength) | |
| { | |
| std::string filePath = getFSCommDataFilePath(true); | |
| if (fileExists(filePath)) | |
| { | |
| std::ifstream file(filePath.c_str(), std::ios::in | std::ios::binary | std::ios::ate); | |
| if (!file.is_open()) | |
| { | |
| return false; | |
| } | |
| dataLength = file.tellg(); | |
| file.seekg(0, std::ios::beg); | |
| file.read((char*)gTempBuffer, dataLength); | |
| file.close(); | |
| DeleteFile(filePath.c_str()); | |
| return true; | |
| } | |
| return false; | |
| } | |
| void writeDataToClientFS(uint8_t* data, size_t dataLength) | |
| { | |
| std::string filePath = getFSCommDataFilePath(false); | |
| std::ofstream outfile(filePath.c_str(), std::ofstream::binary); | |
| if (outfile.fail()) | |
| { | |
| printf("ERROR writing data to file: %s\n", filePath.c_str()); | |
| return; | |
| } | |
| outfile.write((const char*)data, dataLength); | |
| if (outfile.bad()) | |
| { | |
| printf("ERROR writing data to file: %s\n", filePath.c_str()); | |
| return; | |
| } | |
| outfile.close(); | |
| } | |
| #endif // BLENDERSYNC_FILESYSTEM_COMMS_ENABLED | |
| uint32_t readUint(enet_uint8* data, uint32_t index) | |
| { | |
| // This is little-endian: | |
| return data[index] + (data[index + 1] << 8) + (data[index + 2] << 16) + (data[index + 3] << 24); | |
| // This is big-endian: | |
| //return (data[index] << 24) + (data[index + 1] << 16) + (data[index + 2] << 8) + data[index + 3]; | |
| } | |
| void writeUint(enet_uint8* data, uint32_t index, uint32_t value) | |
| { | |
| // This is little-endian: | |
| data[index + 0] = (value) & 0xFF; | |
| data[index + 1] = (value >> 8) & 0xFF; | |
| data[index + 2] = (value >> 16) & 0xFF; | |
| data[index + 3] = (value >> 24) & 0xFF; | |
| } | |
| float readFloat(enet_uint8* data, uint32_t index) | |
| { | |
| union FloatUnion | |
| { | |
| float floatValue; | |
| enet_uint8 bytes[4]; | |
| }; | |
| FloatUnion u; | |
| for (int i = 0; i < 4; ++i) | |
| u.bytes[i] = data[index + i]; | |
| return u.floatValue; | |
| } | |
| int parseData(enet_uint8* data, size_t dataLength) | |
| { | |
| /* | |
| Data protocol: | |
| Byte 0: Data packet type (uint8) | |
| Bytes 1 - 4: Edit ID (uint32) | |
| Bytes 5 - 8: Target object rotation W (float) | |
| Bytes 9 - 12: Target object rotation X (float) | |
| Bytes 13 - 16: Target object rotation Y (float) | |
| Bytes 17 - 20: Target object rotation Z (float) | |
| Bytes 21 - 24: Target mesh path length (uint32) | |
| Bytes: 25 - N: Target mesh path (string data, UTF-8) | |
| Bytes: N+1 - N+4: Surroundings mesh path length(uint32) | |
| Bytes: N+5 - EOD: Surroundings mesh path (string data, UTF-8) | |
| */ | |
| uint8_t dataPacketType = data[0]; | |
| uint32_t cursor = 1; // Move to the second byte. | |
| gEditID = readUint(data, cursor); | |
| cursor += 4; | |
| gTargetObjectRotationW = readFloat(data, cursor); | |
| cursor += 4; | |
| gTargetObjectRotationX = readFloat(data, cursor); | |
| cursor += 4; | |
| gTargetObjectRotationY = readFloat(data, cursor); | |
| cursor += 4; | |
| gTargetObjectRotationZ = readFloat(data, cursor); | |
| cursor += 4; | |
| if (dataPacketType == BLENDERSYNC_DATA_PACKET_TARGET_MESH_PATH || | |
| dataPacketType == BLENDERSYNC_DATA_PACKET_TARGET_AND_SURROUNDINGS_MESH_PATH) | |
| { | |
| // Target mesh path received: | |
| uint32_t pathLength = readUint(data, cursor); | |
| cursor += 4; | |
| if (pathLength > 0) | |
| { | |
| strncpy_s(gTargetMeshFilePath, (const char*)(data + cursor), pathLength); | |
| cursor += pathLength; | |
| } | |
| else | |
| { | |
| strcpy_s(gTargetMeshFilePath, sizeof(gTargetMeshFilePath), ""); | |
| } | |
| if (dataPacketType == BLENDERSYNC_DATA_PACKET_TARGET_MESH_PATH) | |
| { | |
| return BLENDERSYNC_EVENT_TARGET_MESH_PATH_RECEIVED; | |
| } | |
| } | |
| if (dataPacketType == BLENDERSYNC_DATA_PACKET_TARGET_AND_SURROUNDINGS_MESH_PATH) | |
| { | |
| // Target + surroundings mesh path received: | |
| uint32_t pathLength = readUint(data, cursor); | |
| cursor += 4; | |
| strncpy_s(gSurroundingsMeshFilePath, (const char*)(data + cursor), pathLength); | |
| cursor += pathLength; | |
| return BLENDERSYNC_EVENT_TARGET_AND_SURROUNDINGS_MESH_PATH_RECEIVED; | |
| } | |
| return BLENDERSYNC_EVENT_NONE; | |
| } | |
| extern "C" BLENDERSYNCLIBAPI int initBlenderSyncLib() | |
| { | |
| if (enet_initialize() != 0) | |
| { | |
| return 1; // Error initializing ENet. | |
| } | |
| strcpy_s(gTargetMeshFilePath, sizeof(gTargetMeshFilePath), ""); | |
| strcpy_s(gSurroundingsMeshFilePath, sizeof(gSurroundingsMeshFilePath), ""); | |
| return 0; | |
| } | |
| extern "C" BLENDERSYNCLIBAPI void deinitBlenderSyncLib() | |
| { | |
| enet_deinitialize(); | |
| } | |
| extern "C" BLENDERSYNCLIBAPI int startServer(int port) | |
| { | |
| #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED | |
| gRunningWithFilesystemComms = false; | |
| #endif | |
| ENetAddress addr; | |
| addr.host = ENET_HOST_ANY; | |
| addr.port = port; | |
| size_t maxPeerCount = 1; | |
| size_t channelLimit = 0; // 0 means maximum number of allowed channels | |
| gEnetHost = enet_host_create(&addr, maxPeerCount, channelLimit, 0, 0); | |
| if (!gEnetHost) | |
| { | |
| return 1; // Error. | |
| } | |
| return 0; // Success. | |
| } | |
| #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED | |
| extern "C" BLENDERSYNCLIBAPI int startServerFS() | |
| { | |
| gRunningWithFilesystemComms = true; | |
| // Delete any communications data files from a previous session, left in the temp folder. | |
| { | |
| std::string filePath = getFSCommDataFilePath(true); | |
| if (fileExists(filePath)) DeleteFile(filePath.c_str()); | |
| } | |
| { | |
| std::string filePath = getFSCommDataFilePath(false); | |
| if (fileExists(filePath)) DeleteFile(filePath.c_str()); | |
| } | |
| return 0; // Success. | |
| } | |
| #endif | |
| extern "C" BLENDERSYNCLIBAPI void stopServer() | |
| { | |
| #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED | |
| if (!gRunningWithFilesystemComms && !gEnetHost) | |
| #else | |
| if (!gEnetHost) | |
| #endif | |
| { | |
| return; | |
| } | |
| if (gEnetClientPeer) | |
| { | |
| ENetEvent event; | |
| enet_peer_disconnect(gEnetClientPeer, 0); | |
| // Allow up to 3 seconds for the disconnect to succeed and drop any packets received packets. | |
| while (enet_host_service(gEnetHost, &event, 3000) > 0) | |
| { | |
| switch (event.type) | |
| { | |
| case ENET_EVENT_TYPE_RECEIVE: | |
| enet_packet_destroy(event.packet); | |
| break; | |
| case ENET_EVENT_TYPE_DISCONNECT: | |
| gEnetClientPeer = nullptr; | |
| break; | |
| } | |
| } | |
| if (gEnetClientPeer) | |
| { | |
| // We've arrived here, so the disconnect attempt didn't succeed yet. Force the connection down. | |
| enet_peer_reset(gEnetClientPeer); | |
| gEnetClientPeer = nullptr; | |
| } | |
| } | |
| if (gEnetHost) | |
| { | |
| enet_host_destroy(gEnetHost); | |
| gEnetHost = nullptr; | |
| } | |
| #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED | |
| gRunningWithFilesystemComms = false; | |
| #endif | |
| } | |
| extern "C" BLENDERSYNCLIBAPI int isServerRunning() | |
| { | |
| #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED | |
| return (gRunningWithFilesystemComms || gEnetHost != nullptr) ? 1 : 0; | |
| #else | |
| return (gEnetHost != nullptr) ? 1 : 0; | |
| #endif | |
| } | |
| extern "C" BLENDERSYNCLIBAPI float getTargetObjectRotationW() | |
| { | |
| return gTargetObjectRotationW; | |
| } | |
| extern "C" BLENDERSYNCLIBAPI float getTargetObjectRotationX() | |
| { | |
| return gTargetObjectRotationX; | |
| } | |
| extern "C" BLENDERSYNCLIBAPI float getTargetObjectRotationY() | |
| { | |
| return gTargetObjectRotationY; | |
| } | |
| extern "C" BLENDERSYNCLIBAPI float getTargetObjectRotationZ() | |
| { | |
| return gTargetObjectRotationZ; | |
| } | |
| extern "C" BLENDERSYNCLIBAPI const char* getTargetMeshFilePath() | |
| { | |
| return gTargetMeshFilePath; | |
| } | |
| extern "C" BLENDERSYNCLIBAPI const char* getSurroundingsMeshFilePath() | |
| { | |
| return gSurroundingsMeshFilePath; | |
| } | |
| extern "C" BLENDERSYNCLIBAPI int sendUpdatedMeshFilePaths(const char* workFileZipPath, int workFileZipPathLength, const char* binmeshFilePath, int binmeshFilePathLength, bool keepEditing) | |
| { | |
| printf("BlenderSyncLib: workFileZipPath: %s, length: %d\n", workFileZipPath, workFileZipPathLength); | |
| printf("BlenderSyncLib: binmeshFilePath: %s, length: %d\n", binmeshFilePath, binmeshFilePathLength); | |
| #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED | |
| if (gRunningWithFilesystemComms || gEnetClientPeer) | |
| #else | |
| if (gEnetClientPeer) | |
| #endif | |
| { | |
| gTempBuffer[0] = BLENDERSYNC_DATA_PACKET_TARGET_MESH_UPDATED; | |
| uint32_t cursor = 1; // Move to the second byte. | |
| writeUint(gTempBuffer, cursor, gEditID); | |
| cursor += 4; | |
| // One byte specifying whether or not to keep editing. | |
| gTempBuffer[cursor] = keepEditing ? 1 : 0; | |
| cursor += 1; | |
| // Writing the paths prepended with the length as uint32_ts means that we can "get" them out of the stream as Basis strings on the C++ side. | |
| writeUint(gTempBuffer, cursor, (uint32_t)workFileZipPathLength); | |
| cursor += 4; | |
| memcpy_s(gTempBuffer + cursor, BLENDERSYNC_RESPONSE_BUFFER_SIZE - cursor, workFileZipPath, workFileZipPathLength); | |
| cursor += workFileZipPathLength; | |
| writeUint(gTempBuffer, cursor, (uint32_t)binmeshFilePathLength); | |
| cursor += 4; | |
| memcpy_s(gTempBuffer + cursor, BLENDERSYNC_RESPONSE_BUFFER_SIZE - cursor, binmeshFilePath, binmeshFilePathLength); | |
| cursor += binmeshFilePathLength; | |
| #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED | |
| if (gRunningWithFilesystemComms) | |
| { | |
| writeDataToClientFS(gTempBuffer, cursor); | |
| } | |
| else | |
| #endif | |
| { | |
| ENetPacket* packet = enet_packet_create(gTempBuffer, cursor, ENET_PACKET_FLAG_RELIABLE); | |
| enet_peer_send(gEnetClientPeer, 0, packet); | |
| enet_host_flush(gEnetHost); | |
| } | |
| if (!keepEditing) | |
| { | |
| // The edit is officially "over" now, so reset the ID. | |
| gEditID = 0xFFFFFFFF; | |
| } | |
| return 0; | |
| } | |
| return 1; // No peer connected, cannot send. | |
| } | |
| extern "C" BLENDERSYNCLIBAPI int cancelEdit() | |
| { | |
| #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED | |
| if (gRunningWithFilesystemComms || gEnetClientPeer) | |
| #else | |
| if (gEnetClientPeer) | |
| #endif | |
| { | |
| printf("BlenderSyncLib: cancelEdit()\n"); | |
| gTempBuffer[0] = BLENDERSYNC_DATA_PACKET_CANCEL_EDIT; | |
| uint32_t cursor = 1; // Move to the second byte. | |
| writeUint(gTempBuffer, cursor, gEditID); | |
| cursor += 4; | |
| #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED | |
| if (gRunningWithFilesystemComms) | |
| { | |
| writeDataToClientFS(gTempBuffer, cursor); | |
| } | |
| else | |
| #endif | |
| { | |
| ENetPacket* packet = enet_packet_create(gTempBuffer, cursor, ENET_PACKET_FLAG_RELIABLE); | |
| enet_peer_send(gEnetClientPeer, 0, packet); | |
| enet_host_flush(gEnetHost); | |
| } | |
| gEditID = 0xFFFFFFFF; | |
| return 0; | |
| } | |
| return 1; // No peer connected, cannot send. | |
| } | |
| extern "C" BLENDERSYNCLIBAPI int service() | |
| { | |
| #ifdef BLENDERSYNC_FILESYSTEM_COMMS_ENABLED | |
| if (gRunningWithFilesystemComms) | |
| { | |
| size_t dataLength = 0; | |
| if (readDataFromClientFS(dataLength)) | |
| { | |
| printf("BlenderSyncLib: Data received (via filesystem), %u bytes\n", (int)dataLength); | |
| int returnCode = parseData(gTempBuffer, dataLength); | |
| return returnCode; | |
| } | |
| } | |
| else | |
| #endif | |
| if (gEnetHost) | |
| { | |
| ENetEvent enetEvent; | |
| uint32_t timeoutMs = 0; | |
| int res = enet_host_service(gEnetHost, &enetEvent, timeoutMs); | |
| if (res > 0) | |
| { | |
| switch (enetEvent.type) | |
| { | |
| case ENET_EVENT_TYPE_CONNECT: | |
| gEnetClientPeer = enetEvent.peer; | |
| // Set longer timeouts. | |
| enet_peer_timeout(gEnetClientPeer, BLENDERSYNC_PEER_TIMEOUT_LIMIT, | |
| BLENDERSYNC_PEER_TIMEOUT_MINIMUM, BLENDERSYNC_PEER_TIMEOUT_MAXIMUM); | |
| printf("BlenderSyncLib: Client connected.\n"); | |
| return BLENDERSYNC_EVENT_CLIENT_CONNECTED; | |
| case ENET_EVENT_TYPE_DISCONNECT: | |
| gEnetClientPeer = nullptr; | |
| printf("BlenderSyncLib: Client disconnected.\n"); | |
| return BLENDERSYNC_EVENT_CLIENT_DISCONNECTED; | |
| case ENET_EVENT_TYPE_RECEIVE: | |
| { | |
| printf("BlenderSyncLib: Data received, %u bytes\n", (int)enetEvent.packet->dataLength); | |
| int returnCode = parseData(enetEvent.packet->data, enetEvent.packet->dataLength); | |
| enet_packet_destroy(enetEvent.packet); | |
| return returnCode; | |
| } | |
| default: | |
| return BLENDERSYNC_EVENT_NONE; | |
| } | |
| } | |
| else //if (res == 0) | |
| { | |
| return BLENDERSYNC_EVENT_NONE; | |
| } | |
| /*else | |
| { | |
| // A negative value indicates an error. | |
| }*/ | |
| } | |
| return BLENDERSYNC_EVENT_NONE; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment