def handler_on_undo_redo_post(scene, dummy): logger.info("on_undo_redo_post") share_data.set_dirty() share_data.clear_lists() # apply only in object mode if not is_in_object_mode(): return old_objects_name = dict([(k, None) for k in share_data.old_objects.keys() ]) # value not needed remap_objects_info() for k, v in share_data.old_objects.items(): if k in old_objects_name: old_objects_name[k] = v update_object_state(old_objects_name, share_data.old_objects) update_collections_state() update_scenes_state() remove_objects_from_scenes() remove_objects_from_collections() remove_collections_from_scenes() remove_collections_from_collections() remove_collections() remove_scenes() add_scenes() add_objects() add_collections() add_collections_to_scenes() add_collections_to_collections() add_objects_to_collections() add_objects_to_scenes() update_collections_parameters() create_vrtist_objects() delete_scene_objects() rename_objects() update_objects_visibility() update_objects_transforms() reparent_objects() # send selection content (including data) materials = set() for obj in bpy.context.selected_objects: update_transform(obj) if hasattr(obj, "data"): update_params(obj) if hasattr(obj, "material_slots"): for slot in obj.material_slots[:]: materials.add(slot.material) for material in materials: share_data.client.send_material(material) share_data.update_current_data()
def network_consumer(self): """ This method can be considered the entry point of this class. It is meant to be called regularly to send pending commands to the server, and receive then process new ones. Pending commands are accumulated with add_command(), most calls originate from handlers function. Incoming commands are read from the socket and directly processed here to update Blender's data. This can be costly and a possible optimization in the future would be to split the processing accross several timer run. This can be challenging because we need to keep the current update state. Maybe this can be solved naturally with coroutines. We call it from the timer registered by the addon. """ from mixer.bl_panels import redraw as redraw_panels, update_ui_lists assert self.is_connected() set_draw_handlers() # Loop remains infinite while we have GROUP_BEGIN commands without their corresponding GROUP_END received # todo Change this -> probably not a good idea because the sending client might disconnect before GROUP_END occurs # or it needs to be guaranteed by the server group_count = 0 while True: received_commands = self.fetch_commands(get_mixer_prefs().commands_send_interval) set_dirty = True # Process all received commands for command in received_commands: if self._joining and command.type.value > common.MessageType.COMMAND.value: self._received_byte_size += command.byte_size() self._received_command_count += 1 if self._joining_room_name in self.rooms_attributes: get_mixer_props().joining_percentage = ( self._received_byte_size / self.rooms_attributes[self._joining_room_name][RoomAttributes.BYTE_SIZE] ) redraw_panels() if command.type == MessageType.GROUP_BEGIN: group_count += 1 continue if command.type == MessageType.GROUP_END: group_count -= 1 continue if self.has_default_handler(command.type): if command.type == MessageType.JOIN_ROOM and self._joining: self._joining = False get_mixer_props().joining_percentage = 1 update_ui_lists() self.block_signals = False # todo investigate why we should but this to false here continue if set_dirty: share_data.set_dirty() set_dirty = False self.block_signals = True try: # manage wrapped commands with this blender id # time synced command for now # Consume messages with its client_id to receive commands from other clients # like play/pause. Ignore all other client_id. if command.type == MessageType.CLIENT_ID_WRAPPER: id, index = common.decode_string(command.data, 0) if id != share_data.client.client_id: continue command_type, index = common.decode_int(command.data, index) command_data = command.data[index:] command = common.Command(command_type, command_data) if command.type == MessageType.CONTENT: # The server asks for scene content (at room creation) try: assert share_data.client.current_room is not None self.set_room_attributes( share_data.client.current_room, # Documentation to update if you change "experimental_sync": doc/protocol.md {"experimental_sync": get_mixer_prefs().experimental_sync}, ) send_scene_content() # Inform end of content self.add_command(common.Command(MessageType.CONTENT)) except Exception as e: raise SendSceneContentFailed() from e continue # Put this to true by default # todo Check build commands that do not trigger depsgraph update # because it can lead to ignoring real updates when a false positive is encountered command_triggers_depsgraph_update = True if command.type == MessageType.GREASE_PENCIL_MESH: grease_pencil_api.build_grease_pencil_mesh(command.data) elif command.type == MessageType.GREASE_PENCIL_MATERIAL: grease_pencil_api.build_grease_pencil_material(command.data) elif command.type == MessageType.GREASE_PENCIL_CONNECTION: grease_pencil_api.build_grease_pencil_connection(command.data) elif command.type == MessageType.CLEAR_CONTENT: clear_scene_content() self._joining = True self._received_command_count = 0 self._received_byte_size = 0 get_mixer_props().joining_percentage = 0 redraw_panels() elif command.type == MessageType.MESH: self.build_mesh(command.data) elif command.type == MessageType.TRANSFORM: self.build_transform(command.data) elif command.type == MessageType.MATERIAL: material_api.build_material(command.data) elif command.type == MessageType.ASSIGN_MATERIAL: material_api.build_assign_material(command.data) elif command.type == MessageType.DELETE: self.build_delete(command.data) elif command.type == MessageType.CAMERA: camera_api.build_camera(command.data) elif command.type == MessageType.LIGHT: light_api.build_light(command.data) elif command.type == MessageType.RENAME: self.build_rename(command.data) elif command.type == MessageType.DUPLICATE: self.build_duplicate(command.data) elif command.type == MessageType.SEND_TO_TRASH: self.build_send_to_trash(command.data) elif command.type == MessageType.RESTORE_FROM_TRASH: self.build_restore_from_trash(command.data) elif command.type == MessageType.TEXTURE: self.build_texture_file(command.data) elif command.type == MessageType.COLLECTION: collection_api.build_collection(command.data) elif command.type == MessageType.COLLECTION_REMOVED: collection_api.build_collection_removed(command.data) elif command.type == MessageType.INSTANCE_COLLECTION: collection_api.build_collection_instance(command.data) elif command.type == MessageType.ADD_COLLECTION_TO_COLLECTION: collection_api.build_collection_to_collection(command.data) elif command.type == MessageType.REMOVE_COLLECTION_FROM_COLLECTION: collection_api.build_remove_collection_from_collection(command.data) elif command.type == MessageType.ADD_OBJECT_TO_COLLECTION: collection_api.build_add_object_to_collection(command.data) elif command.type == MessageType.REMOVE_OBJECT_FROM_COLLECTION: collection_api.build_remove_object_from_collection(command.data) elif command.type == MessageType.ADD_COLLECTION_TO_SCENE: scene_api.build_collection_to_scene(command.data) elif command.type == MessageType.REMOVE_COLLECTION_FROM_SCENE: scene_api.build_remove_collection_from_scene(command.data) elif command.type == MessageType.ADD_OBJECT_TO_SCENE: scene_api.build_add_object_to_scene(command.data) elif command.type == MessageType.REMOVE_OBJECT_FROM_SCENE: scene_api.build_remove_object_from_scene(command.data) elif command.type == MessageType.SCENE: scene_api.build_scene(command.data) elif command.type == MessageType.SCENE_REMOVED: scene_api.build_scene_removed(command.data) elif command.type == MessageType.SCENE_RENAMED: scene_api.build_scene_renamed(command.data) elif command.type == MessageType.OBJECT_VISIBILITY: object_api.build_object_visibility(command.data) elif command.type == MessageType.FRAME: self.build_frame(command.data) elif command.type == MessageType.QUERY_CURRENT_FRAME: self.query_current_frame() elif command.type == MessageType.PLAY: self.build_play(command.data) elif command.type == MessageType.PAUSE: self.build_pause(command.data) elif command.type == MessageType.ADD_KEYFRAME: self.build_add_keyframe(command.data) elif command.type == MessageType.REMOVE_KEYFRAME: self.build_remove_keyframe(command.data) elif command.type == MessageType.QUERY_OBJECT_DATA: self.build_query_object_data(command.data) elif command.type == MessageType.CLEAR_ANIMATIONS: self.build_clear_animations(command.data) elif command.type == MessageType.SHOT_MANAGER_MONTAGE_MODE: self.build_montage_mode(command.data) elif command.type == MessageType.SHOT_MANAGER_ACTION: shot_manager.build_shot_manager_action(command.data) elif command.type == MessageType.BLENDER_DATA_UPDATE: data_api.build_data_update(command.data) elif command.type == MessageType.BLENDER_DATA_REMOVE: data_api.build_data_remove(command.data) else: # Command is ignored, so no depsgraph update can be triggered command_triggers_depsgraph_update = False if command_triggers_depsgraph_update: self.skip_next_depsgraph_update = True except Exception as e: logger.warning(f"Exception during processing of message {str(command.type)}") log_traceback(logger.warning) if get_mixer_prefs().env == "development" or isinstance(e, SendSceneContentFailed): raise finally: self.block_signals = False if group_count == 0: break if not set_dirty: share_data.update_current_data() # Some objects may have been obtained before their parent # In that case we resolve parenting here # todo Parenting strategy should be changed: we should store the name of the parent in the command instead of # having a path as name if len(share_data.pending_parenting) > 0: remaining_parentings = set() for path in share_data.pending_parenting: path_elem = path.split("/") ob = None parent = None for elem in path_elem: ob = share_data.blender_objects.get(elem) if not ob: remaining_parentings.add(path) break if ob.parent != parent: # do it only if needed, otherwise it resets matrix_parent_inverse ob.parent = parent parent = ob share_data.pending_parenting = remaining_parentings self.set_client_attributes(self.compute_client_custom_attributes())
def handler_on_undo_redo_post(scene, dummy): logger.error(f"Undo/redo post on {scene}") share_data.client.send_error( f"Undo/redo post from {get_mixer_prefs().user}") if not share_data.use_vrtist_protocol(): # Generic sync: reload all datablocks undone = share_data.bpy_data_proxy.snapshot_undo_post() logger.warning(f"undone uuids : {undone}") share_data.bpy_data_proxy.reload_datablocks() else: share_data.set_dirty() share_data.clear_lists() # apply only in object mode if not is_in_object_mode(): return old_objects_name = dict([ (k, None) for k in share_data.old_objects.keys() ]) # value not needed remap_objects_info() for k, v in share_data.old_objects.items(): if k in old_objects_name: old_objects_name[k] = v update_object_state(old_objects_name, share_data.old_objects) update_collections_state() update_scenes_state() remove_objects_from_scenes() remove_objects_from_collections() remove_collections_from_scenes() remove_collections_from_collections() remove_collections() add_scenes() add_objects() add_collections() add_collections_to_scenes() add_collections_to_collections() add_objects_to_collections() add_objects_to_scenes() update_collections_parameters() create_vrtist_objects() delete_scene_objects() rename_objects() update_objects_visibility() update_objects_constraints() update_objects_transforms() reparent_objects() # send selection content (including data) materials = set() for obj in bpy.context.selected_objects: update_transform(obj) if hasattr(obj, "data"): update_params(obj) if hasattr(obj, "material_slots"): for slot in obj.material_slots[:]: materials.add(slot.material) for material in materials: share_data.client.send_material(material) share_data.update_current_data()
def send_scene_data_to_server(scene, dummy): logger.debug( "send_scene_data_to_server(): skip_next_depsgraph_update %s, pending_test_update %s", share_data.client.skip_next_depsgraph_update, share_data.pending_test_update, ) if not share_data.client: logger.info("send_scene_data_to_server canceled (no client instance)") return share_data.set_dirty() share_data.clear_lists() depsgraph = bpy.context.evaluated_depsgraph_get() if depsgraph.updates: logger.debug("Current dg updates ...") for update in depsgraph.updates: logger.debug(" ......%s", update.id.original) # prevent processing self events, but always process test updates if not share_data.pending_test_update and share_data.client.skip_next_depsgraph_update: share_data.client.skip_next_depsgraph_update = False logger.debug( "send_scene_data_to_server canceled (skip_next_depsgraph_update = True) ..." ) return share_data.pending_test_update = False if not is_in_object_mode(): if depsgraph.updates: logger.info( "send_scene_data_to_server canceled (not is_in_object_mode). Skipping updates" ) for update in depsgraph.updates: logger.info(" ......%s", update.id.original) return update_object_state(share_data.old_objects, share_data.blender_objects) update_scenes_state() update_collections_state() changed = False changed |= remove_objects_from_collections() changed |= remove_objects_from_scenes() changed |= remove_collections_from_collections() changed |= remove_collections_from_scenes() changed |= remove_collections() changed |= add_scenes() changed |= add_collections() changed |= add_objects() changed |= update_transforms() changed |= add_collections_to_scenes() changed |= add_collections_to_collections() changed |= add_objects_to_collections() changed |= add_objects_to_scenes() changed |= update_collections_parameters() changed |= create_vrtist_objects() changed |= delete_scene_objects() changed |= rename_objects() changed |= update_objects_visibility() changed |= update_objects_constraints() changed |= update_objects_transforms() changed |= reparent_objects() changed |= shot_manager.check_montage_mode() if not changed: update_objects_data() # update for next change share_data.update_current_data() logger.debug("send_scene_data_to_server: end")
def send_scene_data_to_server(scene, dummy): logger.debug( "send_scene_data_to_server(): skip_next_depsgraph_update %s, pending_test_update %s", share_data.client.skip_next_depsgraph_update, share_data.pending_test_update, ) timer = share_data.current_stats_timer if not share_data.client: logger.info("send_scene_data_to_server canceled (no client instance)") return share_data.set_dirty() with timer.child("clear_lists"): share_data.clear_lists() depsgraph = bpy.context.evaluated_depsgraph_get() if depsgraph.updates: logger.debug("Current dg updates ...") for update in depsgraph.updates: logger.debug(" ......%s", update.id.original) # prevent processing self events, but always process test updates if not share_data.pending_test_update and share_data.client.skip_next_depsgraph_update: share_data.client.skip_next_depsgraph_update = False logger.debug("send_scene_data_to_server canceled (skip_next_depsgraph_update = True) ...") return share_data.pending_test_update = False if not is_in_object_mode(): logger.info("send_scene_data_to_server canceled (not is_in_object_mode)") return update_object_state(share_data.old_objects, share_data.blender_objects) with timer.child("update_scenes_state"): update_scenes_state() with timer.child("update_collections_state"): update_collections_state() changed = False with timer.child("checkForChangeAndSendUpdates"): changed |= remove_objects_from_collections() changed |= remove_objects_from_scenes() changed |= remove_collections_from_collections() changed |= remove_collections_from_scenes() changed |= remove_collections() changed |= remove_scenes() changed |= add_scenes() changed |= add_collections() changed |= add_objects() # Updates from the VRtist protocol and from the full Blender protocol must be cafully intermixed # This is an unfortunate requirements from the current coexistence status of # both protocols # After creation of meshes : meshes are not yet supported by full Blender protocol, # but needed to properly create objects # Before creation of objects : the VRtint protocol will implicitely create objects with # unappropriate default values (e.g. transform creates an object with no data) if share_data.use_experimental_sync(): # Compute the difference between the proxy state and the Blender state # It is a coarse difference at the ID level(created, removed, renamed) diff = BpyBlendDiff() diff.diff(share_data.proxy, safe_context) # Ask the proxy to compute the list of elements to synchronize and update itself depsgraph = bpy.context.evaluated_depsgraph_get() updates, removals = share_data.proxy.update(diff, safe_context, depsgraph.updates) # Send the data update messages (includes serialization) data_api.send_data_removals(removals) data_api.send_data_updates(updates) share_data.proxy.debug_check_id_proxies() # send the VRtist transforms after full Blender protocol has the opportunity to create the object data # that is not handled by VRtist protocol, otherwise the receiver creates an empty when it receives a transform changed |= update_transforms() changed |= add_collections_to_scenes() changed |= add_collections_to_collections() changed |= add_objects_to_collections() changed |= add_objects_to_scenes() changed |= update_collections_parameters() changed |= create_vrtist_objects() changed |= delete_scene_objects() changed |= rename_objects() changed |= update_objects_visibility() changed |= update_objects_transforms() changed |= reparent_objects() changed |= shot_manager.check_montage_mode() if not changed: with timer.child("update_objects_data"): update_objects_data() # update for next change with timer.child("update_current_data"): share_data.update_current_data() logger.debug("send_scene_data_to_server: end")