def load( self, bl_collection: bpy.types.bpy_prop_collection, attr_name: str, attr_property: T.Property, context: Context, ): """ - bl_collection: a collection of structure, e.g. a SplineBezierPoints instance - attr_name: the name of the member to load, such as "handle_left_type" """ for index, item in enumerate(bl_collection): self._data[index] = read_attribute(getattr(item, attr_name), index, attr_property, bl_collection, context) try: if not isinstance(self._data[0], str): logger.error( f"unsupported type for {bl_collection}[{attr_name}]: {type(self._data[0])}" ) except KeyError: pass return self
def load( self, datablock: T.ID, context: Context, ) -> DatablockProxy: """Load a datablock into this proxy. Args: datablock: the embedded datablock to load into this proxy context: visit and proxy state Returns: this DatablockProxy """ self.clear_data() self._has_datablock = True properties = context.synchronized_properties.properties(datablock) # this assumes that specifics.py apply only to ID, not Struct properties = specifics.conditional_properties(datablock, properties) with context.visit_state.enter_datablock(self, datablock): for name, bl_rna_property in properties: attr = getattr(datablock, name) attr_value = read_attribute(attr, name, bl_rna_property, context) # Also write None values to reset attributes like Camera.dof.focus_object # TODO for scene, test difference, only send update if dirty as continuous updates to scene # master collection will conflicting writes with Master Collection self._data[name] = attr_value self.attach_filepath_raw(datablock) self.attach_media_descriptor(datablock, context) self._custom_properties.load(datablock) return self
def load(self, attribute: T.bpy_struct, key: Union[int, str], context: Context) -> StructProxy: """ Load the attribute Blender struct into this proxy Args: attribute: the Blender struct to load into this proxy, (e.g an ObjectDisplay instance) key: the identifier of attribute in its parent (e.g. "display") context: the proxy and visit state """ self.clear_data() properties = context.synchronized_properties.properties(attribute) # includes properties from the bl_rna only, not the "view like" properties like MeshPolygon.edge_keys # that we do not want to load anyway properties = specifics.conditional_properties(attribute, properties) context.visit_state.path.append(key) try: for name, bl_rna_property in properties: attr = getattr(attribute, name) attr_value = read_attribute(attr, name, bl_rna_property, context) self._data[name] = attr_value finally: context.visit_state.path.pop() return self
def load( self, bl_instance: T.ID, key: str, context: Context, bpy_data_collection_name: str = None, ): """ Load a datablock into this proxy """ if bl_instance.is_embedded_data and bpy_data_collection_name is not None: logger.error( f"DatablockProxy.load() for {bl_instance} : is_embedded_data is True and bpy_prop_collection is {bpy_data_collection_name}. Item ignored" ) return if bl_instance.is_embedded_data: self._bpy_data_collection = None if bpy_data_collection_name is not None: self._bpy_data_collection = bpy_data_collection_name self._class_name = bl_instance.__class__.__name__ self._data.clear() properties = context.synchronized_properties.properties(bl_instance) # this assumes that specifics.py apply only to ID, not Struct properties = specifics.conditional_properties(bl_instance, properties) try: context.visit_state.datablock_proxy = self for name, bl_rna_property in properties: attr = getattr(bl_instance, name) attr_value = read_attribute(attr, name, bl_rna_property, context) # Also write None values to reset attributes like Camera.dof.focus_object # TODO for scene, test difference, only send update if dirty as continuous updates to scene # master collection will conflicting writes with Master Collection self._data[name] = attr_value finally: context.visit_state.datablock_proxy = None specifics.post_save_id(self, bl_instance) uuid = bl_instance.get("mixer_uuid") if uuid: # It is a bpy.data ID, not an ID "embedded" inside another ID, like scene.collection id_ = context.proxy_state.datablocks.get(uuid) if id_ is not bl_instance: # this occurs when # - when we find a reference to a BlendData ID that was not loaded # - the ID are not properly ordred at creation time, for instance (objects, meshes) # instead of (meshes, objects) : a bug logger.debug( "DatablockProxy.load(): %s not in context.proxy_state.datablocks[uuid]", bl_instance) self._datablock_uuid = bl_instance.mixer_uuid context.proxy_state.proxies[uuid] = self self.attach_media_descriptor(bl_instance) return self
def diff(self, collection: T.bpy_prop_collection, key: str, collection_property: T.Property, context: Context) -> Optional[DeltaUpdate]: """ Computes the difference between the state of an item tracked by this proxy and its Blender state. As this proxy tracks a collection, the result will be a DeltaUpdate that contains a DatablockCollectionProxy with an Delta item per added, deleted or update item Args: collection: the collection diff against this proxy collection_property: the property of collection in its enclosing object """ # This method is called from the depsgraph handler. The proxy holds a representation of the Blender state # before the modification being processed. So the changeset is (Blender state - proxy state) # TODO how can this replace BpyBlendDiff ? diff = self.__class__() item_property = collection_property.fixed_type # keys are uuids # BpyDataCollectionDiff.diff() for why proxies without datablocks are ignores proxy_keys = {k for k, v in self._data.items() if v.target(context)} blender_items = { datablock.mixer_uuid: datablock for datablock in collection.values() } blender_keys = blender_items.keys() added_keys = blender_keys - proxy_keys deleted_keys = proxy_keys - blender_keys maybe_updated_keys = proxy_keys & blender_keys for k in added_keys: value = read_attribute(blender_items[k], k, item_property, collection, context) assert isinstance(value, (DatablockProxy, DatablockRefProxy)) diff._data[k] = DeltaAddition(value) for k in deleted_keys: diff._data[k] = DeltaDeletion(self._data[k]) for k in maybe_updated_keys: delta = diff_attribute(blender_items[k], k, item_property, self.data(k), context) if delta is not None: assert isinstance(delta, DeltaUpdate) diff._data[k] = delta if len(diff._data): return DeltaUpdate(diff) return None
def diff( self, collection: T.bpy_prop_collection, key: Union[int, str], collection_property: T.Property, context: Context ) -> Optional[Union[DeltaUpdate, DeltaReplace]]: """ Computes the difference between the state of an item tracked by this proxy and its Blender state. This proxy tracks a collection of items indexed by string (e.g Scene.render.views) or int. The result will be a ProxyDiff that contains a Delta item per added, deleted or updated item Args: collection; the collection that must be diffed agains this proxy key: the name of the collection, to record in the visit path collection_property; the property os collection as found in its enclosing object """ sequence = self._sequence if len(sequence) == 0 and len(collection) == 0: return None if specifics.diff_must_replace(collection, sequence, collection_property): # A collection cannot be updated because either: # - some of its members cannot be updated : # SplineBezierPoints has no API to remove points, so Curve.splines cannot be update and must be replaced # - updating the name of members will cause unsolicited renames. # When swapping layers A and B in a GreasePencilLayers, renaming layer 0 into B cause an unsolicited # rename of layer 0 into B.001 # Send a replacement for the whole collection self.load(collection, context) return DeltaReplace(self) else: item_property = collection_property.fixed_type diff = self.__class__() # items from clear_from index cannot be updated, most often because eir type has changed (e.g # ObjectModifier) clear_from = specifics.clear_from(collection, sequence, context) # run a diff for the head, that can be updated in-place for i in range(clear_from): delta = diff_attribute(collection[i], i, item_property, sequence[i], context) if delta is not None: diff._diff_updates.append((i, delta)) if specifics.can_resize(collection, context): # delete the existing tail that cannot be modified diff._diff_deletions = len(sequence) - clear_from # add the new tail for i, item in enumerate(collection[clear_from:], clear_from): value = read_attribute(item, i, item_property, collection, context) diff._diff_additions.append(DeltaAddition(value)) if diff._diff_updates or diff._diff_deletions or diff._diff_additions: return DeltaUpdate(diff) return None
def diff( self, container: Union[T.bpy_prop_collection, T.Struct], key: Union[str, int], prop: T.Property, context: Context, ) -> Optional[DeltaUpdate]: attr = read_attribute(container, key, prop, None, context) if isinstance(attr, NonePtrProxy): return None return DeltaReplace(attr)
def diff(self, datablock: T.ID, key: Union[int, str], datablock_property: T.Property, context: Context) -> Optional[DeltaReplace]: """ Computes the difference between this proxy and its Blender state. """ if datablock is None: return DeltaReplace(DatablockRefProxy()) value = read_attribute(datablock, key, datablock_property, None, context) assert isinstance(value, DatablockRefProxy) if value._datablock_uuid != self._datablock_uuid: return DeltaReplace(value) else: return None
def load( self, bl_collection: bpy.types.bpy_prop_collection, attr_name: str, attr_property: T.Property, context: Context, ): """ - bl_collection: a collection of structure, e.g. T.Mesh.vertices - item_bl_rna: the bl_rna if the structure contained in the collection, e.g. T.MeshVertices.bl_rna - attr_name: a member if the structure to be loaded as a sequence, e.g. "groups" """ for index, item in enumerate(bl_collection): self._data[index] = read_attribute(getattr(item, attr_name), index, attr_property, context) return self
def load(self, bl_instance: Any, parent_key: Union[int, str], context: Context): """ Load a Blender object into this proxy """ self._data.clear() properties = context.synchronized_properties.properties(bl_instance) # includes properties from the bl_rna only, not the "view like" properties like MeshPolygon.edge_keys # that we do not want to load anyway properties = specifics.conditional_properties(bl_instance, properties) try: context.visit_state.path.append(parent_key) for name, bl_rna_property in properties: attr = getattr(bl_instance, name) attr_value = read_attribute(attr, name, bl_rna_property, context) # Also write None values. We use them to reset attributes like Camera.dof.focus_object self._data[name] = attr_value finally: context.visit_state.path.pop() return self
def _read_attribute(): self._data["edit_bones"] = read_attribute( armature_data.edit_bones, "edit_bones", self._edit_bones_property, armature_data, context)
def diff( self, collection: T.bpy_prop_collection, key: Union[int, str], collection_property: T.Property, context: Context ) -> Optional[DeltaUpdate]: """ Computes the difference between the state of an item tracked by this proxy and its Blender state. This proxy tracks a collection of items indexed by string (e.g Scene.render.views) or int. The result will be a ProxyDiff that contains a Delta item per added, deleted or updated item Args: collection; the collection that must be diffed agains this proxy collection_property; the property os collection as found in its enclosing object """ diff = self.__class__() item_property = collection_property.fixed_type try: context.visit_state.path.append(key) sequence = self._data.get(MIXER_SEQUENCE) if sequence: # indexed by int # TODO This produces one DeltaDeletion by removed item. Produce a range in case may items are # deleted # since the diff sequence is hollow, we cannot store it in a list. Use a dict with int keys instead for i, (proxy_value, blender_value) in enumerate(itertools.zip_longest(sequence, collection)): if proxy_value is None: value = read_attribute(collection[i], i, item_property, context) diff._data[i] = DeltaAddition(value) elif blender_value is None: diff._data[i] = DeltaDeletion(self.data(i)) else: delta = diff_attribute(collection[i], i, item_property, proxy_value, context) if delta is not None: diff._data[i] = delta else: # index by string. This is similar to DatablockCollectionProxy.diff # Renames are detected as Deletion + Addition # This assumes that keys ordring is the same in the proxy and in blender, which is # guaranteed by the fact that proxy load uses SynchronizedProperties.properties() bl_rna = getattr(collection, "bl_rna", None) if bl_rna is not None and isinstance( bl_rna, (type(T.ObjectModifiers.bl_rna), type(T.ObjectGpencilModifiers)) ): # TODO move this into specifics.py # order-dependant collections with different types like Modifiers proxy_names = list(self._data.keys()) blender_names = collection.keys() proxy_types = [self.data(name).data("type") for name in proxy_names] blender_types = [collection[name].type for name in blender_names] if proxy_types == blender_types and proxy_names == blender_names: # Same types and names : do sparse modification for name in proxy_names: delta = diff_attribute(collection[name], name, item_property, self.data(name), context) if delta is not None: diff._data[name] = delta else: # names or types do not match, rebuild all # There are name collisions during Modifier order change for instance, so prefix # the names to avoid them (using a tuple fails in the json encoder) for name in proxy_names: diff._data["D" + name] = DeltaDeletion(self.data(name)) for name in blender_names: value = read_attribute(collection[name], name, item_property, context) diff._data["A" + name] = DeltaAddition(value) else: # non order dependant, uniform types proxy_keys = self._data.keys() blender_keys = collection.keys() added_keys = blender_keys - proxy_keys for k in added_keys: value = read_attribute(collection[k], k, item_property, context) diff._data["A" + k] = DeltaAddition(value) deleted_keys = proxy_keys - blender_keys for k in deleted_keys: diff._data["D" + k] = DeltaDeletion(self.data(k)) maybe_updated_keys = proxy_keys & blender_keys for k in maybe_updated_keys: delta = diff_attribute(collection[k], k, item_property, self.data(k), context) if delta is not None: diff._data[k] = delta finally: context.visit_state.path.pop() if len(diff._data): return DeltaUpdate(diff) return None