def save(self, collection: T.bpy_prop_collection, parent: T.bpy_struct, key: str, context: Context): """ Saves this proxy into collection Args: collection: a collection of datablock references with link/unlink interface (e.g a_Collection_instance.objects) parent: the structure that contains collection to be loaded (e.g. a Collection instance) key: the name of the bpy_collection (e.g "objects") context: """ for _, ref_proxy in self._data.items(): assert isinstance(ref_proxy, DatablockRefProxy) datablock = ref_proxy.target(context) if datablock: collection.link(datablock) else: logger.info( f"unresolved reference {parent}.{key} -> {ref_proxy.display_string} {ref_proxy.mixer_uuid}" ) add_element = collection.link context.proxy_state.unresolved_refs.append( ref_proxy.mixer_uuid, add_element, f"{collection!r}.link({ref_proxy.display_string})")
def add_element(collection: T.bpy_prop_collection, proxy: Proxy, index: int, context: Context): """Add an element to a bpy_prop_collection using the collection specific API.s""" if hasattr(collection, "add"): # either a bpy_prop_collection with an rna or a bpy_prop_collection_idprop try: collection.add() return except Exception as e: logger.error( f"add_element: call to add() failed for {context.visit_state.display_path()} ..." ) logger.error(f"... {e!r}") raise AddElementFailed from None if not hasattr(collection, "bl_rna"): # a bpy.types.bpy_prop_collection, e.g Pose.bones # We should not even attempt to add elements in these collections since they do not allow it at all. # However bpy_prop_collection and collections with an rna both managed by StructCollectionProxy. We need # proxy update to update the contents of existing elements, but it should not attempt to add/remove elements. # As a consequence, for attributes that fall into this category we trigger updates with additions and # deletions that are meaningless. Ignore them. # The right design could be to have different proxies for bpy_prop_collection and bpy_struct that behave like # collections. # see Proxy construction in read_attribute() return
def _add_element_sequence(collection: T.bpy_prop_collection, proxy: Proxy, context: Context): type_name = proxy.data("type") name = proxy.data("name") channel = proxy.data("channel") frame_start = proxy.data("frame_start") if type_name in _effect_sequences: # overwritten anyway frame_end = frame_start + 1 return collection.new_effect(name, type_name, channel, frame_start, frame_end=frame_end) if type_name == "SOUND": sound = proxy.data("sound") target = sound.target(context) if not target: logger.warning( f"missing target ID block for bpy.data.{sound.collection}[{sound.key}] " ) return None filepath = target.filepath return collection.new_sound(name, filepath, channel, frame_start) if type_name == "MOVIE": filepath = proxy.data("filepath") return collection.new_movie(name, filepath, channel, frame_start) if type_name == "IMAGE": directory = proxy.data("directory") filename = proxy.data("elements").data(0).data("filename") filepath = str(Path(directory) / filename) return collection.new_image(name, filepath, channel, frame_start) logger.warning(f"Sequence type not implemented: {type_name}") return None
def _truncate_collection_remove(collection: T.bpy_prop_collection, size: int): try: while len(collection) > size: collection.remove(collection[-1]) except Exception as e: logger.error(f"truncate_collection {collection}: exception ...") logger.error(f"... {e!r}")
def diff(self, aos: T.bpy_prop_collection, key: str, prop: T.Property, context: Context) -> Optional[DeltaUpdate]: if len(aos) == 0: return None array_size, member_type = self.array_attr(aos, self._member_name, prop.bl_rna) typecode = self._array.typecode tmp_array = array.array(typecode, soa_initializer(member_type, array_size)) if logger.isEnabledFor(logging.DEBUG): message = ( f"diff {aos}.{self._member_name} proxy({len(self._array)} {typecode}) blender'{len(aos)} {member_type}'" ) logger.debug(message) try: aos.foreach_get(self._member_name, tmp_array) except RuntimeError as e: logger.error(f"diff soa {aos}.{self._member_name} failed") logger.error( f"... member size: {len(aos)}, tmp_array: ('{tmp_array.typecode}', {len(tmp_array)})" ) logger.error(f"... exception {e!r}") if self._array == tmp_array: return None diff = self.__class__() diff._member_name = self._member_name diff._array = tmp_array diff._attach(context) return DeltaUpdate(diff)
def add_element(collection: T.bpy_prop_collection, proxy: Proxy, context: Context): """Add an element to a bpy_prop_collection using the collection specific API""" try: collection.add() except Exception: logger.error(f"add_element: failed for {collection}")
def load( self, bl_collection: T.bpy_prop_collection, key: Union[int, str], bl_collection_property: T.Property, context: Context, ): if len(bl_collection) == 0: self._data.clear() return self try: context.visit_state.path.append(key) # no keys means it is a sequence. However bl_collection.items() returns [(index, item)...] is_sequence = not bl_collection.keys() if is_sequence: # easier for the encoder to always have a dict self._data = { MIXER_SEQUENCE: [ StructProxy.make(v).load(v, i, context) for i, v in enumerate(bl_collection.values()) ] } else: self._data = {k: StructProxy().load(v, k, context) for k, v in bl_collection.items()} finally: context.visit_state.path.pop() return self
def add_element(collection: T.bpy_prop_collection, proxy: Proxy, index: int, context: Context): """Add an element to a bpy_prop_collection using the collection specific API""" try: collection.bl_rna except AttributeError: return try: collection.add() except Exception: logger.error(f"add_element: failed for {collection}")
def remove_datablock(collection: T.bpy_prop_collection, datablock: T.ID): """Delete a datablock from its bpy.data collection""" if isinstance(datablock, T.Scene): from mixer.blender_client.scene import delete_scene delete_scene(datablock) elif isinstance(datablock, T.Key): # the doc labels it unsafe, use sparingly bpy.data.batch_remove([datablock]) elif isinstance(datablock, T.Library): # TODO 2.91 has BlendDatalibraries.remove() logger.warning(f"remove_datablock({datablock}): ignored (library)") else: collection.remove(datablock)
def add_datablock_ref_element(collection: T.bpy_prop_collection, datablock: T.ID): """Add an element to a bpy_prop_collection using the collection specific API""" bl_rna = getattr(collection, "bl_rna", None) if bl_rna is not None: if isinstance(bl_rna, _link_collections): collection.link(datablock) return if isinstance(bl_rna, type(T.IDMaterials.bl_rna)): collection.append(datablock) return logging.warning( f"add_datablock_ref_element : no implementation for {collection} ")
def _add_element_default(collection: T.bpy_prop_collection, proxy: Proxy, context: Context): try: return collection.add() except Exception: pass # try our best new_or_add = getattr(collection, "new", None) if new_or_add is None: new_or_add = getattr(collection, "add", None) if new_or_add is None: logger.warning(f"Not implemented new or add for {collection} ...") return None try: return new_or_add() except TypeError: try: key = proxy.data("name") return new_or_add(key) except Exception: logger.warning( f"Not implemented new or add for type {type(collection)} for {collection}[{key}] ..." ) for s in traceback.format_exc().splitlines(): logger.warning(f"...{s}") return None
def save_array(self, aos: T.bpy_prop_collection, member_name, array_: array.array): if logger.isEnabledFor(logging.DEBUG): message = f"save_array {aos}.{member_name}" if self._array is not None: message += f" proxy ({len(self._array)} {self._array.typecode})" message += f" incoming ({len(array_)} {array_.typecode})" message += f" blender_length ({len(aos)})" logger.debug(message) self._array = array_ try: aos.foreach_set(member_name, array_) except RuntimeError as e: logger.error(f"saving soa {aos}.{member_name} failed") logger.error(f"... member size: {len(aos)}, array: ('{array_.typecode}', {len(array_)})") logger.error(f"... exception {e!r}")
def truncate_collection(target: T.bpy_prop_collection, incoming_keys: List[str]): if not hasattr(target, "bl_rna"): return target_rna = target.bl_rna if any(isinstance(target_rna, t) for t in always_clear): target.clear() return incoming_keys = set(incoming_keys) existing_keys = set(target.keys()) truncate_keys = existing_keys - incoming_keys if not truncate_keys: return if isinstance(target_rna, type(T.KeyingSets.bl_rna)): for k in truncate_keys: target.active_index = target.find(k) bpy.ops.anim.keying_set_remove() else: try: for k in truncate_keys: target.remove(target[k]) except Exception: logger.warning( f"Not implemented truncate_collection for type {target.bl_rna} for {target} ..." ) for s in traceback.format_exc().splitlines(): logger.warning(f"...{s}")
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 _(collection: T.bpy_prop_collection, proxy: Proxy, index: int, context: Context) -> T.bpy_struct: node = context.visit_state.attribute(-1) if not isinstance(node, _node_groups): logger.warning( f"Unexpected add node input for {node} at {context.visit_path.path()}" ) socket_type = proxy.data("bl_idname") name = proxy.data("name") return collection.new(socket_type, name)
def _(collection: T.bpy_prop_collection, proxy: Proxy, index: int, context: Context) -> T.bpy_struct: node_type = proxy.data("bl_idname") try: return collection.new(node_type) except RuntimeError as e: name = proxy.data("name") logger.error( f"add_element failed for node {name!r} into {context.visit_state.display_path()} ..." ) logger.error(f"... {e!r}") raise AddElementFailed from None
def fit_aos(target: T.bpy_prop_collection, proxy: AosProxy, context: Context): """ Adjust the size of a bpy_prop_collection proxified as an array of structures (e.g. MeshVertices) """ if not hasattr(target, "bl_rna"): return target_rna = target.bl_rna if isinstance(target_rna, _resize_geometry_types): existing_length = len(target) incoming_length = proxy.length if existing_length != incoming_length: if existing_length != 0: logger.error(f"resize_geometry(): size mismatch for {target}") logger.error( f"... existing: {existing_length} incoming {incoming_length}" ) return logger.debug( f"resizing geometry: add({incoming_length}) for {target}") target.add(incoming_length) return if isinstance(target_rna, type(T.GPencilStrokePoints.bl_rna)): existing_length = len(target) incoming_length = proxy.length delta = incoming_length - existing_length if delta > 0: target.add(delta) else: while delta < 0: target.pop() delta += 1 return if isinstance(target_rna, type(T.SplineBezierPoints.bl_rna)): existing_length = len(target) incoming_length = proxy.length delta = incoming_length - existing_length if delta > 0: target.add(delta) else: logger.error("Remove not implemented for type SplineBezierPoints") return logger.error( f"Not implemented fit_aos for type {target.bl_rna} for {target} ...")
def diff(self, aos: T.bpy_prop_collection, key: str, prop: T.Property, context: Context) -> Optional[DeltaUpdate]: if len(aos) == 0: return None array_size, member_type = self.array_attr(aos, self._member_name, prop.bl_rna) typecode = self._array.typecode tmp_array = array.array(typecode, soa_initializer(member_type, array_size)) if logger.isEnabledFor(logging.DEBUG): message = ( f"diff {aos}.{self._member_name} proxy({len(self._array)} {typecode}) blender'{len(aos)} {member_type}'" ) logger.debug(message) aos.foreach_get(self._member_name, tmp_array) force_diff = context.visit_state.scratchpad.get("force_soa_diff", False) if self._array == tmp_array and not force_diff: return None diff = self.__class__() diff._member_name = self._member_name diff._array = tmp_array diff._attach(context) return DeltaUpdate(diff)
def load( self, bl_collection: T.bpy_prop_collection, context: Context, ): self._sequence.clear() for i, v in enumerate(bl_collection.values()): context.visit_state.push(v, i) try: self._sequence.append(_proxy_factory(v).load(v, context)) except Exception as e: logger.error(f"Exception during load at {context.visit_state.display_path()} ...") logger.error(f"... {e!r}") finally: context.visit_state.pop() return self
def load( self, bl_collection: T.bpy_prop_collection, key: Union[int, str], bl_collection_property: T.Property, context: Context, ): context.visit_state.path.append(key) try: self._sequence = [ _proxy_factory(v).load(v, i, context) for i, v in enumerate(bl_collection.values()) ] finally: context.visit_state.path.pop() return self
def _add_element_keyingset(collection: T.bpy_prop_collection, proxy: Proxy, context: Context): # TODO current implementation fails # All keying sets paths have an empty name, and insertion with add() fails # with an empty name target_ref = proxy.data("id") if target_ref is None: target = None else: target = target_ref.target(context) data_path = proxy.data("data_path") index = proxy.data("array_index") group_method = proxy.data("group_method") group_name = proxy.data("group") return collection.add(target_id=target, data_path=data_path, index=index, group_method=group_method, group_name=group_name)
def add_element(proxy: Proxy, collection: T.bpy_prop_collection, key: str): """Add an element to a bpy_prop_collection using the collection specific API """ bl_rna = getattr(collection, "bl_rna", None) if bl_rna is not None: if isinstance(bl_rna, type(T.KeyingSets.bl_rna)): idname = proxy.data("bl_idname") return collection.new(name=key, idname=idname) if isinstance(bl_rna, type(T.KeyingSetPaths.bl_rna)): # TODO current implementation fails # All keying sets paths have an empty name, and insertion with add()à failes # with an empty name target_ref = proxy.data("id") if target_ref is None: target = None else: target = target_ref.target() data_path = proxy.data("data_path") index = proxy.data("array_index") group_method = proxy.data("group_method") group_name = proxy.data("group") return collection.add(target_id=target, data_path=data_path, index=index, group_method=group_method, group_name=group_name) if isinstance(bl_rna, type(T.Nodes.bl_rna)): node_type = proxy.data("bl_idname") return collection.new(node_type) if isinstance(bl_rna, type(T.Sequences.bl_rna)): type_ = proxy.data("type") name = proxy.data("name") channel = proxy.data("channel") frame_start = proxy.data("frame_start") if type_ in effect_sequences: # overwritten anyway frame_end = frame_start + 1 return collection.new_effect(name, type_, channel, frame_start, frame_end=frame_end) if type_ == "SOUND": sound = proxy.data("sound") target = sound.target() if not target: logger.warning( f"missing target ID block for bpy.data.{sound.collection}[{sound.key}] " ) return None filepath = target.filepath return collection.new_sound(name, filepath, channel, frame_start) if type_ == "MOVIE": filepath = proxy.data("filepath") return collection.new_movie(name, filepath, channel, frame_start) if type_ == "IMAGE": directory = proxy.data("directory") filename = proxy.data("elements").data(0).data("filename") filepath = str(Path(directory) / filename) return collection.new_image(name, filepath, channel, frame_start) logger.warning(f"Sequence type not implemented: {type_}") # SCENE may be harder than it seems, since we cannot order scene creations. # Currently the creation order is the "deepmost" order as listed in proxy.py:_creation_order # but it does not work for this case return None if isinstance(bl_rna, type(T.SequenceModifiers.bl_rna)): name = proxy.data("name") type_ = proxy.data("type") return collection.new(name, type_) try: return collection.add() except Exception: pass # try our best new_or_add = getattr(collection, "new", None) if new_or_add is None: new_or_add = getattr(collection, "add", None) if new_or_add is None: logger.warning( f"Not implemented new or add for bpy.data.{collection}[{key}] ...") return None try: return new_or_add(key) except Exception: logger.warning( f"Not implemented new or add for type {type(collection)} for {collection}[{key}] ..." ) for s in traceback.format_exc().splitlines(): logger.warning(f"...{s}") return None
def _add_element_location(collection: T.bpy_prop_collection, proxy: Proxy, context: Context): location = proxy.data("location") return collection.new(location[0], location[1])
def _add_element_name_eq(collection: T.bpy_prop_collection, proxy: Proxy, context: Context): name = proxy.data("name") return collection.new(name=name)
def _add_element_idname(collection: T.bpy_prop_collection, proxy: Proxy, context: Context): node_type = proxy.data("bl_idname") return collection.new(node_type)
def _truncate_collection_clear(collection: T.bpy_prop_collection, size: int): collection.clear()
def _add_element_info(collection: T.bpy_prop_collection, proxy: Proxy, context: Context): name = proxy.data("info") return collection.new(name)
def _add_element_frame_number(collection: T.bpy_prop_collection, proxy: Proxy, context: Context): frame_number = proxy.data("frame_number") return collection.new(frame_number)
def _add_element_bl_label(collection: T.bpy_prop_collection, proxy: Proxy, context: Context): label = proxy.data("bl_label") idname = proxy.data("bl_idname") return collection.new(name=label, idname=idname)
def _add_element_type_eq(collection: T.bpy_prop_collection, proxy: Proxy, context: Context): type_ = proxy.data("type") return collection.new(type=type_)