From 5e3f0ab337dec07f6a76f10267241f1ba1daa40a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Jan 2020 19:11:05 +0100 Subject: [PATCH 01/99] initial commit --- pype/scripts/slate/base.py | 799 +++++++++++++++++++++++++++++++++++++ 1 file changed, 799 insertions(+) create mode 100644 pype/scripts/slate/base.py diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py new file mode 100644 index 0000000000..cd51c94aab --- /dev/null +++ b/pype/scripts/slate/base.py @@ -0,0 +1,799 @@ +import sys +sys.path.append(r"C:\Users\Public\pype_env2\Lib\site-packages") +from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageColor +from uuid import uuid4 + +class BaseObj: + """Base Object for slates.""" + + obj_type = None + available_parents = [] + + def __init__(self, parent, style={}, name=None): + if not self.obj_type: + raise NotImplemented( + "Class don't have set object type <{}>".format( + self.__class__.__name__ + ) + ) + + parent_obj_type = None + if parent: + parent_obj_type = parent.obj_type + + if parent_obj_type not in self.available_parents: + expected_parents = ", ".join(self.available_parents) + raise Exception(( + "Invalid parent <{}> for <{}>. Expected <{}>" + ).format( + parent.__class__.__name__, self.obj_type, expected_parents + )) + + self.parent = parent + self._style = style + + self.id = uuid4() + self.parent = parent + self.name = name + self._style = style + self.items = {} + self.final_style = None + + @property + def main_style(self): + default_style_v1 = { + "*": { + "font-family": "arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": False, + "font-italic": False, + "bg-color": None, + "bg-alter-color": None, + "alignment-vertical": "left", #left, center, right + "alignment-horizontal": "top", #top, center, bottom + "padding": 0, + # "padding-left": 0, + # "padding-right": 0, + # "padding-top": 0, + # "padding-bottom": 0 + "margin": 0, + # "margin-left": 0, + # "margin-right": 0, + # "margin-top": 0, + # "margin-bottom": 0 + }, + "layer": {}, + "image": {}, + "text": {}, + "table": {}, + "table-item": {}, + "table-item-col-0": {}, + "#MyName": {} + } + return default_style_v1 + + @property + def is_drawing(self): + return self.parent.is_drawing + + @property + def height(self): + raise NotImplementedError( + "Attribute `height` is not implemented for <{}>".format( + self.__clas__.__name__ + ) + ) + + @property + def width(self): + raise NotImplementedError( + "Attribute `width` is not implemented for <{}>".format( + self.__clas__.__name__ + ) + ) + + @property + def full_style(self): + if self.is_drawing and self.final_style is not None: + return self.final_style + + if self.parent is not None: + style = dict(val for val in self.parent.full_style.items()) + else: + style = self.main_style + + for key, value in self._style.items(): + if key in style: + style[key] = value + style.update() + + if self.is_drawing: + self.final_style = style + + return style + + @property + def style(self): + style = self.full_style + style.update(self._style) + + base = style.get("*", style.get("global")) or {} + obj_specific = style.get(self.obj_type) or {} + name_specific = {} + if self.name: + name = str(self.name) + if not name.startswith("#"): + name += "#" + name_specific = style.get(name) or {} + + output = {} + output.update(base) + output.update(obj_specific) + output.update(name_specific) + + return output + + @property + def pos_x(self): + return 0 + + @property + def pos_y(self): + return 0 + + @property + def pos_start(self): + return (self.pos_x, self.pos_y) + + @property + def pos_end(self): + pos_x, pos_y = self.pos_start + pos_x += self.width + pos_y += self.height + return (pos_x, pos_y) + + @property + def max_width(self): + return self.style.get("max-width") or (self.width * 1) + + @property + def max_height(self): + return self.style.get("max-height") or (self.height * 1) + + @property + def max_content_width(self): + width = self.max_width + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + padding_right = self.style.get("padding-right") or padding + return (width - (padding_left + padding_right)) + + @property + def max_content_height(self): + height = self.max_height + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or padding + padding_bottom = self.style.get("padding-bottom") or padding + return (height - (padding_top + padding_bottom)) + + def add_item(self, item): + self.items[item.id] = item + + def reset(self): + self.final_style = None + for item in self.items.values(): + item.reset() + + +class MainFrame(BaseObj): + + obj_type = "main_frame" + available_parents = [None] + + def __init__(self, width, height, style={}, parent=None, name=None): + super(MainFrame, self).__init__(parent, style, name) + self._width = width + self._height = height + self._is_drawing = False + + @property + def width(self): + return self._width + + @property + def height(self): + return self._height + + @property + def is_drawing(self): + return self._is_drawing + + def draw(self, path): + self._is_drawing = True + bg_color = self.style["bg-color"] + image = Image.new("RGB", (self.width, self.height))#, color=bg_color) + for item in self.items.values(): + item.draw(image) + + image.save(path) + self._is_drawing = False + self.reset() + + + +class Layer(BaseObj): + obj_type = "layer" + available_parents = ["main_frame", "layer"] + + # Direction can be 0=horizontal/ 1=vertical + def __init__(self, *args, **kwargs): + super(Layer, self).__init__(*args, **kwargs) + + @property + def pos_x(self): + if self.parent.obj_type == self.obj_type: + pos_x = self.parent.item_pos_x(self.id) + else: + pos_x = self.parent.pos_x + return pos_x + + @property + def pos_y(self): + if self.parent.obj_type == self.obj_type: + pos_y = self.parent.item_pos_y(self.id) + else: + pos_y = self.parent.pos_y + return pos_y + + @property + def direction(self): + direction = self.style.get("direction", 0) + if direction not in (0, 1): + raise Exception( + "Direction must be 0 or 1 (0 is Vertical / 1 is horizontal)!" + ) + return direction + + def item_pos_x(self, item_id): + pos_x = self.pos_x + if self.direction != 0: + for id, item in self.items.items(): + if id == item_id: + break + pos_x += item.width + if item.obj_type != "image": + pos_x += 1 + + if pos_x != self.pos_x: + pos_x += 1 + return pos_x + + def item_pos_y(self, item_id): + pos_y = self.pos_y + if self.direction != 1: + for id, item in self.items.items(): + if item_id == id: + break + pos_y += item.height + if item.obj_type != "image": + pos_y += 1 + + return pos_y + + @property + def height(self): + height = 0 + for item in self.items.values(): + if self.direction == 0: + if height > item.height: + continue + + # times 1 because won't get object pointer but number + height = item.height * 1 + else: + height += item.height + + min_height = self.style.get("min-height") + if min_height > height: + return min_height + return height + + @property + def width(self): + width = 0 + for item in self.items.values(): + if self.direction == 0: + if width > item.width: + continue + + # times 1 because won't get object pointer but number + width = item.width * 1 + else: + width += item.width + + min_width = self.style.get("min-width") + if min_width > width: + return min_width + return width + + def draw(self, image, drawer=None): + if drawer is None: + drawer = ImageDraw.Draw(image) + + for item in self.items.values(): + item.draw(image, drawer) + + +class BaseItem(BaseObj): + available_parents = ["layer"] + + def __init__(self, *args, **kwargs): + super(BaseItem, self).__init__(*args, **kwargs) + + @property + def pos_x(self): + return self.parent.item_pos_x(self.id) + + @property + def pos_y(self): + return self.parent.item_pos_y(self.id) + + @property + def content_pos_x(self): + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + + return self.pos_x + margin_left + padding_left + + @property + def content_pos_y(self): + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or padding + + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + + return self.pos_y + margin_top + padding_top + + @property + def content_width(self): + raise NotImplementedError( + "Attribute is not implemented" + ) + + @property + def content_height(self): + raise NotImplementedError( + "Attribute is not implemented" + ) + + @property + def width(self): + width = self.content_width + + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + padding_right = self.style.get("padding-right") or padding + + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + margin_right = self.style.get("margin-right") or margin + + return ( + width + + margin_left + margin_right + + padding_left + padding_right + ) + + @property + def height(self): + height = self.content_height + + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or padding + padding_bottom = self.style.get("padding-bottom") or padding + + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + margin_bottom = self.style.get("margin-bottom") or margin + + return ( + height + + margin_bottom + margin_top + + padding_top + padding_bottom + ) + + def add_item(self, *args, **kwargs): + raise Exception("Can't add item to an item, use layers instead.") + + def draw(self, image, drawer): + raise NotImplementedError( + "Method `draw` is not implemented for <{}>".format( + self.__clas__.__name__ + ) + ) + +class ItemImage(BaseItem): + obj_type = "image" + + def __init__(self, image_path, *args, **kwargs): + super(ItemImage, self).__init__(*args, **kwargs) + self.image_path = image_path + + def draw(self, image, drawer): + paste_image = Image.open(self.image_path) + paste_image = paste_image.resize( + (self.content_width, self.content_height), + Image.ANTIALIAS + ) + image.paste( + paste_image, + (self.content_pos_x, self.content_pos_y) + ) + + @property + def content_width(self): + return self.style.get("width") + + @property + def content_height(self): + return self.style.get("height") + + +class ItemText(BaseItem): + obj_type = "text" + + def __init__(self, value, *args, **kwargs): + super(ItemText, self).__init__(*args, **kwargs) + self.value = value + + def draw(self, image, drawer): + bg_color = self.style["bg-color"] + if bg_color and bg_color.lower() != "transparent": + padding = self.style["padding"] + + padding_left = self.style.get("padding-left") or padding + padding_right = self.style.get("padding-right") or padding + padding_top = self.style.get("padding-top") or padding + padding_bottom = self.style.get("padding-bottom") or padding + # TODO border outline styles + drawer.rectangle( + (self.pos_start, self.pos_end), + fill=bg_color, + outline=None + ) + + text_pos_start = (self.content_pos_x, self.content_pos_y) + + font_color = self.style["font-color"] + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + drawer.text( + text_pos_start, + self.value, + font=font, + fill=font_color + ) + + @property + def content_width(self): + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + width = font.getsize(self.value)[0] + return width + + @property + def content_height(self): + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + height = font.getsize(self.value)[1] + return height + + +class ItemTable(BaseItem): + + obj_type = "table" + + def __init__(self, values, *args, **kwargs): + super(ItemTable, self).__init__(*args, **kwargs) + self.size_values = None + self.values_by_cords = None + + self.prepare_values(values) + self.calculate_sizes() + + def prepare_values(self, _values): + values = [] + values_by_cords = [] + for row_idx, row in enumerate(_values): + if len(values_by_cords) < row_idx + 1: + for i in range(len(values_by_cords), row_idx + 1): + values_by_cords.append([]) + + for col_idx, col in enumerate(_values[row_idx]): + if len(values_by_cords[row_idx]) < col_idx + 1: + for i in range(len(values_by_cords[row_idx]), col_idx + 1): + values_by_cords[row_idx].append("") + + if not col: + col = "" + col_item = TableField(row_idx, col_idx, col, self) + values_by_cords[row_idx][col_idx] = col_item + values.append(col_item) + + self.values = values + self.values_by_cords = values_by_cords + + def calculate_sizes(self): + row_heights = [] + col_widths = [] + for row_idx, row in enumerate(self.values_by_cords): + row_heights.append(0) + for col_idx, col_item in enumerate(row): + if len(col_widths) < col_idx + 1: + col_widths.append(0) + + _width = col_widths[col_idx] + item_width = col_item.width + if _width < item_width: + col_widths[col_idx] = item_width + + _height = row_heights[row_idx] + item_height = col_item.height + if _height < item_height: + row_heights[row_idx] = item_height + + self.size_values = (row_heights, col_widths) + + def draw(self, image, drawer): + bg_color = self.style["bg-color"] + if bg_color and bg_color.lower() != "transparent": + padding = self.style["padding"] + + padding_left = self.style.get("padding-left") or padding + padding_right = self.style.get("padding-right") or padding + padding_top = self.style.get("padding-top") or padding + padding_bottom = self.style.get("padding-bottom") or padding + # TODO border outline styles + drawer.rectangle( + (self.pos_start, self.pos_end), + fill=bg_color, + outline=None + ) + + for value in self.values: + value.draw(image, drawer) + + @property + def content_width(self): + row_heights, col_widths = self.size_values + width = 0 + for _width in col_widths: + width += _width + + if width != 0: + width -= 1 + return width + + @property + def content_height(self): + row_heights, col_widths = self.size_values + height = 0 + for _height in row_heights: + height += _height + + if height != 0: + height -= 1 + return height + + def pos_info_by_cord(self, cord_x, cord_y): + row_heights, col_widths = self.size_values + pos_x = self.pos_x + pos_y = self.pos_y + width = 0 + height = 0 + for idx, value in enumerate(col_widths): + if cord_y == idx: + width = value + break + pos_x += value + + for idx, value in enumerate(row_heights): + if cord_x == idx: + height = value + break + pos_y += value + + padding = self.style["padding"] + + padding_left = self.style.get("padding-left") or padding + padding_top = self.style.get("padding-top") or padding + pos_x += padding_left + pos_y += padding_top + return (pos_x, pos_y, width, height) + + def filled_style(self, cord_x, cord_y): + # TODO CHANGE STYLE BY CORDS + return self.style + + +class TableField(BaseItem): + + obj_type = "table-item" + available_parents = ["table"] + + def __init__(self, cord_x, cord_y, value, *args, **kwargs): + super(TableField, self).__init__(*args, **kwargs) + self.cord_x = cord_x + self.cord_y = cord_y + self.value = value + + @property + def content_width(self): + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + width = font.getsize(self.value)[0] + 1 + return width + + @property + def content_height(self): + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + height = font.getsize(self.value)[1] + 1 + return height + + def content_pos_x(self, pos_x, width): + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + return pos_x + padding_left + + def content_pos_y(self, pos_y, height): + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or padding + return pos_y + padding_top + + def draw(self, image, drawer): + pos_x, pos_y, width, height = ( + self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + ) + pos_start = (pos_x, pos_y) + pos_end = (pos_x + width, pos_y + height) + bg_color = self.style["bg-color"] + if bg_color and bg_color.lower() != "transparent": + padding = self.style["padding"] + + padding_left = self.style.get("padding-left") or padding + padding_right = self.style.get("padding-right") or padding + padding_top = self.style.get("padding-top") or padding + padding_bottom = self.style.get("padding-bottom") or padding + # TODO border outline styles + drawer.rectangle( + (pos_start, pos_end), + fill=bg_color, + outline=None + ) + + text_pos_start = ( + self.content_pos_x(pos_x, width), + self.content_pos_y(pos_y, height) + ) + + font_color = self.style["font-color"] + font_family = self.style["font-family"] + font_size = self.style["font-size"] + + font = ImageFont.truetype(font_family, font_size) + drawer.text( + text_pos_start, + self.value, + font=font, + fill=font_color + ) + +if __name__ == "__main__": + main_style = { + "bg-color": "#777777" + } + text_1_style = { + "padding": 10, + "bg-color": "#00ff77" + } + text_2_style = { + "padding": 8, + "bg-color": "#ff0066" + } + text_3_style = { + "padding": 0, + "bg-color": "#ff5500" + } + main = MainFrame(1920, 1080, style=main_style) + layer = Layer(parent=main) + main.add_item(layer) + + text_1 = ItemText("Testing message 1", layer, text_1_style) + text_2 = ItemText("Testing message 2", layer, text_2_style) + text_3 = ItemText("Testing message 3", layer, text_3_style) + table_1_items = [["0", "Output text 1"], ["1", "Output text 2"], ["2", "Output text 3"]] + table_1_style = { + "padding": 8, + "bg-color": "#0077ff" + } + table_1 = ItemTable(table_1_items, layer, table_1_style) + + image_1_style = { + "width": 240, + "height": 120, + "bg-color": "#7733aa" + } + image_1_path = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\kitten.jpg" + image_1 = ItemImage(image_1_path, layer, image_1_style) + + layer.add_item(text_1) + layer.add_item(text_2) + layer.add_item(table_1) + layer.add_item(image_1) + layer.add_item(text_3) + dst = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\test_output2.png" + main.draw(dst) + + print("*** Drawing done :)") + + +style_schema = { + "alignment": { + "description": "Alignment of item", + "type": "string", + "enum": ["left", "right", "center"], + "example": "left" + }, + "font-family": { + "description": "Font type", + "type": "string", + "example": "Arial" + }, + "font-size": { + "description": "Font size", + "type": "integer", + "example": 26 + }, + "font-color": { + "description": "Font color", + "type": "string", + "regex": ( + "^[#]{1}[a-fA-F0-9]{1}$" + " | ^[#]{1}[a-fA-F0-9]{3}$" + " | ^[#]{1}[a-fA-F0-9]{6}$" + ), + "example": "#ffffff" + }, + "font-bold": { + "description": "Font boldness", + "type": "boolean", + "example": True + }, + "bg-color": { + "description": "Background color, None means transparent", + "type": ["string", None], + "regex": ( + "^[#]{1}[a-fA-F0-9]{1}$" + " | ^[#]{1}[a-fA-F0-9]{3}$" + " | ^[#]{1}[a-fA-F0-9]{6}$" + ), + "example": "#333" + }, + "bg-alter-color": None, +} From 89a0dd05fd325053df72163ed4727acfd16b8d77 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Jan 2020 19:11:50 +0100 Subject: [PATCH 02/99] added set.json need rework --- pype/scripts/slate/set.json | 59 +++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 pype/scripts/slate/set.json diff --git a/pype/scripts/slate/set.json b/pype/scripts/slate/set.json new file mode 100644 index 0000000000..e8bba8ac41 --- /dev/null +++ b/pype/scripts/slate/set.json @@ -0,0 +1,59 @@ +{ + "width": 1920, + "height": 1020, + "bg-color": "#000000", + "__bg-color__": "For setting constant color of background. May cause issues if not set when there are not painted spaces.", + "bg-image": null, + "__bg-image__": "May be path to static image???", + "defaults": { + "font-family": "Arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": false, + "bg-color": null, + "bg-alter-color": null, + "alignment": "left", + "__alignment[enum]__": ["left", "right", "center"] + }, + "items": [{ + "rel_pos_x": 0.1, + "rel_pos_y": 0.1, + "rel_width": 0.5, + "type": "table", + "col-format": [ + { + "font-size": 12, + "font-color": "#777777", + "alignment": "right" + }, + { + "alignment": "left" + } + ], + "rows": [ + [ + { + "name": "Version", + }, + { + "value": "mei_101_001_0020_slate_NFX_v001", + "font-bold": true + } + ], [ + { + "value": "Date:" + }, + { + "value": "{date}" + } + ] + ] + }, { + "rel_pos_x": 0.1, + "rel_pos_y": 0.1, + "rel_width": 0.5, + "rel_height": 0.5, + "type": "image", + } + ] +} From 18db21c82837ea80f1f6e93f1290443b8489784a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Jan 2020 19:12:03 +0100 Subject: [PATCH 03/99] first successfull image inspiration --- pype/scripts/slate/slate.py | 355 ++++++++++++++++++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 pype/scripts/slate/slate.py diff --git a/pype/scripts/slate/slate.py b/pype/scripts/slate/slate.py new file mode 100644 index 0000000000..4e66f68d21 --- /dev/null +++ b/pype/scripts/slate/slate.py @@ -0,0 +1,355 @@ +import textwrap +from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageColor + + +class TableDraw: + def __init__( + self, image_width, image_height, + rel_pos_x, rel_pos_y, rel_width, + col_fonts=None, col_font_colors=None, + default_font=None, default_font_color=None, + col_alignments=None, rel_col_widths=None, bg_color=None, + alter_bg_color=None, pad=20, + pad_top=None, pad_bottom=None, pad_left=None, pad_right=None + ): + self.image_width = image_width + + pos_x = image_width * rel_pos_x + pos_y = image_height * rel_pos_y + width = image_width * rel_width + + self.pos_x_start = pos_x + self.pos_y_start = pos_y + self.pos_x_end = pos_x + width + self.width = width + + self.rel_col_widths = list(rel_col_widths) + self._col_widths = None + self._col_alignments = col_alignments + + if bg_color and isinstance(bg_color, str): + bg_color = ImageColor.getrgb(bg_color) + + if alter_bg_color and isinstance(alter_bg_color, str): + alter_bg_color = ImageColor.getrgb(alter_bg_color) + + self._bg_color = bg_color + self._alter_bg_color = alter_bg_color + + self.alter_use = False + + if col_fonts: + _col_fonts = [] + for col_font in col_fonts: + font_name, font_size = col_font + font = ImageFont.truetype(font_name, font_size) + _col_fonts.append(font) + col_fonts = _col_fonts + + self._col_fonts = col_fonts + + self._col_font_colors = col_font_colors + + if not default_font: + default_font = ImageFont.truetype("times", 26) + self.default_font = default_font + + if not default_font_color: + default_font_color = "#ffffff" + self.default_font_color = default_font_color + + self.texts = [] + + if pad is None: + pad = 5 + + _pad_top = pad + if pad_top is not None: + _pad_top = pad_top + + _pad_bottom = pad + if pad_bottom is not None: + _pad_bottom = pad_bottom + + _pad_left = pad + if pad_left is not None: + _pad_left = pad_left + + _pad_right = pad + if pad_right is not None: + _pad_right = pad_right + + self.pad_top = _pad_top + self.pad_bottom = _pad_bottom + self.pad_left = _pad_left + self.pad_right = _pad_right + + @property + def col_widths(self): + if self._col_widths is None: + sum_width = 0 + for w in self.rel_col_widths: + sum_width += w + + one_piece = self.width / sum_width + self._col_widths = [] + for w in self.rel_col_widths: + self._col_widths.append(one_piece * w) + + return self._col_widths + + @property + def col_fonts(self): + if self._col_fonts is None: + self._col_fonts = [] + for _ in range(len(self.col_widths)): + self._col_fonts.append(self.default_font) + + elif len(self._col_fonts) < len(self.col_widths): + if isinstance(self._col_fonts, tuple): + self._col_fonts = list(self._col_fonts) + + while len(self._col_fonts) < len(self.col_widths): + self._col_fonts.append(self.default_font) + + return self._col_fonts + + @property + def col_font_colors(self): + if self._col_font_colors is None: + self._col_font_colors = [] + for _ in range(len(self.col_widths)): + self._col_font_colors.append(self.default_font_color) + + elif len(self._col_font_colors) < len(self.col_widths): + if isinstance(self._col_font_colors, tuple): + self._col_font_colors = list(self._col_font_colors) + + while len(self._col_font_colors) < len(self.col_widths): + self._col_font_colors.append(self.default_font_color) + + return self._col_font_colors + + @property + def col_alignments(self): + if self._col_alignments is None: + self._col_alignments = [] + for _ in range(len(self.col_widths)): + self._col_alignments.append("left") + + elif len(self._col_alignments) < len(self.col_widths): + if isinstance(self._col_alignments, tuple): + self._col_alignments = list(self._col_alignments) + + while len(self._col_alignments) < len(self.col_widths): + self._col_alignments.append("left") + + return self._col_alignments + + @property + def bg_color(self): + if self.alter_use is True: + value = self.alter_bg_color + self.alter_use = False + else: + value = self._bg_color + self.alter_use = True + return value + + @property + def alter_bg_color(self): + if self._alter_bg_color: + return self._alter_bg_color + return self.bg_color + + def add_texts(self, texts): + if isinstance(texts, str): + texts = [texts] + + for text in texts: + if isinstance(text, str): + text = [text] + + if len(text) > len(self.rel_col_widths): + for _ in (len(text) - len(self.rel_col_widths)): + self.rel_col_widths.append(1) + for _t in self.texts: + _t.append("") + + self.texts.append(text) + + def draw(self, drawer): + y_pos = self.pos_y_start + for texts in self.texts: + max_height = None + cols_data = [] + for _idx, col in enumerate(texts): + width = self.col_widths[_idx] + font = self.col_fonts[_idx] + lines, line_height = self.lines_height_by_width( + drawer, col, width - self.pad_left - self.pad_right, font + ) + row_height = line_height * len(lines) + if max_height is None or row_height > max_height: + max_height = row_height + + cols_data.append({ + "lines": lines, + "line_height": line_height + }) + + drawer.rectangle( + ( + (self.pos_x_start, y_pos), + ( + self.pos_x_end, + y_pos + max_height + self.pad_top + self.pad_bottom + ) + ), + fill=self.bg_color + ) + + pos_x_start = self.pos_x_start + self.pad_left + for col, col_data in enumerate(cols_data): + lines = col_data["lines"] + line_height = col_data["line_height"] + alignment = self.col_alignments[col] + x_offset = self.col_widths[col] + font = self.col_fonts[col] + font_color = self.col_font_colors[col] + for idx, line_data in enumerate(lines): + line = line_data["text"] + line_width = line_data["width"] + if alignment == "left": + x_start = pos_x_start + self.pad_left + elif alignment == "right": + x_start = ( + pos_x_start + x_offset - line_width - + self.pad_right - self.pad_left + ) + else: + # TODO else + x_start = pos_x_start + self.pad_left + + drawer.text( + ( + x_start, + y_pos + (idx * line_height) + self.pad_top + ), + line, + font=font, + fill=font_color + ) + pos_x_start += x_offset + + y_pos += max_height + self.pad_top + self.pad_bottom + + def lines_height_by_width(self, drawer, text, width, font): + lines = [] + lines.append([part for part in text.split() if part]) + + line = 0 + while True: + thistext = lines[line] + line = line + 1 + if not thistext: + break + newline = [] + + while True: + _textwidth = drawer.textsize(" ".join(thistext), font)[0] + if ( + _textwidth <= width + ): + break + elif _textwidth > width and len(thistext) == 1: + # TODO raise error? + break + + val = thistext.pop(-1) + + if not val: + break + newline.insert(0, val) + + if len(newline) > 0: + lines.append(newline) + else: + break + + _lines = [] + height = None + for line_items in lines: + line = " ".join(line_items) + (width, _height) = drawer.textsize(line, font) + if height is None or height < _height: + height = _height + + _lines.append({ + "width": width, + "text": line + }) + + return (_lines, height) + + +width = 1920 +height = 1080 +# width = 800 +# height = 600 +bg_color_hex = "#242424" +bg_color = ImageColor.getrgb(bg_color_hex) + +base = Image.new('RGB', (width, height), color=bg_color) + +texts = [ + ("Version name:", "mei_101_001_0020_slate_NFX_v001"), + ("Date:", "2019-08-09"), + ("Shot Types:", "2d comp"), + # ("Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production.") +] +text_widths_rel = (2, 8) +col_alignments = ("right", "left") +fonts = (("arial", 20), ("times", 26)) +font_colors = ("#999999", "#ffffff") + +table_color_hex = "#212121" +table_alter_color_hex = "#272727" + +drawer = ImageDraw.Draw(base) +table_d = TableDraw( + width, height, + 0.1, 0.1, 0.5, + col_fonts=fonts, + col_font_colors=font_colors, + rel_col_widths=text_widths_rel, + col_alignments=col_alignments, + bg_color=table_color_hex, + alter_bg_color=table_alter_color_hex, + pad_top=20, pad_bottom=20, pad_left=5, pad_right=5 +) + +table_d.add_texts(texts) +table_d.draw(drawer) + +image_path = r"C:\Users\iLLiCiT\Desktop\Prace\Pillow\image.jpg" +image = Image.open(image_path) +img_width, img_height = image.size + +rel_image_width = 0.3 +rel_image_pos_x = 0.65 +rel_image_pos_y = 0.1 +image_pos_x = int(width * rel_image_pos_x) +image_pos_y = int(width * rel_image_pos_y) + +new_width = int(width * rel_image_width) +new_height = int(new_width * img_height / img_width) +image = image.resize((new_width, new_height), Image.ANTIALIAS) + +# mask = Image.new("L", image.size, 255) + +base.paste(image, (image_pos_x, image_pos_y)) +base.save(r"C:\Users\iLLiCiT\Desktop\Prace\Pillow\test{}x{}.jpg".format( + width, height +)) +base.show() From 449fb26aa796681ed77ffb62260ff0dcebc666b4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 12 Jan 2020 23:14:40 +0100 Subject: [PATCH 04/99] added margins and modified style --- pype/scripts/slate/base.py | 117 +++++++++----------------- pype/scripts/slate/default_style.json | 44 ++++++++++ pype/scripts/slate/style_schema.json | 44 ++++++++++ 3 files changed, 130 insertions(+), 75 deletions(-) create mode 100644 pype/scripts/slate/default_style.json create mode 100644 pype/scripts/slate/style_schema.json diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index cd51c94aab..5ae7109ed4 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -3,6 +3,7 @@ sys.path.append(r"C:\Users\Public\pype_env2\Lib\site-packages") from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageColor from uuid import uuid4 + class BaseObj: """Base Object for slates.""" @@ -11,7 +12,7 @@ class BaseObj: def __init__(self, parent, style={}, name=None): if not self.obj_type: - raise NotImplemented( + raise NotImplementedError( "Class don't have set object type <{}>".format( self.__class__.__name__ ) @@ -50,18 +51,10 @@ class BaseObj: "font-italic": False, "bg-color": None, "bg-alter-color": None, - "alignment-vertical": "left", #left, center, right - "alignment-horizontal": "top", #top, center, bottom + "alignment-vertical": "left", + "alignment-horizontal": "top", "padding": 0, - # "padding-left": 0, - # "padding-right": 0, - # "padding-top": 0, - # "padding-bottom": 0 "margin": 0, - # "margin-left": 0, - # "margin-right": 0, - # "margin-top": 0, - # "margin-bottom": 0 }, "layer": {}, "image": {}, @@ -105,8 +98,9 @@ class BaseObj: for key, value in self._style.items(): if key in style: - style[key] = value - style.update() + style[key].update(value) + else: + style[self.obj_type][key] = value if self.is_drawing: self.final_style = style @@ -134,6 +128,7 @@ class BaseObj: return output + @property def pos_x(self): return 0 @@ -191,8 +186,8 @@ class MainFrame(BaseObj): obj_type = "main_frame" available_parents = [None] - def __init__(self, width, height, style={}, parent=None, name=None): - super(MainFrame, self).__init__(parent, style, name) + def __init__(self, width, height, *args, **kwargs): + super(MainFrame, self).__init__(*args, **kwargs) self._width = width self._height = height self._is_drawing = False @@ -212,7 +207,7 @@ class MainFrame(BaseObj): def draw(self, path): self._is_drawing = True bg_color = self.style["bg-color"] - image = Image.new("RGB", (self.width, self.height))#, color=bg_color) + image = Image.new("RGB", (self.width, self.height), color=bg_color) for item in self.items.values(): item.draw(image) @@ -221,14 +216,14 @@ class MainFrame(BaseObj): self.reset() - class Layer(BaseObj): obj_type = "layer" available_parents = ["main_frame", "layer"] # Direction can be 0=horizontal/ 1=vertical - def __init__(self, *args, **kwargs): + def __init__(self, direction=0, *args, **kwargs): super(Layer, self).__init__(*args, **kwargs) + self._direction = direction @property def pos_x(self): @@ -248,15 +243,20 @@ class Layer(BaseObj): @property def direction(self): - direction = self.style.get("direction", 0) - if direction not in (0, 1): - raise Exception( + if self._direction not in (0, 1): + print( "Direction must be 0 or 1 (0 is Vertical / 1 is horizontal)!" ) - return direction + return 0 + return self._direction def item_pos_x(self, item_id): pos_x = self.pos_x + + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + + pos_x += margin_left if self.direction != 0: for id, item in self.items.items(): if id == item_id: @@ -265,12 +265,15 @@ class Layer(BaseObj): if item.obj_type != "image": pos_x += 1 - if pos_x != self.pos_x: - pos_x += 1 return pos_x def item_pos_y(self, item_id): pos_y = self.pos_y + + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + + pos_y += margin_top if self.direction != 1: for id, item in self.items.items(): if item_id == id: @@ -283,7 +286,11 @@ class Layer(BaseObj): @property def height(self): - height = 0 + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + margin_bottom = self.style.get("margin-bottom") or margin + + height = (margin_top + margin_bottom) for item in self.items.values(): if self.direction == 0: if height > item.height: @@ -294,6 +301,7 @@ class Layer(BaseObj): else: height += item.height + min_height = self.style.get("min-height") if min_height > height: return min_height @@ -301,7 +309,11 @@ class Layer(BaseObj): @property def width(self): - width = 0 + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + margin_right = self.style.get("margin-right") or margin + + width = (margin_left + margin_right) for item in self.items.values(): if self.direction == 0: if width > item.width: @@ -402,9 +414,9 @@ class BaseItem(BaseObj): margin_bottom = self.style.get("margin-bottom") or margin return ( - height + - margin_bottom + margin_top + - padding_top + padding_bottom + height + + margin_bottom + margin_top + + padding_top + padding_bottom ) def add_item(self, *args, **kwargs): @@ -417,6 +429,7 @@ class BaseItem(BaseObj): ) ) + class ItemImage(BaseItem): obj_type = "image" @@ -751,49 +764,3 @@ if __name__ == "__main__": main.draw(dst) print("*** Drawing done :)") - - -style_schema = { - "alignment": { - "description": "Alignment of item", - "type": "string", - "enum": ["left", "right", "center"], - "example": "left" - }, - "font-family": { - "description": "Font type", - "type": "string", - "example": "Arial" - }, - "font-size": { - "description": "Font size", - "type": "integer", - "example": 26 - }, - "font-color": { - "description": "Font color", - "type": "string", - "regex": ( - "^[#]{1}[a-fA-F0-9]{1}$" - " | ^[#]{1}[a-fA-F0-9]{3}$" - " | ^[#]{1}[a-fA-F0-9]{6}$" - ), - "example": "#ffffff" - }, - "font-bold": { - "description": "Font boldness", - "type": "boolean", - "example": True - }, - "bg-color": { - "description": "Background color, None means transparent", - "type": ["string", None], - "regex": ( - "^[#]{1}[a-fA-F0-9]{1}$" - " | ^[#]{1}[a-fA-F0-9]{3}$" - " | ^[#]{1}[a-fA-F0-9]{6}$" - ), - "example": "#333" - }, - "bg-alter-color": None, -} diff --git a/pype/scripts/slate/default_style.json b/pype/scripts/slate/default_style.json new file mode 100644 index 0000000000..c0f1006a4a --- /dev/null +++ b/pype/scripts/slate/default_style.json @@ -0,0 +1,44 @@ +{ + "*": { + "font-family": "arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": false, + "font-italic": false, + "bg-color": "#777777", + "bg-alter-color": "#666666", + "__alignment-vertical__values": ["left", "center", "right"], + "alignment-vertical": "left", + "__alignment-horizontal__values": ["top", "center", "bottom"], + "alignment-horizontal": "top", + "__padding__variants": [ + "padding-top", "padding-right", "padding-bottom", "padding-left" + ], + "padding": 0, + "__margin__variants": [ + "margin-top", "margin-right", "margin-bottom", "margin-left" + ], + "margin": 0, + }, + "layer": { + + }, + "image": { + + }, + "text": { + + }, + "table": { + + }, + "table-item": { + + }, + "table-item-col-0": { + + }, + "#MyName": { + + } +} diff --git a/pype/scripts/slate/style_schema.json b/pype/scripts/slate/style_schema.json new file mode 100644 index 0000000000..dab6151df5 --- /dev/null +++ b/pype/scripts/slate/style_schema.json @@ -0,0 +1,44 @@ +{ + "alignment": { + "description": "Alignment of item", + "type": "string", + "enum": ["left", "right", "center"], + "example": "left" + }, + "font-family": { + "description": "Font type", + "type": "string", + "example": "Arial" + }, + "font-size": { + "description": "Font size", + "type": "integer", + "example": 26 + }, + "font-color": { + "description": "Font color", + "type": "string", + "regex": ( + "^[#]{1}[a-fA-F0-9]{1}$" + " | ^[#]{1}[a-fA-F0-9]{3}$" + " | ^[#]{1}[a-fA-F0-9]{6}$" + ), + "example": "#ffffff" + }, + "font-bold": { + "description": "Font boldness", + "type": "boolean", + "example": True + }, + "bg-color": { + "description": "Background color, None means transparent", + "type": ["string", None], + "regex": ( + "^[#]{1}[a-fA-F0-9]{1}$" + " | ^[#]{1}[a-fA-F0-9]{3}$" + " | ^[#]{1}[a-fA-F0-9]{6}$" + ), + "example": "#333" + }, + "bg-alter-color": None, +} From 43fbdbf8fe95809b46f95d361cfd817364197a64 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jan 2020 12:45:46 +0100 Subject: [PATCH 05/99] added margins --- pype/scripts/slate/base.py | 395 +++++++++++++++++++------------------ 1 file changed, 205 insertions(+), 190 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 5ae7109ed4..bb5d127a2d 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -9,6 +9,15 @@ class BaseObj: obj_type = None available_parents = [] + all_style_keys = [ + "font-family", "font-size", "font-color", "font-bold", "font-italic", + "bg-color", "bg-alter-color", + "alignment-vertical", "alignment-horizontal", + "padding", "padding-left", "padding-right", + "padding-top", "padding-bottom", + "margin", "margin-left", "margin-right", + "margin-top", "margin-bottom", "width", "height" + ] def __init__(self, parent, style={}, name=None): if not self.obj_type: @@ -34,9 +43,7 @@ class BaseObj: self._style = style self.id = uuid4() - self.parent = parent self.name = name - self._style = style self.items = {} self.final_style = None @@ -51,18 +58,41 @@ class BaseObj: "font-italic": False, "bg-color": None, "bg-alter-color": None, - "alignment-vertical": "left", + "alignment-vertical": "right", "alignment-horizontal": "top", "padding": 0, "margin": 0, }, - "layer": {}, - "image": {}, - "text": {}, - "table": {}, - "table-item": {}, - "table-item-col-0": {}, - "#MyName": {} + "main_frame": { + "padding": 0, + "margin": 0 + }, + "layer": { + "padding": 0, + "margin": 0 + }, + "image": { + "padding": 0, + "margin": 0 + }, + "text": { + # "alignment-vertical": "left", + # "alignment-horizontal": "top", + "padding": 0, + "margin": 0 + }, + "table": { + "padding": 0, + "margin": 0 + }, + "table-item": { + "padding": 0, + "margin": 0 + }, + "__not_implemented__": { + "table-item-col-0": {}, + "#MyName": {} + } } return default_style_v1 @@ -97,10 +127,18 @@ class BaseObj: style = self.main_style for key, value in self._style.items(): - if key in style: - style[key].update(value) - else: + if key in self.all_style_keys: + # TODO which variant is right? style[self.obj_type][key] = value + # style["*"][key] = value + else: + if key not in style: + style[key] = {} + + if isinstance(style[key], dict): + style[key].update(value) + else: + style[key] = value if self.is_drawing: self.final_style = style @@ -110,9 +148,8 @@ class BaseObj: @property def style(self): style = self.full_style - style.update(self._style) - base = style.get("*", style.get("global")) or {} + base = style.get("*") or {} obj_specific = style.get(self.obj_type) or {} name_specific = {} if self.name: @@ -128,49 +165,121 @@ class BaseObj: return output - @property - def pos_x(self): + def item_pos_x(self): return 0 @property - def pos_y(self): + def item_pos_y(self): return 0 @property - def pos_start(self): - return (self.pos_x, self.pos_y) + def content_pos_x(self): + pos_x = self.item_pos_x + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + margin_right = self.style.get("margin-right") or margin + return pos_x + margin_left @property - def pos_end(self): - pos_x, pos_y = self.pos_start + def content_pos_y(self): + pos_y = self.item_pos_y + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + margin_bottom = self.style.get("margin-bottom") or margin + return pos_y + margin_top + + @property + def value_pos_x(self): + pos_x = self.content_pos_x * 1 + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + pos_x += padding_left + + return pos_x + + @property + def value_pos_y(self): + pos_y = self.content_pos_y * 1 + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or padding + pos_y += padding_top + + return pos_y + + @property + def value_pos_start(self): + return (self.value_pos_x, self.value_pos_y) + + @property + def value_pos_end(self): + pos_x, pos_y = self.value_pos_start pos_x += self.width pos_y += self.height return (pos_x, pos_y) @property - def max_width(self): - return self.style.get("max-width") or (self.width * 1) + def content_pos_start(self): + return (self.content_pos_x, self.content_pos_y) @property - def max_height(self): - return self.style.get("max-height") or (self.height * 1) + def content_pos_end(self): + pos_x, pos_y = self.content_pos_start + pos_x += self.content_width + pos_y += self.content_height + return (pos_x, pos_y) @property - def max_content_width(self): - width = self.max_width + def value_width(self): + raise NotImplementedError( + "Attribute is not implemented <{}>".format( + self.__class__.__name__ + ) + ) + + @property + def value_height(self): + raise NotImplementedError( + "Attribute is not implemented for <{}>".format( + self.__class__.__name__ + ) + ) + + @property + def content_width(self): + width = self.value_width padding = self.style["padding"] padding_left = self.style.get("padding-left") or padding padding_right = self.style.get("padding-right") or padding - return (width - (padding_left + padding_right)) + return width + padding_left + padding_right @property - def max_content_height(self): - height = self.max_height + def content_height(self): + height = self.value_height padding = self.style["padding"] padding_top = self.style.get("padding-top") or padding padding_bottom = self.style.get("padding-bottom") or padding - return (height - (padding_top + padding_bottom)) + return height + padding_top + padding_bottom + + @property + def width(self): + width = self.content_width + + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + margin_right = self.style.get("margin-right") or margin + + return width + margin_left + margin_right + + @property + def height(self): + height = self.content_height + + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + margin_bottom = self.style.get("margin-bottom") or margin + + return height + margin_bottom + margin_top def add_item(self, item): self.items[item.id] = item @@ -187,11 +296,26 @@ class MainFrame(BaseObj): available_parents = [None] def __init__(self, width, height, *args, **kwargs): + kwargs["parent"] = None super(MainFrame, self).__init__(*args, **kwargs) self._width = width self._height = height self._is_drawing = False + @property + def value_width(self): + width = 0 + for item in self.items.values(): + width += item.width + return width + + @property + def value_height(self): + height = 0 + for item in self.items.values(): + height += item.height + return height + @property def width(self): return self._width @@ -226,19 +350,19 @@ class Layer(BaseObj): self._direction = direction @property - def pos_x(self): + def item_pos_x(self): if self.parent.obj_type == self.obj_type: - pos_x = self.parent.item_pos_x(self.id) + pos_x = self.parent.child_pos_x(self.id) else: - pos_x = self.parent.pos_x + pos_x = self.parent.value_pos_x return pos_x @property - def pos_y(self): + def item_pos_y(self): if self.parent.obj_type == self.obj_type: - pos_y = self.parent.item_pos_y(self.id) + pos_y = self.parent.child_pos_y(self.id) else: - pos_y = self.parent.pos_y + pos_y = self.parent.value_pos_y return pos_y @property @@ -250,30 +374,21 @@ class Layer(BaseObj): return 0 return self._direction - def item_pos_x(self, item_id): - pos_x = self.pos_x + def child_pos_x(self, item_id): + pos_x = self.value_pos_x - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - - pos_x += margin_left if self.direction != 0: for id, item in self.items.items(): - if id == item_id: + if item_id == id: break - pos_x += item.width + pos_x += item.height if item.obj_type != "image": pos_x += 1 - return pos_x - def item_pos_y(self, item_id): - pos_y = self.pos_y + def child_pos_y(self, item_id): + pos_y = self.value_pos_y - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - - pos_y += margin_top if self.direction != 1: for id, item in self.items.items(): if item_id == id: @@ -281,21 +396,15 @@ class Layer(BaseObj): pos_y += item.height if item.obj_type != "image": pos_y += 1 - return pos_y @property - def height(self): - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - margin_bottom = self.style.get("margin-bottom") or margin - - height = (margin_top + margin_bottom) + def value_height(self): + height = 0 for item in self.items.values(): if self.direction == 0: if height > item.height: continue - # times 1 because won't get object pointer but number height = item.height * 1 else: @@ -308,24 +417,19 @@ class Layer(BaseObj): return height @property - def width(self): - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - margin_right = self.style.get("margin-right") or margin - - width = (margin_left + margin_right) + def value_width(self): + width = 0 for item in self.items.values(): if self.direction == 0: if width > item.width: continue - # times 1 because won't get object pointer but number width = item.width * 1 else: width += item.width min_width = self.style.get("min-width") - if min_width > width: + if min_width and min_width > width: return min_width return width @@ -344,80 +448,12 @@ class BaseItem(BaseObj): super(BaseItem, self).__init__(*args, **kwargs) @property - def pos_x(self): - return self.parent.item_pos_x(self.id) + def item_pos_x(self): + return self.parent.child_pos_x(self.id) @property - def pos_y(self): - return self.parent.item_pos_y(self.id) - - @property - def content_pos_x(self): - padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding - - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - - return self.pos_x + margin_left + padding_left - - @property - def content_pos_y(self): - padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding - - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - - return self.pos_y + margin_top + padding_top - - @property - def content_width(self): - raise NotImplementedError( - "Attribute is not implemented" - ) - - @property - def content_height(self): - raise NotImplementedError( - "Attribute is not implemented" - ) - - @property - def width(self): - width = self.content_width - - padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding - padding_right = self.style.get("padding-right") or padding - - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - margin_right = self.style.get("margin-right") or margin - - return ( - width + - margin_left + margin_right + - padding_left + padding_right - ) - - @property - def height(self): - height = self.content_height - - padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding - padding_bottom = self.style.get("padding-bottom") or padding - - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - margin_bottom = self.style.get("margin-bottom") or margin - - return ( - height - + margin_bottom + margin_top - + padding_top + padding_bottom - ) + def item_pos_y(self): + return self.parent.child_pos_y(self.id) def add_item(self, *args, **kwargs): raise Exception("Can't add item to an item, use layers instead.") @@ -440,20 +476,20 @@ class ItemImage(BaseItem): def draw(self, image, drawer): paste_image = Image.open(self.image_path) paste_image = paste_image.resize( - (self.content_width, self.content_height), + (self.value_width, self.value_height), Image.ANTIALIAS ) image.paste( paste_image, - (self.content_pos_x, self.content_pos_y) + (self.value_pos_x, self.value_pos_y) ) @property - def content_width(self): + def value_width(self): return self.style.get("width") @property - def content_height(self): + def value_height(self): return self.style.get("height") @@ -467,35 +503,27 @@ class ItemText(BaseItem): def draw(self, image, drawer): bg_color = self.style["bg-color"] if bg_color and bg_color.lower() != "transparent": - padding = self.style["padding"] - - padding_left = self.style.get("padding-left") or padding - padding_right = self.style.get("padding-right") or padding - padding_top = self.style.get("padding-top") or padding - padding_bottom = self.style.get("padding-bottom") or padding # TODO border outline styles drawer.rectangle( - (self.pos_start, self.pos_end), + (self.content_pos_start, self.content_pos_end), fill=bg_color, outline=None ) - text_pos_start = (self.content_pos_x, self.content_pos_y) - font_color = self.style["font-color"] font_family = self.style["font-family"] font_size = self.style["font-size"] font = ImageFont.truetype(font_family, font_size) drawer.text( - text_pos_start, + self.value_pos_start, self.value, font=font, fill=font_color ) @property - def content_width(self): + def value_width(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -504,7 +532,7 @@ class ItemText(BaseItem): return width @property - def content_height(self): + def value_height(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -571,15 +599,9 @@ class ItemTable(BaseItem): def draw(self, image, drawer): bg_color = self.style["bg-color"] if bg_color and bg_color.lower() != "transparent": - padding = self.style["padding"] - - padding_left = self.style.get("padding-left") or padding - padding_right = self.style.get("padding-right") or padding - padding_top = self.style.get("padding-top") or padding - padding_bottom = self.style.get("padding-bottom") or padding # TODO border outline styles drawer.rectangle( - (self.pos_start, self.pos_end), + (self.content_pos_start, self.content_pos_end), fill=bg_color, outline=None ) @@ -588,7 +610,7 @@ class ItemTable(BaseItem): value.draw(image, drawer) @property - def content_width(self): + def value_width(self): row_heights, col_widths = self.size_values width = 0 for _width in col_widths: @@ -599,7 +621,7 @@ class ItemTable(BaseItem): return width @property - def content_height(self): + def value_height(self): row_heights, col_widths = self.size_values height = 0 for _height in row_heights: @@ -611,8 +633,8 @@ class ItemTable(BaseItem): def pos_info_by_cord(self, cord_x, cord_y): row_heights, col_widths = self.size_values - pos_x = self.pos_x - pos_y = self.pos_y + pos_x = self.value_pos_x + pos_y = self.value_pos_y width = 0 height = 0 for idx, value in enumerate(col_widths): @@ -627,12 +649,6 @@ class ItemTable(BaseItem): break pos_y += value - padding = self.style["padding"] - - padding_left = self.style.get("padding-left") or padding - padding_top = self.style.get("padding-top") or padding - pos_x += padding_left - pos_y += padding_top return (pos_x, pos_y, width, height) def filled_style(self, cord_x, cord_y): @@ -652,7 +668,7 @@ class TableField(BaseItem): self.value = value @property - def content_width(self): + def value_width(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -661,7 +677,7 @@ class TableField(BaseItem): return width @property - def content_height(self): + def value_height(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -669,15 +685,19 @@ class TableField(BaseItem): height = font.getsize(self.value)[1] + 1 return height - def content_pos_x(self, pos_x, width): - padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding - return pos_x + padding_left + @property + def item_pos_x(self): + pos_x, pos_y, width, height = ( + self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + ) + return pos_x - def content_pos_y(self, pos_y, height): - padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding - return pos_y + padding_top + @property + def item_pos_y(self): + pos_x, pos_y, width, height = ( + self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + ) + return pos_y def draw(self, image, drawer): pos_x, pos_y, width, height = ( @@ -700,18 +720,13 @@ class TableField(BaseItem): outline=None ) - text_pos_start = ( - self.content_pos_x(pos_x, width), - self.content_pos_y(pos_y, height) - ) - font_color = self.style["font-color"] font_family = self.style["font-family"] font_size = self.style["font-size"] font = ImageFont.truetype(font_family, font_size) drawer.text( - text_pos_start, + self.value_pos_start, self.value, font=font, fill=font_color From 3ef67998f7b3c0c2e80453e0d3dc18bb116bd66c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 13 Jan 2020 16:34:15 +0100 Subject: [PATCH 06/99] added initial alignment --- pype/scripts/slate/base.py | 231 +++++++++++++++++++++++++++---------- 1 file changed, 173 insertions(+), 58 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index bb5d127a2d..84b8faf0da 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -12,7 +12,7 @@ class BaseObj: all_style_keys = [ "font-family", "font-size", "font-color", "font-bold", "font-italic", "bg-color", "bg-alter-color", - "alignment-vertical", "alignment-horizontal", + "alignment-horizontal", "alignment-vertical", "padding", "padding-left", "padding-right", "padding-top", "padding-bottom", "margin", "margin-left", "margin-right", @@ -56,10 +56,10 @@ class BaseObj: "font-color": "#ffffff", "font-bold": False, "font-italic": False, - "bg-color": None, - "bg-alter-color": None, - "alignment-vertical": "right", - "alignment-horizontal": "top", + "bg-color": "#0077ff", + "bg-alter-color": "#0055dd", + "alignment-horizontal": "center", + "alignment-vertical": "bottom", "padding": 0, "margin": 0, }, @@ -76,8 +76,6 @@ class BaseObj: "margin": 0 }, "text": { - # "alignment-vertical": "left", - # "alignment-horizontal": "top", "padding": 0, "margin": 0 }, @@ -86,6 +84,7 @@ class BaseObj: "margin": 0 }, "table-item": { + "alignment-horizontal": "right", "padding": 0, "margin": 0 }, @@ -178,15 +177,16 @@ class BaseObj: pos_x = self.item_pos_x margin = self.style["margin"] margin_left = self.style.get("margin-left") or margin - margin_right = self.style.get("margin-right") or margin - return pos_x + margin_left + + pos_x += margin_left + + return pos_x @property def content_pos_y(self): pos_y = self.item_pos_y margin = self.style["margin"] margin_top = self.style.get("margin-top") or margin - margin_bottom = self.style.get("margin-bottom") or margin return pos_y + margin_top @property @@ -281,6 +281,30 @@ class BaseObj: return height + margin_bottom + margin_top + # @property + # def max_width(self): + # return self.style.get("max-width") or self.width + # + # @property + # def max_height(self): + # return self.style.get("max-height") or self.height + # + # @property + # def max_content_width(self): + # width = self.max_width + # padding = self.style["padding"] + # padding_left = self.style.get("padding-left") or padding + # padding_right = self.style.get("padding-right") or padding + # return (width - (padding_left + padding_right)) + # + # @property + # def max_content_height(self): + # height = self.max_height + # padding = self.style["padding"] + # padding_top = self.style.get("padding-top") or padding + # padding_bottom = self.style.get("padding-bottom") or padding + # return (height - (padding_top + padding_bottom)) + def add_item(self, item): self.items[item.id] = item @@ -344,7 +368,7 @@ class Layer(BaseObj): obj_type = "layer" available_parents = ["main_frame", "layer"] - # Direction can be 0=horizontal/ 1=vertical + # Direction can be 0=vertical/ 1=horizontal def __init__(self, direction=0, *args, **kwargs): super(Layer, self).__init__(*args, **kwargs) self._direction = direction @@ -369,25 +393,51 @@ class Layer(BaseObj): def direction(self): if self._direction not in (0, 1): print( - "Direction must be 0 or 1 (0 is Vertical / 1 is horizontal)!" + "Direction must be 0 or 1 (0 is horizontal / 1 is vertical)!" ) return 0 return self._direction def child_pos_x(self, item_id): pos_x = self.value_pos_x + alignment_hor = self.style["alignment-horizontal"].lower() + + item = None + for id, _item in self.items.items(): + if item_id == id: + item = _item + break if self.direction != 0: - for id, item in self.items.items(): + for id, _item in self.items.items(): if item_id == id: break - pos_x += item.height - if item.obj_type != "image": + pos_x += _item.height + if _item.obj_type != "image": pos_x += 1 - return pos_x + + else: + if alignment_hor in ["center", "centre"]: + pos_x += (self.content_width - item.content_width) / 2 + + elif alignment_hor == "right": + pos_x += self.content_width - item.content_width + + else: + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + pos_x += margin_left + return int(pos_x) def child_pos_y(self, item_id): pos_y = self.value_pos_y + alignment_ver = self.style["alignment-horizontal"].lower() + + item = None + for id, _item in self.items.items(): + if item_id == id: + item = _item + break if self.direction != 1: for id, item in self.items.items(): @@ -396,7 +446,19 @@ class Layer(BaseObj): pos_y += item.height if item.obj_type != "image": pos_y += 1 - return pos_y + + else: + if alignment_ver in ["center", "centre"]: + pos_y += (self.content_height - item.content_height) / 2 + + elif alignment_ver == "bottom": + pos_y += self.content_height - item.content_height + + else: + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + pos_y += margin_top + return int(pos_y) @property def value_height(self): @@ -545,7 +607,7 @@ class ItemTable(BaseItem): obj_type = "table" - def __init__(self, values, *args, **kwargs): + def __init__(self, values, use_alternate_color=False, *args, **kwargs): super(ItemTable, self).__init__(*args, **kwargs) self.size_values = None self.values_by_cords = None @@ -553,22 +615,28 @@ class ItemTable(BaseItem): self.prepare_values(values) self.calculate_sizes() + self.use_alternate_color = use_alternate_color + def prepare_values(self, _values): values = [] values_by_cords = [] - for row_idx, row in enumerate(_values): - if len(values_by_cords) < row_idx + 1: - for i in range(len(values_by_cords), row_idx + 1): - values_by_cords.append([]) + row_count = 0 + col_count = 0 + for row in _values: + row_count += 1 + if len(row) > col_count: + col_count = len(row) - for col_idx, col in enumerate(_values[row_idx]): - if len(values_by_cords[row_idx]) < col_idx + 1: - for i in range(len(values_by_cords[row_idx]), col_idx + 1): - values_by_cords[row_idx].append("") - - if not col: + for row_idx in range(row_count): + values_by_cords.append([]) + for col_idx in range(col_count): + values_by_cords[row_idx].append([]) + if col_idx <= len(_values[row_idx]) - 1: + col = _values[row_idx][col_idx] + else: col = "" - col_item = TableField(row_idx, col_idx, col, self) + + col_item = TableField(row_idx, col_idx, col, parent=self) values_by_cords[row_idx][col_idx] = col_item values.append(col_item) @@ -631,7 +699,7 @@ class ItemTable(BaseItem): height -= 1 return height - def pos_info_by_cord(self, cord_x, cord_y): + def content_pos_info_by_cord(self, cord_x, cord_y): row_heights, col_widths = self.size_values pos_x = self.value_pos_x pos_y = self.value_pos_y @@ -669,6 +737,9 @@ class TableField(BaseItem): @property def value_width(self): + if not self.value: + return 0 + font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -678,6 +749,8 @@ class TableField(BaseItem): @property def value_height(self): + if not self.value: + return 0 font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -688,31 +761,68 @@ class TableField(BaseItem): @property def item_pos_x(self): pos_x, pos_y, width, height = ( - self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) ) return pos_x @property def item_pos_y(self): pos_x, pos_y, width, height = ( - self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) ) return pos_y + @property + def value_pos_x(self): + pos_x, pos_y, width, height = ( + self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + ) + + alignment_hor = self.style["alignment-horizontal"].lower() + if alignment_hor in ["center", "centre"]: + pos_x += (width - self.value_width) / 2 + + elif alignment_hor == "right": + pos_x += width - self.value_width + + else: + padding = self.style["padding"] + padding_left = self.style.get("padding-left") or padding + pos_x += padding_left + + return int(pos_x) + + @property + def value_pos_y(self): + pos_x, pos_y, width, height = ( + self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + ) + + alignment_ver = self.style["alignment-vertical"].lower() + if alignment_ver in ["center", "centre"]: + pos_y += (height - self.value_height) / 2 + + elif alignment_ver == "bottom": + pos_y += height - self.value_height + + else: + padding = self.style["padding"] + padding_top = self.style.get("padding-top") or padding + pos_y += padding_top + + return int(pos_y) + def draw(self, image, drawer): pos_x, pos_y, width, height = ( - self.parent.pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) ) pos_start = (pos_x, pos_y) pos_end = (pos_x + width, pos_y + height) bg_color = self.style["bg-color"] - if bg_color and bg_color.lower() != "transparent": - padding = self.style["padding"] + if self.parent.use_alternate_color and (self.cord_x % 2) == 1: + bg_color = self.style["bg-alter-color"] - padding_left = self.style.get("padding-left") or padding - padding_right = self.style.get("padding-right") or padding - padding_top = self.style.get("padding-top") or padding - padding_bottom = self.style.get("padding-bottom") or padding + if bg_color and bg_color.lower() != "transparent": # TODO border outline styles drawer.rectangle( (pos_start, pos_end), @@ -734,48 +844,53 @@ class TableField(BaseItem): if __name__ == "__main__": main_style = { - "bg-color": "#777777" + "bg-color": "#777777", + "margin": 0 } text_1_style = { - "padding": 10, + "padding": 0, "bg-color": "#00ff77" } text_2_style = { - "padding": 8, + "padding": 0, "bg-color": "#ff0066" } text_3_style = { "padding": 0, "bg-color": "#ff5500" } - main = MainFrame(1920, 1080, style=main_style) - layer = Layer(parent=main) - main.add_item(layer) - - text_1 = ItemText("Testing message 1", layer, text_1_style) - text_2 = ItemText("Testing message 2", layer, text_2_style) - text_3 = ItemText("Testing message 3", layer, text_3_style) - table_1_items = [["0", "Output text 1"], ["1", "Output text 2"], ["2", "Output text 3"]] - table_1_style = { - "padding": 8, - "bg-color": "#0077ff" - } - table_1 = ItemTable(table_1_items, layer, table_1_style) - image_1_style = { "width": 240, "height": 120, "bg-color": "#7733aa" } + table_1_style = { + "padding": 0, + "bg-color": "#0077ff" + } + + main = MainFrame(1920, 1080, style=main_style) + layer = Layer(parent=main) + main.add_item(layer) + + text_1 = ItemText("Testing message 1", layer, text_1_style) + text_2 = ItemText("Testing 2", layer, text_2_style) + text_3 = ItemText("Testing message 3", layer, text_3_style) + + table_1_items = [["0", "Output text 1", "ha"], ["1", "Output 2"], ["2", "Output text 3"]] + table_1 = ItemTable(table_1_items, True, parent=layer, style=table_1_style) + image_1_path = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\kitten.jpg" image_1 = ItemImage(image_1_path, layer, image_1_style) layer.add_item(text_1) layer.add_item(text_2) + layer.add_item(text_3) + layer.add_item(table_1) layer.add_item(image_1) - layer.add_item(text_3) - dst = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\test_output2.png" + + dst = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\test_output3.png" main.draw(dst) print("*** Drawing done :)") From 8899ce974fbfe91c69e87b5a390910a71e044a75 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jan 2020 14:33:59 +0100 Subject: [PATCH 07/99] added important imports --- pype/scripts/slate/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 84b8faf0da..33266bf62d 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -1,4 +1,9 @@ +import os import sys +import re +import copy +import json +from queue import Queue sys.path.append(r"C:\Users\Public\pype_env2\Lib\site-packages") from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageColor from uuid import uuid4 From de2d156fc4955f1d4e246a5a0b74b4e5b263d58b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jan 2020 14:34:26 +0100 Subject: [PATCH 08/99] base object may have pos_x and pos_y --- pype/scripts/slate/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 33266bf62d..1aca5062be 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -24,7 +24,7 @@ class BaseObj: "margin-top", "margin-bottom", "width", "height" ] - def __init__(self, parent, style={}, name=None): + def __init__(self, parent, style={}, name=None, pos_x=None, pos_y=None): if not self.obj_type: raise NotImplementedError( "Class don't have set object type <{}>".format( @@ -50,7 +50,9 @@ class BaseObj: self.id = uuid4() self.name = name self.items = {} - self.final_style = None + + self._pos_x = pos_x or 0 + self._pos_y = pos_y or 0 @property def main_style(self): From 549898730a60c099b52f958fed5b24d242f59284 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jan 2020 14:35:51 +0100 Subject: [PATCH 09/99] style property use get_style_for_obj_type method --- pype/scripts/slate/base.py | 77 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 1aca5062be..6430e5d8f3 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -151,12 +151,12 @@ class BaseObj: return style - @property - def style(self): - style = self.full_style + def get_style_for_obj_type(self, obj_type, style=None): + if not style: + style = copy.deepcopy(self.full_style) base = style.get("*") or {} - obj_specific = style.get(self.obj_type) or {} + obj_specific = style.get(obj_type) or {} name_specific = {} if self.name: name = str(self.name) @@ -164,6 +164,67 @@ class BaseObj: name += "#" name_specific = style.get(name) or {} + + if obj_type == "table-item": + col_regex = r"table-item-col\[([\d\-, ]+)*\]" + row_regex = r"table-item-row\[([\d\-, ]+)*\]" + field_regex = ( + r"table-item-field\[(([ ]+)?\d+([ ]+)?:([ ]+)?\d+([ ]+)?)*\]" + ) + # STRICT field regex (not allowed spaces) + # fild_regex = r"table-item-field\[(\d+:\d+)*\]" + + def get_indexes_from_regex_match(result, field=False): + group = result.group(1) + indexes = [] + if field: + return [ + int(part.strip()) for part in group.strip().split(":") + ] + + parts = group.strip().split(",") + for part in parts: + part = part.strip() + if "-" not in part: + indexes.append(int(part)) + continue + + sub_parts = [ + int(sub.strip()) for sub in part.split("-") + ] + if len(sub_parts) != 2: + # TODO logging + print("invalid range '{}'".format(part)) + continue + + for idx in range(sub_parts[0], sub_parts[1]+1): + indexes.append(idx) + return indexes + + for key, value in style.items(): + if not key.startswith(obj_type): + continue + + result = re.search(col_regex, key) + if result: + indexes = get_indexes_from_regex_match(result) + if self.col_idx in indexes: + obj_specific.update(value) + continue + + result = re.search(row_regex, key) + if result: + indexes = get_indexes_from_regex_match(result) + if self.row_idx in indexes: + obj_specific.update(value) + continue + + result = re.search(field_regex, key) + if result: + col_idx, row_idx = get_indexes_from_regex_match(result) + if self.row_idx == col_idx and self.row_idx == row_idx: + obj_specific.update(value) + output = {} output.update(base) output.update(obj_specific) @@ -171,12 +232,20 @@ class BaseObj: return output + @property + def style(self): + return self.get_style_for_obj_type(self.obj_type) + @property def item_pos_x(self): + if self.parent.obj_type == "main_frame": + return self._pos_x return 0 @property def item_pos_y(self): + if self.parent.obj_type == "main_frame": + return self._pos_x return 0 @property From 7d40f571d58455d724a03f37fa190ed9ccc80184 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jan 2020 14:39:02 +0100 Subject: [PATCH 10/99] removed final_style, is_drawing and cleaned up little bit --- pype/scripts/slate/base.py | 54 +++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 6430e5d8f3..737d92de67 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -102,10 +102,6 @@ class BaseObj: } return default_style_v1 - @property - def is_drawing(self): - return self.parent.is_drawing - @property def height(self): raise NotImplementedError( @@ -124,9 +120,6 @@ class BaseObj: @property def full_style(self): - if self.is_drawing and self.final_style is not None: - return self.final_style - if self.parent is not None: style = dict(val for val in self.parent.full_style.items()) else: @@ -146,9 +139,6 @@ class BaseObj: else: style[key] = value - if self.is_drawing: - self.final_style = style - return style def get_style_for_obj_type(self, obj_type, style=None): @@ -385,7 +375,6 @@ class BaseObj: self.items[item.id] = item def reset(self): - self.final_style = None for item in self.items.values(): item.reset() @@ -395,12 +384,12 @@ class MainFrame(BaseObj): obj_type = "main_frame" available_parents = [None] - def __init__(self, width, height, *args, **kwargs): + def __init__(self, width, height, destination_path=None, *args, **kwargs): kwargs["parent"] = None super(MainFrame, self).__init__(*args, **kwargs) self._width = width self._height = height - self._is_drawing = False + self.dst_path = destination_path @property def value_width(self): @@ -424,19 +413,27 @@ class MainFrame(BaseObj): def height(self): return self._height - @property - def is_drawing(self): - return self._is_drawing + def draw(self, path=None): + if not path: + path = self.dst_path + + if not path: + raise TypeError(( + "draw() missing 1 required positional argument: 'path'" + " if 'destination_path is not specified'" + )) + + dir_path = os.path.dirname(path) + if not os.path.exists(dir_path): + os.makedirs(dir_path) - def draw(self, path): - self._is_drawing = True bg_color = self.style["bg-color"] - image = Image.new("RGB", (self.width, self.height), color=bg_color) + image = Image.new("RGB", (self.width(), self.height()), color=bg_color) + drawer = ImageDraw.Draw(image) for item in self.items.values(): - item.draw(image) + item.draw(image, drawer) image.save(path) - self._is_drawing = False self.reset() @@ -571,26 +568,27 @@ class Layer(BaseObj): return min_width return width - def draw(self, image, drawer=None): - if drawer is None: - drawer = ImageDraw.Draw(image) - + def draw(self, image, drawer): for item in self.items.values(): item.draw(image, drawer) class BaseItem(BaseObj): - available_parents = ["layer"] + available_parents = ["main_frame", "layer"] def __init__(self, *args, **kwargs): super(BaseItem, self).__init__(*args, **kwargs) @property def item_pos_x(self): + if self.parent.obj_type == "main_frame": + return self._pos_x return self.parent.child_pos_x(self.id) @property def item_pos_y(self): + if self.parent.obj_type == "main_frame": + return self._pos_y return self.parent.child_pos_y(self.id) def add_item(self, *args, **kwargs): @@ -795,10 +793,6 @@ class ItemTable(BaseItem): return (pos_x, pos_y, width, height) - def filled_style(self, cord_x, cord_y): - # TODO CHANGE STYLE BY CORDS - return self.style - class TableField(BaseItem): From 2b199538c461381d5939b8291d1fc1f1653988c1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:26:47 +0100 Subject: [PATCH 11/99] return self._pos_x if parent in main_frame --- pype/scripts/slate/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 737d92de67..e985d9fd04 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -450,6 +450,8 @@ class Layer(BaseObj): def item_pos_x(self): if self.parent.obj_type == self.obj_type: pos_x = self.parent.child_pos_x(self.id) + elif self.parent.obj_type == "main_frame": + pos_x = self._pos_x else: pos_x = self.parent.value_pos_x return pos_x @@ -458,6 +460,8 @@ class Layer(BaseObj): def item_pos_y(self): if self.parent.obj_type == self.obj_type: pos_y = self.parent.child_pos_y(self.id) + elif self.parent.obj_type == "main_frame": + pos_y = self._pos_y else: pos_y = self.parent.value_pos_y return pos_y From ef609344dfddc2b0dd356362eac8c295b7c2bf5f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:30:12 +0100 Subject: [PATCH 12/99] initial netflix version json --- pype/scripts/slate/netflix_v01.1.json | 141 ++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 pype/scripts/slate/netflix_v01.1.json diff --git a/pype/scripts/slate/netflix_v01.1.json b/pype/scripts/slate/netflix_v01.1.json new file mode 100644 index 0000000000..a2be6d13e0 --- /dev/null +++ b/pype/scripts/slate/netflix_v01.1.json @@ -0,0 +1,141 @@ +{ + "width": 1920, + "height": 1080, + "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v001.png", + "style": { + "*": { + "font-family": "arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": false, + "font-italic": false, + "bg-color": "#0077ff", + "alignment-horizontal": "left", + "alignment-vertical": "top", + "padding": 0, + "margin": 0 + }, + "rectangle": { + "padding": 0, + "margin": 0, + "bg-color": "#E9324B", + "fill": true + }, + "main_frame": { + "bg-color": "#252525" + }, + "table": { + "bg-color": "transparent" + }, + "table-item": { + "bg-color": "#272727", + "bg-alter-color": "#212121", + "font-color": "#ffffff", + "font-bold": true, + "font-italic": false, + "padding": 5, + "padding-bottom": 10, + "alignment-vertical": "top", + "alignment-horizontal": "left", + "word-wrap": true, + "ellide": true, + "max-lines": 3 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989" + }, + "table-item-col[1]": { + "font-size": 30, + "padding-left": 20 + } + }, + "items": [{ + "type": "layer", + "name": "LeftSide", + "pos_x": 40, + "pos_y": 40, + "style": { + "width": 1094, + "height": 1000 + }, + "items": [{ + "type": "table", + "values": [ + ["Show:", "First Contact"] + ], + "style": { + "table-item-col[0]": { + "width": 300 + } + } + }, { + "type": "rectangle", + "style": { + "bg-color": "#d40914", + "width": 1094, + "height": 5, + "fill": true + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Version name:", "mei_101_001_0020_slate_NFX_v001"], + ["Date:", "2019-08-09"], + ["Shot Types:", "2d comp"], + ["Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production."] + ], + "style": { + "table-item-col[0]": { + "alignment-horizontal": "right", + "width": 300 + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "width": 786 + } + } + }] + }, { + "type": "layer", + "name": "RightSide", + "pos_x": 1174, + "pos_y": 40, + "style": { + "width": 733, + "height": 1000 + }, + "items": [{ + "type": "image", + "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", + "style": { + "width": 733, + "height": 414 + } + }, { + "type": "image", + "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", + "style": { + "width": 733, + "height": 55 + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Vendor:", "DAZZLE"], + ["Shot Name:", "SLATE_SIMPLE"], + ["Frames:", "0 - 1 (2)"] + ], + "style": { + "table-item-col[0]": { + "alignment-horizontal": "left" + }, + "table-item-col[1]": { + "alignment-horizontal": "right" + } + } + }] + }] +} From b872ee29919dd568e9ae935870e6ea6f708028b8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:30:54 +0100 Subject: [PATCH 13/99] added argument when processing field style --- pype/scripts/slate/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index e985d9fd04..467b504ca1 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -211,7 +211,9 @@ class BaseObj: result = re.search(field_regex, key) if result: - col_idx, row_idx = get_indexes_from_regex_match(result) + col_idx, row_idx = get_indexes_from_regex_match( + result, True + ) if self.row_idx == col_idx and self.row_idx == row_idx: obj_specific.update(value) From 8c5fd923fe5e4581e53febb930b7d1ee422b54a8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:32:17 +0100 Subject: [PATCH 14/99] created FontFactory --- pype/scripts/slate/base.py | 201 ++++++++++++++++++++++++++++++++----- 1 file changed, 174 insertions(+), 27 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 467b504ca1..37d59f13bf 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -918,32 +918,102 @@ class TableField(BaseItem): fill=font_color ) -if __name__ == "__main__": - main_style = { - "bg-color": "#777777", - "margin": 0 - } - text_1_style = { - "padding": 0, - "bg-color": "#00ff77" - } - text_2_style = { - "padding": 0, - "bg-color": "#ff0066" - } - text_3_style = { - "padding": 0, - "bg-color": "#ff5500" - } - image_1_style = { - "width": 240, - "height": 120, - "bg-color": "#7733aa" - } - table_1_style = { - "padding": 0, - "bg-color": "#0077ff" - } + +class FontFactory: + fonts = None + default = None + + @classmethod + def get_font(cls, family, font_size=None, italic=False, bold=False): + if cls.fonts is None: + cls.load_fonts() + + styles = [] + if bold: + styles.append("Bold") + + if italic: + styles.append("Italic") + + if not styles: + styles.append("Regular") + + style = " ".join(styles) + family = family.lower() + family_styles = cls.fonts.get(family) + if not family_styles: + return cls.default + + font = family_styles.get(style) + if font: + if font_size: + font = font.font_variant(size=font_size) + return font + + # Return first found + for font in family_styles: + if font_size: + font = font.font_variant(size=font_size) + return font + + return cls.default + + @classmethod + def load_fonts(cls): + + cls.default = ImageFont.load_default() + + available_font_ext = [".ttf", ".ttc"] + dirs = [] + if sys.platform == "win32": + # check the windows font repository + # NOTE: must use uppercase WINDIR, to work around bugs in + # 1.5.2's os.environ.get() + windir = os.environ.get("WINDIR") + if windir: + dirs.append(os.path.join(windir, "fonts")) + + elif sys.platform in ("linux", "linux2"): + lindirs = os.environ.get("XDG_DATA_DIRS", "") + if not lindirs: + # According to the freedesktop spec, XDG_DATA_DIRS should + # default to /usr/share + lindirs = "/usr/share" + dirs += [ + os.path.join(lindir, "fonts") for lindir in lindirs.split(":") + ] + + elif sys.platform == "darwin": + dirs += [ + "/Library/Fonts", + "/System/Library/Fonts", + os.path.expanduser("~/Library/Fonts") + ] + + available_fonts = collections.defaultdict(dict) + for directory in dirs: + for walkroot, walkdir, walkfilenames in os.walk(directory): + for walkfilename in walkfilenames: + ext = os.path.splitext(walkfilename)[1] + if ext.lower() not in available_font_ext: + continue + + fontpath = os.path.join(walkroot, walkfilename) + font_obj = ImageFont.truetype(fontpath) + family = font_obj.font.family.lower() + style = font_obj.font.style + available_fonts[family][style] = font_obj + + cls.fonts = available_fonts + + +def main_v01(): + main_style = {"bg-color": "#777777", "margin": 0} + text_1_style = {"padding": 0, "bg-color": "#00ff77"} + text_2_style = {"padding": 0, "bg-color": "#ff0066"} + text_3_style = {"padding": 0, "bg-color": "#ff5500"} + image_1_style = {"width": 240, "height": 120, "bg-color": "#7733aa"} + table_1_style = {"padding": 0, "bg-color": "#0077ff"} main = MainFrame(1920, 1080, style=main_style) layer = Layer(parent=main) @@ -969,4 +1039,81 @@ if __name__ == "__main__": dst = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\test_output3.png" main.draw(dst) - print("*** Drawing done :)") + +def main_v02(): + cur_folder = os.path.dirname(os.path.abspath(__file__)) + input_json = os.path.join(cur_folder, "netflix_v01.1.json") + with open(input_json) as json_file: + slate_data = json.load(json_file) + + width = slate_data["width"] + height = slate_data["height"] + style = slate_data.get("style") or {} + dst_path = slate_data.get("destination_path") + main = MainFrame(width, height, destination_path=dst_path, style=style) + + load_queue = Queue() + for item in slate_data["items"]: + load_queue.put((item, main)) + + all_objs = [] + while not load_queue.empty(): + item_data, parent = load_queue.get() + + item_type = item_data["type"].lower() + item_style = item_data.get("style", {}) + item_name = item_data.get("name") + + pos_x = None + pos_y = None + if parent.obj_type == "main_frame": + pos_x = item_data.get("pos_x", {}) + pos_y = item_data.get("pos_y", {}) + + kwargs = { + "parent": parent, + "style": item_style, + "pos_x": pos_x, + "pos_y": pos_y + } + + item_obj = None + if item_type == "layer": + direction = item_data.get("direction", 0) + item_obj = Layer(direction, **kwargs) + for item in item_data.get("items", []): + load_queue.put((item, item_obj)) + + elif item_type == "table": + use_alternate_color = item_data.get("use_alternate_color", False) + values = item_data.get("values") or [] + item_obj = ItemTable(values, use_alternate_color, **kwargs) + + elif item_type == "image": + path = item_data["path"] + item_obj = ItemImage(path, **kwargs) + + elif item_type == "rectangle": + item_obj = ItemRectangle(**kwargs) + + if not item_obj: + print( + "Slate item not implemented <{}> - skipping".format(item_type) + ) + continue + + all_objs.append(item_obj) + + parent.add_item(item_obj) + + main.draw() + # for item in all_objs: + # print(item.style.get("width"), item.style.get("height")) + # print(item.width, item.height) + # print(item.content_pos_x, item.content_pos_y) + # print(item.value_pos_x, item.value_pos_y) + + +if __name__ == "__main__": + main_v02() + print("*** Drawing is done") From 7a800142077bc2c580366344e3dd739070ae2185 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:37:32 +0100 Subject: [PATCH 15/99] all heights and widths are callable and not property --- pype/scripts/slate/base.py | 82 ++++++++++++++------------------------ 1 file changed, 30 insertions(+), 52 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 37d59f13bf..750061a844 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -102,7 +102,6 @@ class BaseObj: } return default_style_v1 - @property def height(self): raise NotImplementedError( "Attribute `height` is not implemented for <{}>".format( @@ -110,7 +109,6 @@ class BaseObj: ) ) - @property def width(self): raise NotImplementedError( "Attribute `width` is not implemented for <{}>".format( @@ -282,8 +280,8 @@ class BaseObj: @property def value_pos_end(self): pos_x, pos_y = self.value_pos_start - pos_x += self.width - pos_y += self.height + pos_x += self.width() + pos_y += self.height() return (pos_x, pos_y) @property @@ -293,11 +291,10 @@ class BaseObj: @property def content_pos_end(self): pos_x, pos_y = self.content_pos_start - pos_x += self.content_width - pos_y += self.content_height + pos_x += self.content_width() + pos_y += self.content_height() return (pos_x, pos_y) - @property def value_width(self): raise NotImplementedError( "Attribute is not implemented <{}>".format( @@ -305,7 +302,6 @@ class BaseObj: ) ) - @property def value_height(self): raise NotImplementedError( "Attribute is not implemented for <{}>".format( @@ -313,25 +309,22 @@ class BaseObj: ) ) - @property def content_width(self): - width = self.value_width + width = self.value_width() padding = self.style["padding"] padding_left = self.style.get("padding-left") or padding padding_right = self.style.get("padding-right") or padding return width + padding_left + padding_right - @property def content_height(self): - height = self.value_height + height = self.value_height() padding = self.style["padding"] padding_top = self.style.get("padding-top") or padding padding_bottom = self.style.get("padding-bottom") or padding return height + padding_top + padding_bottom - @property def width(self): - width = self.content_width + width = self.content_width() margin = self.style["margin"] margin_left = self.style.get("margin-left") or margin @@ -339,9 +332,8 @@ class BaseObj: return width + margin_left + margin_right - @property def height(self): - height = self.content_height + height = self.content_height() margin = self.style["margin"] margin_top = self.style.get("margin-top") or margin @@ -393,25 +385,21 @@ class MainFrame(BaseObj): self._height = height self.dst_path = destination_path - @property def value_width(self): width = 0 for item in self.items.values(): - width += item.width + width += item.width() return width - @property def value_height(self): height = 0 for item in self.items.values(): - height += item.height + height += item.height() return height - @property def width(self): return self._width - @property def height(self): return self._height @@ -491,16 +479,17 @@ class Layer(BaseObj): for id, _item in self.items.items(): if item_id == id: break - pos_x += _item.height + + pos_x += _item.height() if _item.obj_type != "image": pos_x += 1 else: if alignment_hor in ["center", "centre"]: - pos_x += (self.content_width - item.content_width) / 2 + pos_x += (self.content_width() - item.content_width()) / 2 elif alignment_hor == "right": - pos_x += self.content_width - item.content_width + pos_x += self.content_width() - item.content_width() else: margin = self.style["margin"] @@ -522,16 +511,16 @@ class Layer(BaseObj): for id, item in self.items.items(): if item_id == id: break - pos_y += item.height + pos_y += item.height() if item.obj_type != "image": pos_y += 1 else: if alignment_ver in ["center", "centre"]: - pos_y += (self.content_height - item.content_height) / 2 + pos_y += (self.content_height() - item.content_height()) / 2 elif alignment_ver == "bottom": - pos_y += self.content_height - item.content_height + pos_y += self.content_height() - item.content_height() else: margin = self.style["margin"] @@ -539,35 +528,33 @@ class Layer(BaseObj): pos_y += margin_top return int(pos_y) - @property def value_height(self): height = 0 for item in self.items.values(): if self.direction == 0: - if height > item.height: + if height > item.height(): continue # times 1 because won't get object pointer but number - height = item.height * 1 + height = item.height() else: - height += item.height + height += item.height() min_height = self.style.get("min-height") - if min_height > height: + if min_height and min_height > height: return min_height return height - @property def value_width(self): width = 0 for item in self.items.values(): if self.direction == 0: - if width > item.width: + if width > item.width(): continue # times 1 because won't get object pointer but number - width = item.width * 1 + width = item.width() else: - width += item.width + width += item.width() min_width = self.style.get("min-width") if min_width and min_width > width: @@ -626,11 +613,9 @@ class ItemImage(BaseItem): (self.value_pos_x, self.value_pos_y) ) - @property def value_width(self): return self.style.get("width") - @property def value_height(self): return self.style.get("height") @@ -664,7 +649,6 @@ class ItemText(BaseItem): fill=font_color ) - @property def value_width(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -673,7 +657,6 @@ class ItemText(BaseItem): width = font.getsize(self.value)[0] return width - @property def value_height(self): font_family = self.style["font-family"] font_size = self.style["font-size"] @@ -733,12 +716,12 @@ class ItemTable(BaseItem): col_widths.append(0) _width = col_widths[col_idx] - item_width = col_item.width + item_width = col_item.width() if _width < item_width: col_widths[col_idx] = item_width _height = row_heights[row_idx] - item_height = col_item.height + item_height = col_item.height() if _height < item_height: row_heights[row_idx] = item_height @@ -757,7 +740,6 @@ class ItemTable(BaseItem): for value in self.values: value.draw(image, drawer) - @property def value_width(self): row_heights, col_widths = self.size_values width = 0 @@ -768,7 +750,6 @@ class ItemTable(BaseItem): width -= 1 return width - @property def value_height(self): row_heights, col_widths = self.size_values height = 0 @@ -811,7 +792,6 @@ class TableField(BaseItem): self.cord_y = cord_y self.value = value - @property def value_width(self): if not self.value: return 0 @@ -823,7 +803,6 @@ class TableField(BaseItem): width = font.getsize(self.value)[0] + 1 return width - @property def value_height(self): if not self.value: return 0 @@ -853,13 +832,12 @@ class TableField(BaseItem): pos_x, pos_y, width, height = ( self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) ) - alignment_hor = self.style["alignment-horizontal"].lower() if alignment_hor in ["center", "centre"]: - pos_x += (width - self.value_width) / 2 + pos_x += (width - self.value_width()) / 2 elif alignment_hor == "right": - pos_x += width - self.value_width + pos_x += width - self.value_width() else: padding = self.style["padding"] @@ -876,10 +854,10 @@ class TableField(BaseItem): alignment_ver = self.style["alignment-vertical"].lower() if alignment_ver in ["center", "centre"]: - pos_y += (height - self.value_height) / 2 + pos_y += (height - self.value_height()) / 2 elif alignment_ver == "bottom": - pos_y += height - self.value_height + pos_y += height - self.value_height() else: padding = self.style["padding"] From 25cb9b82896e343ca76793f47582fcfb7107fc54 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:38:30 +0100 Subject: [PATCH 16/99] added rectangle item --- pype/scripts/slate/base.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 750061a844..4029379655 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -618,6 +618,35 @@ class ItemImage(BaseItem): def value_height(self): return self.style.get("height") +class ItemRectangle(BaseItem): + obj_type = "rectangle" + + def draw(self, image, drawer): + bg_color = self.style["bg-color"] + fill = self.style.get("fill", False) + kwargs = {} + if fill: + kwargs["fill"] = bg_color + else: + kwargs["outline"] = bg_color + + start_pos_x = self.value_pos_x + start_pos_y = self.value_pos_y + end_pos_x = start_pos_x + self.value_width() + end_pos_y = start_pos_y + self.value_height() + drawer.rectangle( + ( + (start_pos_x, start_pos_y), + (end_pos_x, end_pos_y) + ), + **kwargs + ) + + def value_width(self): + return int(self.style["width"]) + + def value_height(self): + return int(self.style["height"]) class ItemText(BaseItem): From 184e1f17615fbfd3b35f3565ab3262b023cedcde Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:39:37 +0100 Subject: [PATCH 17/99] cleanup --- pype/scripts/slate/base.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 4029379655..7912b93026 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -21,7 +21,8 @@ class BaseObj: "padding", "padding-left", "padding-right", "padding-top", "padding-bottom", "margin", "margin-left", "margin-right", - "margin-top", "margin-bottom", "width", "height" + "margin-top", "margin-bottom", "width", "height", + "fill" ] def __init__(self, parent, style={}, name=None, pos_x=None, pos_y=None): @@ -229,13 +230,13 @@ class BaseObj: @property def item_pos_x(self): if self.parent.obj_type == "main_frame": - return self._pos_x + return int(self._pos_x) return 0 @property def item_pos_y(self): if self.parent.obj_type == "main_frame": - return self._pos_x + return int(self._pos_y) return 0 @property @@ -257,7 +258,7 @@ class BaseObj: @property def value_pos_x(self): - pos_x = self.content_pos_x * 1 + pos_x = int(self.content_pos_x) padding = self.style["padding"] padding_left = self.style.get("padding-left") or padding pos_x += padding_left @@ -266,7 +267,7 @@ class BaseObj: @property def value_pos_y(self): - pos_y = self.content_pos_y * 1 + pos_y = int(self.content_pos_y) padding = self.style["padding"] padding_top = self.style.get("padding-top") or padding pos_y += padding_top @@ -444,7 +445,7 @@ class Layer(BaseObj): pos_x = self._pos_x else: pos_x = self.parent.value_pos_x - return pos_x + return int(pos_x) @property def item_pos_y(self): @@ -454,7 +455,7 @@ class Layer(BaseObj): pos_y = self._pos_y else: pos_y = self.parent.value_pos_y - return pos_y + return int(pos_y) @property def direction(self): @@ -569,8 +570,6 @@ class Layer(BaseObj): class BaseItem(BaseObj): available_parents = ["main_frame", "layer"] - def __init__(self, *args, **kwargs): - super(BaseItem, self).__init__(*args, **kwargs) @property def item_pos_x(self): @@ -603,9 +602,9 @@ class ItemImage(BaseItem): self.image_path = image_path def draw(self, image, drawer): - paste_image = Image.open(self.image_path) - paste_image = paste_image.resize( - (self.value_width, self.value_height), + source_image = Image.open(os.path.normpath(self.image_path)) + paste_image = source_image.resize( + (self.value_width(), self.value_height()), Image.ANTIALIAS ) image.paste( @@ -614,10 +613,12 @@ class ItemImage(BaseItem): ) def value_width(self): - return self.style.get("width") + return int(self.style["width"]) def value_height(self): - return self.style.get("height") + return int(self.style["height"]) + + class ItemRectangle(BaseItem): obj_type = "rectangle" @@ -684,7 +685,7 @@ class ItemText(BaseItem): font = ImageFont.truetype(font_family, font_size) width = font.getsize(self.value)[0] - return width + return int(width) def value_height(self): font_family = self.style["font-family"] @@ -692,7 +693,7 @@ class ItemText(BaseItem): font = ImageFont.truetype(font_family, font_size) height = font.getsize(self.value)[1] - return height + return int(height) class ItemTable(BaseItem): From 9dd0079b28669454a1ff446c8459d47ae4a2866d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 12:41:08 +0100 Subject: [PATCH 18/99] use font factory for fonts --- pype/scripts/slate/base.py | 44 +++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 7912b93026..1724984fce 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -670,8 +670,12 @@ class ItemText(BaseItem): font_color = self.style["font-color"] font_family = self.style["font-family"] font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) - font = ImageFont.truetype(font_family, font_size) + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) drawer.text( self.value_pos_start, self.value, @@ -682,16 +686,24 @@ class ItemText(BaseItem): def value_width(self): font_family = self.style["font-family"] font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) - font = ImageFont.truetype(font_family, font_size) + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) width = font.getsize(self.value)[0] return int(width) def value_height(self): font_family = self.style["font-family"] font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) - font = ImageFont.truetype(font_family, font_size) + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) height = font.getsize(self.value)[1] return int(height) @@ -828,20 +840,28 @@ class TableField(BaseItem): font_family = self.style["font-family"] font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) - font = ImageFont.truetype(font_family, font_size) - width = font.getsize(self.value)[0] + 1 - return width + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + width = font.getsize_multiline(self.value)[0] + 1 + return int(width) def value_height(self): if not self.value: return 0 font_family = self.style["font-family"] font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) - font = ImageFont.truetype(font_family, font_size) - height = font.getsize(self.value)[1] + 1 - return height + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + height = font.getsize_multiline(self.value)[1] + 1 + return int(height) @property def item_pos_x(self): @@ -917,8 +937,12 @@ class TableField(BaseItem): font_color = self.style["font-color"] font_family = self.style["font-family"] font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) - font = ImageFont.truetype(font_family, font_size) + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) drawer.text( self.value_pos_start, self.value, From 435713a076dd2e4d644a1b071472d6ce976df5eb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 13:50:42 +0100 Subject: [PATCH 19/99] added recalculate method for word wrapping --- pype/scripts/slate/base.py | 102 +++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 1724984fce..c83394ce54 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -834,6 +834,108 @@ class TableField(BaseItem): self.cord_y = cord_y self.value = value + def recalculate_by_width(self, value, max_width): + if not value: + return "" + + word_wrap = self.style.get("word-wrap") + ellide = self.style.get("ellide") + max_lines = self.style.get("max-lines") + + if not ellide and not word_wrap: + # TODO logging + print(( + "Can't draw text because is too long with" + " `word-wrap` and `ellide` turned off" + )) + return "" + + elif ellide and not word_wrap: + max_lines = 1 + + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + + words = [word for word in value.split()] + words_len = len(words) + lines = [] + last_index = 0 + while True: + line = "" + for idx in range(last_index, words_len): + _word = words[idx] + _line = " ".join([line, _word]) + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + break + line = _line + last_index = idx + + if line: + lines.append(line) + + if last_index == words_len - 1: + break + + elif last_index == 0: + if ellide: + line = "" + for idx, char in enumerate(words[idx]): + _line = line + char + self.ellide_text + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + if idx == 0: + line = _line + break + line = _line + + lines.append(line) + # TODO logging + print("Font size is too big.") + break + + output = "" + if not lines: + return output + + if max_lines and len(lines) > max_lines: + lines = [lines[idx] for idx in range(max_lines)] + if not ellide: + return "\n".join(lines) + + last_line = lines[-1] + last_line_width = font.getsize(last_line + self.ellide_text)[0] + if last_line_width <= max_width: + lines[-1] += self.ellide_text + return "\n".join([line for line in lines]) + + last_line_words = last_line.split() + if len(last_line_words) == 1: + if max_lines > 1: + lines[-1] = self.ellide_text + return "\n".join([line for line in lines]) + + _line = "" + for idx, char in enumerate(last_line): + _line = line + char + self.ellide_text + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + if idx == 0: + line = _line + break + line = _line + lines[-1] = line + return "\n".join([line for line in lines]) + + return "\n".join([line for line in lines]) + + def value_width(self): if not self.value: return 0 From 96d22f8a9be81ceb7bcf094b77e7f67bf568a201 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:01:20 +0100 Subject: [PATCH 20/99] added default ellide text --- pype/scripts/slate/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index c83394ce54..b6e4bd2701 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -827,6 +827,7 @@ class TableField(BaseItem): obj_type = "table-item" available_parents = ["table"] + ellide_text = "..." def __init__(self, cord_x, cord_y, value, *args, **kwargs): super(TableField, self).__init__(*args, **kwargs) From 643ad0fcde771b3d86b6077633c43e93e9277bb9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:01:56 +0100 Subject: [PATCH 21/99] added import for font factory --- pype/scripts/slate/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index b6e4bd2701..1cea093929 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -3,6 +3,7 @@ import sys import re import copy import json +import collections from queue import Queue sys.path.append(r"C:\Users\Public\pype_env2\Lib\site-packages") from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageColor From a4f2464583f30a7a9cb9f1ca94e0588c84006e51 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:03:10 +0100 Subject: [PATCH 22/99] replaced cord_x and cord_y with col_idx and row_idx --- pype/scripts/slate/base.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 1cea093929..5c81d11190 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -803,20 +803,20 @@ class ItemTable(BaseItem): height -= 1 return height - def content_pos_info_by_cord(self, cord_x, cord_y): + def content_pos_info_by_cord(self, row_idx, col_idx): row_heights, col_widths = self.size_values - pos_x = self.value_pos_x - pos_y = self.value_pos_y + pos_x = int(self.value_pos_x) + pos_y = int(self.value_pos_y) width = 0 height = 0 for idx, value in enumerate(col_widths): - if cord_y == idx: + if col_idx == idx: width = value break pos_x += value for idx, value in enumerate(row_heights): - if cord_x == idx: + if row_idx == idx: height = value break pos_y += value @@ -830,10 +830,11 @@ class TableField(BaseItem): available_parents = ["table"] ellide_text = "..." - def __init__(self, cord_x, cord_y, value, *args, **kwargs): + def __init__(self, row_idx, col_idx, value, *args, **kwargs): super(TableField, self).__init__(*args, **kwargs) - self.cord_x = cord_x - self.cord_y = cord_y + self.row_idx = row_idx + self.col_idx = col_idx + self.value = value def recalculate_by_width(self, value, max_width): @@ -970,21 +971,21 @@ class TableField(BaseItem): @property def item_pos_x(self): pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) ) return pos_x @property def item_pos_y(self): pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) ) return pos_y @property def value_pos_x(self): pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) ) alignment_hor = self.style["alignment-horizontal"].lower() if alignment_hor in ["center", "centre"]: @@ -1003,7 +1004,7 @@ class TableField(BaseItem): @property def value_pos_y(self): pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) ) alignment_ver = self.style["alignment-vertical"].lower() @@ -1022,12 +1023,12 @@ class TableField(BaseItem): def draw(self, image, drawer): pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.cord_x, self.cord_y) + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) ) pos_start = (pos_x, pos_y) pos_end = (pos_x + width, pos_y + height) bg_color = self.style["bg-color"] - if self.parent.use_alternate_color and (self.cord_x % 2) == 1: + if self.parent.use_alternate_color and (self.row_idx % 2) == 1: bg_color = self.style["bg-alter-color"] if bg_color and bg_color.lower() != "transparent": From 3526c14c44af95c45912a6d0b481259a3811d679 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:03:27 +0100 Subject: [PATCH 23/99] added few changes for width and height --- pype/scripts/slate/base.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 5c81d11190..40e507209f 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -835,6 +835,13 @@ class TableField(BaseItem): self.row_idx = row_idx self.col_idx = col_idx + self.orig_value = value + + max_width = self.style.get("max-width") + max_width = self.style.get("width") or max_width + if max_width: + value = self.recalculate_by_width(value, max_width) + self.value = value def recalculate_by_width(self, value, max_width): @@ -943,6 +950,10 @@ class TableField(BaseItem): if not self.value: return 0 + width = self.style.get("width") + if width: + return int(width) + font_family = self.style["font-family"] font_size = self.style["font-size"] font_bold = self.style.get("font-bold", False) @@ -952,11 +963,21 @@ class TableField(BaseItem): font_family, font_size, font_italic, font_bold ) width = font.getsize_multiline(self.value)[0] + 1 + + min_width = self.style.get("min-height") + if min_width and min_width > width: + width = min_width + return int(width) def value_height(self): if not self.value: return 0 + + height = self.style.get("height") + if height: + return int(height) + font_family = self.style["font-family"] font_size = self.style["font-size"] font_bold = self.style.get("font-bold", False) @@ -966,6 +987,11 @@ class TableField(BaseItem): font_family, font_size, font_italic, font_bold ) height = font.getsize_multiline(self.value)[1] + 1 + + min_height = self.style.get("min-height") + if min_height and min_height > height: + height = min_height + return int(height) @property From 83edea59360fca36240603c9076b529ddb22a3d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:03:50 +0100 Subject: [PATCH 24/99] remove set.json which is deprecated --- pype/scripts/slate/set.json | 59 ------------------------------------- 1 file changed, 59 deletions(-) delete mode 100644 pype/scripts/slate/set.json diff --git a/pype/scripts/slate/set.json b/pype/scripts/slate/set.json deleted file mode 100644 index e8bba8ac41..0000000000 --- a/pype/scripts/slate/set.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "width": 1920, - "height": 1020, - "bg-color": "#000000", - "__bg-color__": "For setting constant color of background. May cause issues if not set when there are not painted spaces.", - "bg-image": null, - "__bg-image__": "May be path to static image???", - "defaults": { - "font-family": "Arial", - "font-size": 26, - "font-color": "#ffffff", - "font-bold": false, - "bg-color": null, - "bg-alter-color": null, - "alignment": "left", - "__alignment[enum]__": ["left", "right", "center"] - }, - "items": [{ - "rel_pos_x": 0.1, - "rel_pos_y": 0.1, - "rel_width": 0.5, - "type": "table", - "col-format": [ - { - "font-size": 12, - "font-color": "#777777", - "alignment": "right" - }, - { - "alignment": "left" - } - ], - "rows": [ - [ - { - "name": "Version", - }, - { - "value": "mei_101_001_0020_slate_NFX_v001", - "font-bold": true - } - ], [ - { - "value": "Date:" - }, - { - "value": "{date}" - } - ] - ] - }, { - "rel_pos_x": 0.1, - "rel_pos_y": 0.1, - "rel_width": 0.5, - "rel_height": 0.5, - "type": "image", - } - ] -} From fc86e3fb19a5e0be7084e5d212aec4a9d8099f53 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 14:05:00 +0100 Subject: [PATCH 25/99] code cleanup --- pype/scripts/slate/base.py | 49 ++++---------------------------------- 1 file changed, 4 insertions(+), 45 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 40e507209f..6376554e2a 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -1169,41 +1169,7 @@ class FontFactory: cls.fonts = available_fonts - -def main_v01(): - main_style = {"bg-color": "#777777", "margin": 0} - text_1_style = {"padding": 0, "bg-color": "#00ff77"} - text_2_style = {"padding": 0, "bg-color": "#ff0066"} - text_3_style = {"padding": 0, "bg-color": "#ff5500"} - image_1_style = {"width": 240, "height": 120, "bg-color": "#7733aa"} - table_1_style = {"padding": 0, "bg-color": "#0077ff"} - - main = MainFrame(1920, 1080, style=main_style) - layer = Layer(parent=main) - main.add_item(layer) - - text_1 = ItemText("Testing message 1", layer, text_1_style) - text_2 = ItemText("Testing 2", layer, text_2_style) - text_3 = ItemText("Testing message 3", layer, text_3_style) - - table_1_items = [["0", "Output text 1", "ha"], ["1", "Output 2"], ["2", "Output text 3"]] - table_1 = ItemTable(table_1_items, True, parent=layer, style=table_1_style) - - image_1_path = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\kitten.jpg" - image_1 = ItemImage(image_1_path, layer, image_1_style) - - layer.add_item(text_1) - layer.add_item(text_2) - layer.add_item(text_3) - - layer.add_item(table_1) - layer.add_item(image_1) - - dst = r"C:\Users\jakub.trllo\Desktop\Tests\files\image\test_output3.png" - main.draw(dst) - - -def main_v02(): +def main(): cur_folder = os.path.dirname(os.path.abspath(__file__)) input_json = os.path.join(cur_folder, "netflix_v01.1.json") with open(input_json) as json_file: @@ -1219,7 +1185,6 @@ def main_v02(): for item in slate_data["items"]: load_queue.put((item, main)) - all_objs = [] while not load_queue.empty(): item_data, parent = load_queue.get() @@ -1260,23 +1225,17 @@ def main_v02(): item_obj = ItemRectangle(**kwargs) if not item_obj: + # TODO logging print( "Slate item not implemented <{}> - skipping".format(item_type) ) continue - all_objs.append(item_obj) - parent.add_item(item_obj) main.draw() - # for item in all_objs: - # print(item.style.get("width"), item.style.get("height")) - # print(item.width, item.height) - # print(item.content_pos_x, item.content_pos_y) - # print(item.value_pos_x, item.value_pos_y) + print("*** Drawing is done") if __name__ == "__main__": - main_v02() - print("*** Drawing is done") + main() From a2470faa64627036d3b8ae9f1d0e2b9b57b958bd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 15:29:36 +0100 Subject: [PATCH 26/99] col row attribute name fix --- pype/scripts/slate/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 6376554e2a..457618327c 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -214,7 +214,7 @@ class BaseObj: col_idx, row_idx = get_indexes_from_regex_match( result, True ) - if self.row_idx == col_idx and self.row_idx == row_idx: + if self.col_idx == col_idx and self.row_idx == row_idx: obj_specific.update(value) output = {} From e9d2bab7910a1cca0553e218fb036356e54fdea7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 16:33:43 +0100 Subject: [PATCH 27/99] fixes for working slate --- pype/scripts/slate/base.py | 14 ++++--- pype/scripts/slate/netflix_v01.1.json | 57 +++++++++++++++++---------- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 457618327c..bd090b2556 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -477,12 +477,12 @@ class Layer(BaseObj): item = _item break - if self.direction != 0: + if self.direction == 1: for id, _item in self.items.items(): if item_id == id: break - pos_x += _item.height() + pos_x += _item.width() if _item.obj_type != "image": pos_x += 1 @@ -497,6 +497,7 @@ class Layer(BaseObj): margin = self.style["margin"] margin_left = self.style.get("margin-left") or margin pos_x += margin_left + return int(pos_x) def child_pos_y(self, item_id): @@ -537,11 +538,11 @@ class Layer(BaseObj): if height > item.height(): continue # times 1 because won't get object pointer but number - height = item.height() - else: height += item.height() + else: + height = item.height() - + # TODO this is not right min_height = self.style.get("min-height") if min_height and min_height > height: return min_height @@ -550,7 +551,7 @@ class Layer(BaseObj): def value_width(self): width = 0 for item in self.items.values(): - if self.direction == 0: + if self.direction == 1: if width > item.width(): continue # times 1 because won't get object pointer but number @@ -1201,6 +1202,7 @@ def main(): kwargs = { "parent": parent, "style": item_style, + "name": item_name, "pos_x": pos_x, "pos_y": pos_y } diff --git a/pype/scripts/slate/netflix_v01.1.json b/pype/scripts/slate/netflix_v01.1.json index a2be6d13e0..91058ddb50 100644 --- a/pype/scripts/slate/netflix_v01.1.json +++ b/pype/scripts/slate/netflix_v01.1.json @@ -15,6 +15,10 @@ "padding": 0, "margin": 0 }, + "layer": { + "padding": 0, + "margin": 0 + }, "rectangle": { "padding": 0, "margin": 0, @@ -37,9 +41,9 @@ "padding-bottom": 10, "alignment-vertical": "top", "alignment-horizontal": "left", - "word-wrap": true, + "word-wrap": false, "ellide": true, - "max-lines": 3 + "max-lines": 1 }, "table-item-col[0]": { "font-size": 20, @@ -60,20 +64,30 @@ "height": 1000 }, "items": [{ - "type": "table", - "values": [ - ["Show:", "First Contact"] - ], + "type": "layer", + "direction": 1, "style": { + "layer": {"padding": 0}, "table-item-col[0]": { - "width": 300 + "width": 200 } - } + }, + "items": [{ + "type": "table", + "values": [ + ["Show:", "First Contact"] + ] + }, { + "type": "table", + "values": [ + ["Submitting For:", "SAMPLE"] + ] + }] }, { "type": "rectangle", "style": { "bg-color": "#d40914", - "width": 1094, + "width": 1108, "height": 5, "fill": true } @@ -87,6 +101,11 @@ ["Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production."] ], "style": { + "table-item-field[1:3]": { + "word-wrap": true, + "ellide": true, + "max-lines": 4 + }, "table-item-col[0]": { "alignment-horizontal": "right", "width": 300 @@ -100,24 +119,20 @@ }, { "type": "layer", "name": "RightSide", - "pos_x": 1174, - "pos_y": 40, - "style": { - "width": 733, - "height": 1000 - }, + "pos_x": 1164, + "pos_y": 26, "items": [{ "type": "image", "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", "style": { - "width": 733, - "height": 414 + "width": 730, + "height": 412 } }, { "type": "image", "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", "style": { - "width": 733, + "width": 730, "height": 55 } }, { @@ -130,10 +145,12 @@ ], "style": { "table-item-col[0]": { - "alignment-horizontal": "left" + "alignment-horizontal": "left", + "width": 200 }, "table-item-col[1]": { - "alignment-horizontal": "right" + "alignment-horizontal": "right", + "width": 530 } } }] From db1e578cd80bdda0f89a11addf64bd748fd55057 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 18:48:38 +0100 Subject: [PATCH 28/99] fixed cases when value of tablefield does not need to ellide or word wrap --- pype/scripts/slate/base.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index bd090b2556..cb72d9af1c 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -853,6 +853,18 @@ class TableField(BaseItem): ellide = self.style.get("ellide") max_lines = self.style.get("max-lines") + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + val_width = font.getsize(value)[0] + if val_width <= max_width: + return value + if not ellide and not word_wrap: # TODO logging print(( From 9cc2c4d5844e4442256bee204286cb475ce5174e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Jan 2020 18:52:43 +0100 Subject: [PATCH 29/99] few padding fixes --- pype/scripts/slate/base.py | 65 ++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index cb72d9af1c..a5404f318d 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -261,7 +261,10 @@ class BaseObj: def value_pos_x(self): pos_x = int(self.content_pos_x) padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + pos_x += padding_left return pos_x @@ -270,7 +273,10 @@ class BaseObj: def value_pos_y(self): pos_y = int(self.content_pos_y) padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding + padding_top = self.style.get("padding-top") + if padding_top is None: + padding_top = padding + pos_y += padding_top return pos_y @@ -314,15 +320,27 @@ class BaseObj: def content_width(self): width = self.value_width() padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding - padding_right = self.style.get("padding-right") or padding + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + + padding_right = self.style.get("padding-right") + if padding_right is None: + padding_right = padding + return width + padding_left + padding_right def content_height(self): height = self.value_height() padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding - padding_bottom = self.style.get("padding-bottom") or padding + padding_top = self.style.get("padding-top") + if padding_top is None: + padding_top = padding + + padding_bottom = self.style.get("padding-bottom") + if padding_bottom is None: + padding_bottom = padding + return height + padding_top + padding_bottom def width(self): @@ -343,30 +361,6 @@ class BaseObj: return height + margin_bottom + margin_top - # @property - # def max_width(self): - # return self.style.get("max-width") or self.width - # - # @property - # def max_height(self): - # return self.style.get("max-height") or self.height - # - # @property - # def max_content_width(self): - # width = self.max_width - # padding = self.style["padding"] - # padding_left = self.style.get("padding-left") or padding - # padding_right = self.style.get("padding-right") or padding - # return (width - (padding_left + padding_right)) - # - # @property - # def max_content_height(self): - # height = self.max_height - # padding = self.style["padding"] - # padding_top = self.style.get("padding-top") or padding - # padding_bottom = self.style.get("padding-bottom") or padding - # return (height - (padding_top + padding_bottom)) - def add_item(self, item): self.items[item.id] = item @@ -846,6 +840,17 @@ class TableField(BaseItem): self.value = value def recalculate_by_width(self, value, max_width): + padding = self.style["padding"] + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + + padding_right = self.style.get("padding-right") + if padding_right is None: + padding_right = padding + + max_width -= (padding_left + padding_right) + if not value: return "" From ac202b9fbd3fbe1eb3cbf179a06655723f6dc0ca Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 14:28:20 +0100 Subject: [PATCH 30/99] implemented placeholder --- pype/scripts/slate/base.py | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index a5404f318d..4fc7cdb4d9 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -646,6 +646,53 @@ class ItemRectangle(BaseItem): return int(self.style["height"]) +class ItemPlaceHolder(BaseItem): + obj_type = "placeholder" + + def __init__(self, image_path, *args, **kwargs): + self.image_path = image_path + super(ItemPlaceHolder, self).__init__(*args, **kwargs) + + def fill_data_format(self): + if re.match(self.fill_data_regex, self.image_path): + self.image_path = self.image_path.format(**self.fill_data) + + def draw(self, image, drawer): + bg_color = self.style["bg-color"] + + kwargs = {} + if bg_color != "tranparent": + kwargs["fill"] = bg_color + + start_pos_x = self.value_pos_x + start_pos_y = self.value_pos_y + end_pos_x = start_pos_x + self.value_width() + end_pos_y = start_pos_y + self.value_height() + + drawer.rectangle( + ( + (start_pos_x, start_pos_y), + (end_pos_x, end_pos_y) + ), + **kwargs + ) + + def value_width(self): + return int(self.style["width"]) + + def value_height(self): + return int(self.style["height"]) + + def collect_data(self): + return { + "pos_x": self.value_pos_x, + "pos_y": self.value_pos_y, + "width": self.value_width(), + "height": self.value_height(), + "path": self.image_path + } + + class ItemText(BaseItem): obj_type = "text" From 4a8ab6fac8dbe9137304d1be036f8e01ebf32846 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 14:29:19 +0100 Subject: [PATCH 31/99] added collect_data and fill_data abilities --- pype/scripts/slate/base.py | 382 ++++++++++++------ .../{netflix_v01.1.json => netflix_v01.json} | 87 +++- pype/scripts/slate/netflix_v02.json | 213 ++++++++++ pype/scripts/slate/netflix_v03.json | 213 ++++++++++ 4 files changed, 758 insertions(+), 137 deletions(-) rename pype/scripts/slate/{netflix_v01.1.json => netflix_v01.json} (65%) create mode 100644 pype/scripts/slate/netflix_v02.json create mode 100644 pype/scripts/slate/netflix_v03.json diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py index 4fc7cdb4d9..1f90f92992 100644 --- a/pype/scripts/slate/base.py +++ b/pype/scripts/slate/base.py @@ -25,6 +25,7 @@ class BaseObj: "margin-top", "margin-bottom", "width", "height", "fill" ] + fill_data_regex = r"{[^}]+}" def __init__(self, parent, style={}, name=None, pos_x=None, pos_y=None): if not self.obj_type: @@ -56,6 +57,16 @@ class BaseObj: self._pos_x = pos_x or 0 self._pos_y = pos_y or 0 + if parent: + parent.add_item(self) + + def fill_data_format(self): + return + + @property + def fill_data(self): + return self.parent.fill_data + @property def main_style(self): default_style_v1 = { @@ -66,40 +77,43 @@ class BaseObj: "font-bold": False, "font-italic": False, "bg-color": "#0077ff", - "bg-alter-color": "#0055dd", - "alignment-horizontal": "center", - "alignment-vertical": "bottom", - "padding": 0, - "margin": 0, - }, - "main_frame": { - "padding": 0, - "margin": 0 + "alignment-horizontal": "left", + "alignment-vertical": "top" }, "layer": { "padding": 0, "margin": 0 }, - "image": { + "rectangle": { "padding": 0, - "margin": 0 + "margin": 0, + "bg-color": "#E9324B", + "fill": true }, - "text": { + "main_frame": { "padding": 0, - "margin": 0 + "margin": 0, + "bg-color": "#252525" }, "table": { "padding": 0, - "margin": 0 + "margin": 0, + "bg-color": "transparent" }, "table-item": { - "alignment-horizontal": "right", - "padding": 0, - "margin": 0 - }, - "__not_implemented__": { - "table-item-col-0": {}, - "#MyName": {} + "padding": 5, + "padding-bottom": 10, + "margin": 0, + "bg-color": "#212121", + "bg-alter-color": "#272727", + "font-color": "#dcdcdc", + "font-bold": false, + "font-italic": false, + "alignment-horizontal": "left", + "alignment-vertical": "top", + "word-wrap": false, + "ellide": true, + "max-lines": 1 } } return default_style_v1 @@ -118,6 +132,31 @@ class BaseObj: ) ) + def collect_data(self): + return None + + def find_item(self, obj_type=None, name=None): + obj_type_fits = False + name_fits = False + if obj_type is None or self.obj_type == obj_type: + obj_type_fits = True + + if name is None or self.name != name: + name_fits = True + + output = [] + if obj_type_fits and name_fits: + output.append(self) + + if not self.items: + return output + + for item in self.items.values(): + output.extend( + item.find_item(obj_type=obj_type, name=name) + ) + return output + @property def full_style(self): if self.parent is not None: @@ -151,10 +190,9 @@ class BaseObj: if self.name: name = str(self.name) if not name.startswith("#"): - name += "#" + name = "#" + name name_specific = style.get(name) or {} - if obj_type == "table-item": col_regex = r"table-item-col\[([\d\-, ]+)*\]" row_regex = r"table-item-row\[([\d\-, ]+)*\]" @@ -363,6 +401,8 @@ class BaseObj: def add_item(self, item): self.items[item.id] = item + item.fill_data_format() + def reset(self): for item in self.items.values(): @@ -374,12 +414,24 @@ class MainFrame(BaseObj): obj_type = "main_frame" available_parents = [None] - def __init__(self, width, height, destination_path=None, *args, **kwargs): + def __init__( + self, width, height, destination_path, fill_data={}, *args, **kwargs + ): kwargs["parent"] = None super(MainFrame, self).__init__(*args, **kwargs) self._width = width self._height = height self.dst_path = destination_path + self._fill_data = fill_data + self.fill_data_format() + + def fill_data_format(self): + if re.match(self.fill_data_regex, self.dst_path): + self.dst_path = self.dst_path.format(**self.fill_data) + + @property + def fill_data(self): + return self._fill_data def value_width(self): width = 0 @@ -400,16 +452,7 @@ class MainFrame(BaseObj): return self._height def draw(self, path=None): - if not path: - path = self.dst_path - - if not path: - raise TypeError(( - "draw() missing 1 required positional argument: 'path'" - " if 'destination_path is not specified'" - )) - - dir_path = os.path.dirname(path) + dir_path = os.path.dirname(self.dst_path) if not os.path.exists(dir_path): os.makedirs(dir_path) @@ -419,9 +462,24 @@ class MainFrame(BaseObj): for item in self.items.values(): item.draw(image, drawer) - image.save(path) + image.save(self.dst_path) self.reset() + def collect_data(self): + output = {} + output["width"] = self.width() + output["height"] = self.height() + output["slate_path"] = self.dst_path + + placeholders = self.find_item(obj_type="placeholder") + placeholders_data = [] + for placeholder in placeholders: + placeholders_data.append(placeholder.collect_data()) + + output["placeholders"] = placeholders_data + + return output + class Layer(BaseObj): obj_type = "layer" @@ -450,6 +508,7 @@ class Layer(BaseObj): pos_y = self._pos_y else: pos_y = self.parent.value_pos_y + return int(pos_y) @property @@ -477,7 +536,7 @@ class Layer(BaseObj): break pos_x += _item.width() - if _item.obj_type != "image": + if _item.obj_type not in ["image", "placeholder"]: pos_x += 1 else: @@ -509,7 +568,7 @@ class Layer(BaseObj): if item_id == id: break pos_y += item.height() - if item.obj_type != "image": + if item.obj_type not in ["image", "placeholder"]: pos_y += 1 else: @@ -519,22 +578,18 @@ class Layer(BaseObj): elif alignment_ver == "bottom": pos_y += self.content_height() - item.content_height() - else: - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - pos_y += margin_top return int(pos_y) def value_height(self): height = 0 for item in self.items.values(): - if self.direction == 0: + if self.direction == 1: if height > item.height(): continue # times 1 because won't get object pointer but number - height += item.height() - else: height = item.height() + else: + height += item.height() # TODO this is not right min_height = self.style.get("min-height") @@ -545,12 +600,14 @@ class Layer(BaseObj): def value_width(self): width = 0 for item in self.items.values(): - if self.direction == 1: + if self.direction == 0: if width > item.width(): continue # times 1 because won't get object pointer but number width = item.width() else: + if self.name == "LeftSide": + print(item, item.width()) width += item.width() min_width = self.style.get("min-width") @@ -566,7 +623,6 @@ class Layer(BaseObj): class BaseItem(BaseObj): available_parents = ["main_frame", "layer"] - @property def item_pos_x(self): if self.parent.obj_type == "main_frame": @@ -597,6 +653,10 @@ class ItemImage(BaseItem): super(ItemImage, self).__init__(*args, **kwargs) self.image_path = image_path + def fill_data_format(self): + if re.match(self.fill_data_regex, self.image_path): + self.image_path = self.image_path.format(**self.fill_data) + def draw(self, image, drawer): source_image = Image.open(os.path.normpath(self.image_path)) paste_image = source_image.resize( @@ -756,15 +816,25 @@ class ItemTable(BaseItem): obj_type = "table" def __init__(self, values, use_alternate_color=False, *args, **kwargs): + + self.values_by_cords = None + self.prepare_values(values) + super(ItemTable, self).__init__(*args, **kwargs) self.size_values = None - self.values_by_cords = None - - self.prepare_values(values) self.calculate_sizes() self.use_alternate_color = use_alternate_color + def add_item(self, item): + if item.obj_type == "table-item": + return + super(ItemTable, self).add_item(item) + + def fill_data_format(self): + for item in self.values: + item.fill_data_format() + def prepare_values(self, _values): values = [] values_by_cords = [] @@ -876,14 +946,6 @@ class TableField(BaseItem): super(TableField, self).__init__(*args, **kwargs) self.row_idx = row_idx self.col_idx = col_idx - - self.orig_value = value - - max_width = self.style.get("max-width") - max_width = self.style.get("width") or max_width - if max_width: - value = self.recalculate_by_width(value, max_width) - self.value = value def recalculate_by_width(self, value, max_width): @@ -928,24 +990,23 @@ class TableField(BaseItem): elif ellide and not word_wrap: max_lines = 1 - font_family = self.style["font-family"] - font_size = self.style["font-size"] - font_bold = self.style.get("font-bold", False) - font_italic = self.style.get("font-italic", False) - - font = FontFactory.get_font( - font_family, font_size, font_italic, font_bold - ) - words = [word for word in value.split()] words_len = len(words) lines = [] - last_index = 0 + last_index = None while True: + start_index = 0 + if last_index is not None: + start_index = int(last_index) + 1 + line = "" - for idx in range(last_index, words_len): + for idx in range(start_index, words_len): _word = words[idx] - _line = " ".join([line, _word]) + connector = " " + if line == "": + connector = "" + + _line = connector.join([line, _word]) _line_width = font.getsize(_line)[0] if _line_width > max_width: break @@ -958,7 +1019,7 @@ class TableField(BaseItem): if last_index == words_len - 1: break - elif last_index == 0: + elif last_index is None: if ellide: line = "" for idx, char in enumerate(words[idx]): @@ -968,7 +1029,7 @@ class TableField(BaseItem): if idx == 0: line = _line break - line = _line + line = line + char lines.append(line) # TODO logging @@ -979,46 +1040,103 @@ class TableField(BaseItem): if not lines: return output - if max_lines and len(lines) > max_lines: - lines = [lines[idx] for idx in range(max_lines)] - if not ellide: - return "\n".join(lines) + over_max_lines = (max_lines and len(lines) > max_lines) + if not over_max_lines: + return "\n".join([line for line in lines]) - last_line = lines[-1] - last_line_width = font.getsize(last_line + self.ellide_text)[0] - if last_line_width <= max_width: - lines[-1] += self.ellide_text + lines = [lines[idx] for idx in range(max_lines)] + if not ellide: + return "\n".join(lines) + + last_line = lines[-1] + last_line_width = font.getsize(last_line + self.ellide_text)[0] + if last_line_width <= max_width: + lines[-1] += self.ellide_text + return "\n".join([line for line in lines]) + + last_line_words = last_line.split() + if len(last_line_words) == 1: + if max_lines > 1: + # TODO try previous line? + lines[-1] = self.ellide_text return "\n".join([line for line in lines]) - last_line_words = last_line.split() - if len(last_line_words) == 1: - if max_lines > 1: - lines[-1] = self.ellide_text - return "\n".join([line for line in lines]) + line = "" + for idx, word in enumerate(last_line_words): + _line = line + word + self.ellide_text + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + if idx == 0: + line = _line + break + line = _line + lines[-1] = line - _line = "" - for idx, char in enumerate(last_line): - _line = line + char + self.ellide_text - _line_width = font.getsize(_line)[0] - if _line_width > max_width: - if idx == 0: - line = _line - break - line = _line - lines[-1] = line - return "\n".join([line for line in lines]) + return "\n".join([line for line in lines]) + + line = "" + for idx, _word in enumerate(last_line_words): + connector = " " + if line == "": + connector = "" + + _line = connector.join([line, _word + self.ellide_text]) + _line_width = font.getsize(_line)[0] + + if _line_width <= max_width: + line = connector.join([line, _word]) + continue + + if idx != 0: + line += self.ellide_text + break + + if max_lines != 1: + # TODO try previous line? + line = self.ellide_text + break + + for idx, char in enumerate(_word): + _line = line + char + self.ellide_text + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + if idx == 0: + line = _line + break + line = line + char + break + + lines[-1] = line return "\n".join([line for line in lines]) + def fill_data_format(self): + value = self.value + if re.match(self.fill_data_regex, value): + value = value.format(**self.fill_data) + + self.orig_value = value + + max_width = self.style.get("max-width") + max_width = self.style.get("width") or max_width + if max_width: + value = self.recalculate_by_width(value, max_width) + + self.value = value + + def content_width(self): + width = self.style.get("width") + if width: + return int(width) + return super(TableField, self).content_width() + + def content_height(self): + return super(TableField, self).content_height() def value_width(self): if not self.value: return 0 - width = self.style.get("width") - if width: - return int(width) - font_family = self.style["font-family"] font_size = self.style["font-size"] font_bold = self.style.get("font-bold", False) @@ -1087,7 +1205,10 @@ class TableField(BaseItem): else: padding = self.style["padding"] - padding_left = self.style.get("padding-left") or padding + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + pos_x += padding_left return int(pos_x) @@ -1107,7 +1228,10 @@ class TableField(BaseItem): else: padding = self.style["padding"] - padding_top = self.style.get("padding-top") or padding + padding_top = self.style.get("padding-top") + if padding_top is None: + padding_top = padding + pos_y += padding_top return int(pos_y) @@ -1139,11 +1263,17 @@ class TableField(BaseItem): font = FontFactory.get_font( font_family, font_size, font_italic, font_bold ) - drawer.text( + + alignment_hor = self.style["alignment-horizontal"].lower() + if alignment_hor == "centre": + alignment_hor = "center" + + drawer.multiline_text( self.value_pos_start, self.value, font=font, - fill=font_color + fill=font_color, + align=alignment_hor ) @@ -1236,15 +1366,36 @@ class FontFactory: def main(): cur_folder = os.path.dirname(os.path.abspath(__file__)) - input_json = os.path.join(cur_folder, "netflix_v01.1.json") + # input_json = os.path.join(cur_folder, "netflix_v01.json") + # input_json = os.path.join(cur_folder, "netflix_v02.json") + input_json = os.path.join(cur_folder, "netflix_v03.json") with open(input_json) as json_file: slate_data = json.load(json_file) + fill_data = { + "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v03.png", + "project": { + "name": "Project name" + }, + "intent": "WIP", + "version_name": "mei_101_001_0020_slate_NFX_v001", + "date": "2019-08-09", + "shot_type": "2d comp", + "submission_note": "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production.", + "thumbnail_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", + "color_bar_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", + "vendor": "DAZZLE", + "shot_name": "SLATE_SIMPLE", + "frame_start": 1001, + "frame_end": 1004, + "duration": 3 + } width = slate_data["width"] height = slate_data["height"] + dst_path = slate_data["destination_path"] style = slate_data.get("style") or {} - dst_path = slate_data.get("destination_path") - main = MainFrame(width, height, destination_path=dst_path, style=style) + + main = MainFrame(width, height, dst_path, fill_data, style=style) load_queue = Queue() for item in slate_data["items"]: @@ -1271,7 +1422,6 @@ def main(): "pos_y": pos_y } - item_obj = None if item_type == "layer": direction = item_data.get("direction", 0) item_obj = Layer(direction, **kwargs) @@ -1281,26 +1431,28 @@ def main(): elif item_type == "table": use_alternate_color = item_data.get("use_alternate_color", False) values = item_data.get("values") or [] - item_obj = ItemTable(values, use_alternate_color, **kwargs) + ItemTable(values, use_alternate_color, **kwargs) elif item_type == "image": path = item_data["path"] - item_obj = ItemImage(path, **kwargs) + ItemImage(path, **kwargs) elif item_type == "rectangle": - item_obj = ItemRectangle(**kwargs) + ItemRectangle(**kwargs) - if not item_obj: + elif item_type == "placeholder": + path = item_data["path"] + ItemPlaceHolder(path, **kwargs) + + else: # TODO logging print( "Slate item not implemented <{}> - skipping".format(item_type) ) - continue - - parent.add_item(item_obj) main.draw() - print("*** Drawing is done") + print(main.collect_data()) + print("*** Finished") if __name__ == "__main__": diff --git a/pype/scripts/slate/netflix_v01.1.json b/pype/scripts/slate/netflix_v01.json similarity index 65% rename from pype/scripts/slate/netflix_v01.1.json rename to pype/scripts/slate/netflix_v01.json index 91058ddb50..9a5974839d 100644 --- a/pype/scripts/slate/netflix_v01.1.json +++ b/pype/scripts/slate/netflix_v01.json @@ -1,7 +1,7 @@ { "width": 1920, "height": 1080, - "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v001.png", + "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v01.png", "style": { "*": { "font-family": "arial", @@ -32,33 +32,40 @@ "bg-color": "transparent" }, "table-item": { - "bg-color": "#272727", - "bg-alter-color": "#212121", - "font-color": "#ffffff", - "font-bold": true, + "bg-color": "#212121", + "bg-alter-color": "#272727", + "font-color": "#dcdcdc", + "font-bold": false, "font-italic": false, "padding": 5, "padding-bottom": 10, - "alignment-vertical": "top", "alignment-horizontal": "left", + "alignment-vertical": "top", "word-wrap": false, "ellide": true, "max-lines": 1 }, "table-item-col[0]": { "font-size": 20, - "font-color": "#898989" + "font-color": "#898989", + "font-bold": true, + "ellide": false, + "word-wrap": true, + "max-lines": null }, "table-item-col[1]": { - "font-size": 30, - "padding-left": 20 + "font-size": 40, + "padding-left": 10 + }, + "#colorbar": { + "bg-color": "#9932CC" } }, "items": [{ "type": "layer", "name": "LeftSide", - "pos_x": 40, - "pos_y": 40, + "pos_x": 27, + "pos_y": 27, "style": { "width": 1094, "height": 1000 @@ -67,26 +74,52 @@ "type": "layer", "direction": 1, "style": { - "layer": {"padding": 0}, + "table-item": { + "bg-color": "transparent" + }, "table-item-col[0]": { - "width": 200 + "font-size": 20, + "font-color": "#898989", + "alignment-horizontal": "right" + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "font-bold": true, + "font-size": 40 } }, "items": [{ "type": "table", "values": [ ["Show:", "First Contact"] - ] + ], + "style": { + "table-item-field[0:0]": { + "width": 150 + }, + "table-item-field[1:0]": { + "width": 580 + } + } }, { "type": "table", "values": [ ["Submitting For:", "SAMPLE"] - ] + ], + "style": { + "table-item-field[0:0]": { + "width": 160 + }, + "table-item-field[1:0]": { + "width": 218, + "alignment-horizontal": "right" + } + } }] }, { "type": "rectangle", "style": { - "bg-color": "#d40914", + "bg-color": "#bc1015", "width": 1108, "height": 5, "fill": true @@ -101,18 +134,24 @@ ["Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production."] ], "style": { + "table-item": { + "padding-bottom": 20 + }, + "table-item-field[1:0]": { + "font-bold": true + }, "table-item-field[1:3]": { "word-wrap": true, "ellide": true, - "max-lines": 4 + "max-lines": 2 }, "table-item-col[0]": { "alignment-horizontal": "right", - "width": 300 + "width": 150 }, "table-item-col[1]": { "alignment-horizontal": "left", - "width": 786 + "width": 958 } } }] @@ -122,15 +161,18 @@ "pos_x": 1164, "pos_y": 26, "items": [{ - "type": "image", + "type": "placeholder", + "name": "thumbnail", "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", "style": { "width": 730, "height": 412 } }, { - "type": "image", + "type": "placeholder", + "name": "colorbar", "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", + "return_data": true, "style": { "width": 730, "height": 55 @@ -150,7 +192,8 @@ }, "table-item-col[1]": { "alignment-horizontal": "right", - "width": 530 + "width": 530, + "font-size": 30 } } }] diff --git a/pype/scripts/slate/netflix_v02.json b/pype/scripts/slate/netflix_v02.json new file mode 100644 index 0000000000..f373ed8134 --- /dev/null +++ b/pype/scripts/slate/netflix_v02.json @@ -0,0 +1,213 @@ +{ + "width": 1920, + "height": 1080, + "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v02.png", + "style": { + "*": { + "font-family": "arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": false, + "font-italic": false, + "bg-color": "#0077ff", + "alignment-horizontal": "left", + "alignment-vertical": "top" + }, + "layer": { + "padding": 0, + "margin": 0 + }, + "rectangle": { + "padding": 0, + "margin": 0, + "bg-color": "#E9324B", + "fill": true + }, + "main_frame": { + "padding": 0, + "margin": 0, + "bg-color": "#252525" + }, + "table": { + "padding": 0, + "margin": 0, + "bg-color": "transparent" + }, + "table-item": { + "padding": 5, + "padding-bottom": 10, + "margin": 0, + "bg-color": "#212121", + "bg-alter-color": "#272727", + "font-color": "#dcdcdc", + "font-bold": false, + "font-italic": false, + "alignment-horizontal": "left", + "alignment-vertical": "top", + "word-wrap": false, + "ellide": true, + "max-lines": 1 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "font-bold": true, + "ellide": false, + "word-wrap": true, + "max-lines": null + }, + "table-item-col[1]": { + "font-size": 40, + "padding-left": 10 + }, + "#colorbar": { + "bg-color": "#9932CC" + } + }, + "items": [{ + "type": "layer", + "direction": 1, + "name": "MainLayer", + "style": { + "#MainLayer": { + "width": 1094, + "height": 1000, + "margin": 25, + "padding": 0 + }, + "#LeftSide": { + "margin-right": 25 + } + }, + "items": [{ + "type": "layer", + "name": "LeftSide", + "items": [{ + "type": "layer", + "direction": 1, + "style": { + "table-item": { + "bg-color": "transparent", + "padding-bottom": 20 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "alignment-horizontal": "right" + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "font-bold": true, + "font-size": 40 + } + }, + "items": [{ + "type": "table", + "values": [ + ["Show:", "First Contact"] + ], + "style": { + "table-item-field[0:0]": { + "width": 150 + }, + "table-item-field[1:0]": { + "width": 580 + } + } + }, { + "type": "table", + "values": [ + ["Submitting For:", "SAMPLE"] + ], + "style": { + "table-item-field[0:0]": { + "width": 160 + }, + "table-item-field[1:0]": { + "width": 218, + "alignment-horizontal": "right" + } + } + }] + }, { + "type": "rectangle", + "style": { + "bg-color": "#bc1015", + "width": 1108, + "height": 5, + "fill": true + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Version name:", "mei_101_001_0020_slate_NFX_v001"], + ["Date:", "2019-08-09"], + ["Shot Types:", "2d comp"], + ["Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production."] + ], + "style": { + "table-item": { + "padding-bottom": 20 + }, + "table-item-field[1:0]": { + "font-bold": true + }, + "table-item-field[1:3]": { + "word-wrap": true, + "ellide": true, + "max-lines": 2 + }, + "table-item-col[0]": { + "alignment-horizontal": "right", + "width": 150 + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "width": 958 + } + } + }] + }, { + "type": "layer", + "name": "RightSide", + "items": [{ + "type": "placeholder", + "name": "thumbnail", + "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", + "style": { + "width": 730, + "height": 412 + } + }, { + "type": "placeholder", + "name": "colorbar", + "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", + "return_data": true, + "style": { + "width": 730, + "height": 55 + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Vendor:", "DAZZLE"], + ["Shot Name:", "SLATE_SIMPLE"], + ["Frames:", "0 - 1 (2)"] + ], + "style": { + "table-item-col[0]": { + "alignment-horizontal": "left", + "width": 200 + }, + "table-item-col[1]": { + "alignment-horizontal": "right", + "width": 530, + "font-size": 30 + } + } + }] + }] + }] +} diff --git a/pype/scripts/slate/netflix_v03.json b/pype/scripts/slate/netflix_v03.json new file mode 100644 index 0000000000..975cb60133 --- /dev/null +++ b/pype/scripts/slate/netflix_v03.json @@ -0,0 +1,213 @@ +{ + "width": 1920, + "height": 1080, + "destination_path": "{destination_path}", + "style": { + "*": { + "font-family": "arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": false, + "font-italic": false, + "bg-color": "#0077ff", + "alignment-horizontal": "left", + "alignment-vertical": "top" + }, + "layer": { + "padding": 0, + "margin": 0 + }, + "rectangle": { + "padding": 0, + "margin": 0, + "bg-color": "#E9324B", + "fill": true + }, + "main_frame": { + "padding": 0, + "margin": 0, + "bg-color": "#252525" + }, + "table": { + "padding": 0, + "margin": 0, + "bg-color": "transparent" + }, + "table-item": { + "padding": 5, + "padding-bottom": 10, + "margin": 0, + "bg-color": "#212121", + "bg-alter-color": "#272727", + "font-color": "#dcdcdc", + "font-bold": false, + "font-italic": false, + "alignment-horizontal": "left", + "alignment-vertical": "top", + "word-wrap": false, + "ellide": true, + "max-lines": 1 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "font-bold": true, + "ellide": false, + "word-wrap": true, + "max-lines": null + }, + "table-item-col[1]": { + "font-size": 40, + "padding-left": 10 + }, + "#colorbar": { + "bg-color": "#9932CC" + } + }, + "items": [{ + "type": "layer", + "direction": 1, + "name": "MainLayer", + "style": { + "#MainLayer": { + "width": 1094, + "height": 1000, + "margin": 25, + "padding": 0 + }, + "#LeftSide": { + "margin-right": 25 + } + }, + "items": [{ + "type": "layer", + "name": "LeftSide", + "items": [{ + "type": "layer", + "direction": 1, + "style": { + "table-item": { + "bg-color": "transparent", + "padding-bottom": 20 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "alignment-horizontal": "right" + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "font-bold": true, + "font-size": 40 + } + }, + "items": [{ + "type": "table", + "values": [ + ["Show:", "{project[name]}"] + ], + "style": { + "table-item-field[0:0]": { + "width": 150 + }, + "table-item-field[1:0]": { + "width": 580 + } + } + }, { + "type": "table", + "values": [ + ["Submitting For:", "{intent}"] + ], + "style": { + "table-item-field[0:0]": { + "width": 160 + }, + "table-item-field[1:0]": { + "width": 218, + "alignment-horizontal": "right" + } + } + }] + }, { + "type": "rectangle", + "style": { + "bg-color": "#bc1015", + "width": 1108, + "height": 5, + "fill": true + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Version name:", "{version_name}"], + ["Date:", "{date}"], + ["Shot Types:", "{shot_type}"], + ["Submission Note:", "{submission_note}"] + ], + "style": { + "table-item": { + "padding-bottom": 20 + }, + "table-item-field[1:0]": { + "font-bold": true + }, + "table-item-field[1:3]": { + "word-wrap": true, + "ellide": true, + "max-lines": 4 + }, + "table-item-col[0]": { + "alignment-horizontal": "right", + "width": 150 + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "width": 958 + } + } + }] + }, { + "type": "layer", + "name": "RightSide", + "items": [{ + "type": "placeholder", + "name": "thumbnail", + "path": "{thumbnail_path}", + "style": { + "width": 730, + "height": 412 + } + }, { + "type": "placeholder", + "name": "colorbar", + "path": "{color_bar_path}", + "return_data": true, + "style": { + "width": 730, + "height": 55 + } + }, { + "type": "table", + "use_alternate_color": true, + "values": [ + ["Vendor:", "{vendor}"], + ["Shot Name:", "{shot_name}"], + ["Frames:", "{frame_start} - {frame_end} ({duration})"] + ], + "style": { + "table-item-col[0]": { + "alignment-horizontal": "left", + "width": 200 + }, + "table-item-col[1]": { + "alignment-horizontal": "right", + "width": 530, + "font-size": 30 + } + } + }] + }] + }] +} From 6ef5853b65cb86cbdd94ae8d713ddb1fac753bdf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 16:33:15 +0100 Subject: [PATCH 32/99] reorganized slates a little bit --- pype/scripts/slate/base.py | 1459 ----------------- pype/scripts/slate/default_style.json | 44 - pype/scripts/slate/lib.py | 114 ++ pype/scripts/slate/slate.py | 355 ---- pype/scripts/slate/slate_base/__init__.py | 0 pype/scripts/slate/slate_base/base.py | 373 +++++ .../slate/slate_base/default_style.json | 58 + pype/scripts/slate/slate_base/font_factory.py | 93 ++ pype/scripts/slate/slate_base/items.py | 666 ++++++++ pype/scripts/slate/slate_base/layer.py | 139 ++ pype/scripts/slate/slate_base/main_frame.py | 77 + pype/scripts/slate/style_schema.json | 44 - 12 files changed, 1520 insertions(+), 1902 deletions(-) delete mode 100644 pype/scripts/slate/base.py delete mode 100644 pype/scripts/slate/default_style.json create mode 100644 pype/scripts/slate/lib.py delete mode 100644 pype/scripts/slate/slate.py create mode 100644 pype/scripts/slate/slate_base/__init__.py create mode 100644 pype/scripts/slate/slate_base/base.py create mode 100644 pype/scripts/slate/slate_base/default_style.json create mode 100644 pype/scripts/slate/slate_base/font_factory.py create mode 100644 pype/scripts/slate/slate_base/items.py create mode 100644 pype/scripts/slate/slate_base/layer.py create mode 100644 pype/scripts/slate/slate_base/main_frame.py delete mode 100644 pype/scripts/slate/style_schema.json diff --git a/pype/scripts/slate/base.py b/pype/scripts/slate/base.py deleted file mode 100644 index 1f90f92992..0000000000 --- a/pype/scripts/slate/base.py +++ /dev/null @@ -1,1459 +0,0 @@ -import os -import sys -import re -import copy -import json -import collections -from queue import Queue -sys.path.append(r"C:\Users\Public\pype_env2\Lib\site-packages") -from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageColor -from uuid import uuid4 - - -class BaseObj: - """Base Object for slates.""" - - obj_type = None - available_parents = [] - all_style_keys = [ - "font-family", "font-size", "font-color", "font-bold", "font-italic", - "bg-color", "bg-alter-color", - "alignment-horizontal", "alignment-vertical", - "padding", "padding-left", "padding-right", - "padding-top", "padding-bottom", - "margin", "margin-left", "margin-right", - "margin-top", "margin-bottom", "width", "height", - "fill" - ] - fill_data_regex = r"{[^}]+}" - - def __init__(self, parent, style={}, name=None, pos_x=None, pos_y=None): - if not self.obj_type: - raise NotImplementedError( - "Class don't have set object type <{}>".format( - self.__class__.__name__ - ) - ) - - parent_obj_type = None - if parent: - parent_obj_type = parent.obj_type - - if parent_obj_type not in self.available_parents: - expected_parents = ", ".join(self.available_parents) - raise Exception(( - "Invalid parent <{}> for <{}>. Expected <{}>" - ).format( - parent.__class__.__name__, self.obj_type, expected_parents - )) - - self.parent = parent - self._style = style - - self.id = uuid4() - self.name = name - self.items = {} - - self._pos_x = pos_x or 0 - self._pos_y = pos_y or 0 - - if parent: - parent.add_item(self) - - def fill_data_format(self): - return - - @property - def fill_data(self): - return self.parent.fill_data - - @property - def main_style(self): - default_style_v1 = { - "*": { - "font-family": "arial", - "font-size": 26, - "font-color": "#ffffff", - "font-bold": False, - "font-italic": False, - "bg-color": "#0077ff", - "alignment-horizontal": "left", - "alignment-vertical": "top" - }, - "layer": { - "padding": 0, - "margin": 0 - }, - "rectangle": { - "padding": 0, - "margin": 0, - "bg-color": "#E9324B", - "fill": true - }, - "main_frame": { - "padding": 0, - "margin": 0, - "bg-color": "#252525" - }, - "table": { - "padding": 0, - "margin": 0, - "bg-color": "transparent" - }, - "table-item": { - "padding": 5, - "padding-bottom": 10, - "margin": 0, - "bg-color": "#212121", - "bg-alter-color": "#272727", - "font-color": "#dcdcdc", - "font-bold": false, - "font-italic": false, - "alignment-horizontal": "left", - "alignment-vertical": "top", - "word-wrap": false, - "ellide": true, - "max-lines": 1 - } - } - return default_style_v1 - - def height(self): - raise NotImplementedError( - "Attribute `height` is not implemented for <{}>".format( - self.__clas__.__name__ - ) - ) - - def width(self): - raise NotImplementedError( - "Attribute `width` is not implemented for <{}>".format( - self.__clas__.__name__ - ) - ) - - def collect_data(self): - return None - - def find_item(self, obj_type=None, name=None): - obj_type_fits = False - name_fits = False - if obj_type is None or self.obj_type == obj_type: - obj_type_fits = True - - if name is None or self.name != name: - name_fits = True - - output = [] - if obj_type_fits and name_fits: - output.append(self) - - if not self.items: - return output - - for item in self.items.values(): - output.extend( - item.find_item(obj_type=obj_type, name=name) - ) - return output - - @property - def full_style(self): - if self.parent is not None: - style = dict(val for val in self.parent.full_style.items()) - else: - style = self.main_style - - for key, value in self._style.items(): - if key in self.all_style_keys: - # TODO which variant is right? - style[self.obj_type][key] = value - # style["*"][key] = value - else: - if key not in style: - style[key] = {} - - if isinstance(style[key], dict): - style[key].update(value) - else: - style[key] = value - - return style - - def get_style_for_obj_type(self, obj_type, style=None): - if not style: - style = copy.deepcopy(self.full_style) - - base = style.get("*") or {} - obj_specific = style.get(obj_type) or {} - name_specific = {} - if self.name: - name = str(self.name) - if not name.startswith("#"): - name = "#" + name - name_specific = style.get(name) or {} - - if obj_type == "table-item": - col_regex = r"table-item-col\[([\d\-, ]+)*\]" - row_regex = r"table-item-row\[([\d\-, ]+)*\]" - field_regex = ( - r"table-item-field\[(([ ]+)?\d+([ ]+)?:([ ]+)?\d+([ ]+)?)*\]" - ) - # STRICT field regex (not allowed spaces) - # fild_regex = r"table-item-field\[(\d+:\d+)*\]" - - def get_indexes_from_regex_match(result, field=False): - group = result.group(1) - indexes = [] - if field: - return [ - int(part.strip()) for part in group.strip().split(":") - ] - - parts = group.strip().split(",") - for part in parts: - part = part.strip() - if "-" not in part: - indexes.append(int(part)) - continue - - sub_parts = [ - int(sub.strip()) for sub in part.split("-") - ] - if len(sub_parts) != 2: - # TODO logging - print("invalid range '{}'".format(part)) - continue - - for idx in range(sub_parts[0], sub_parts[1]+1): - indexes.append(idx) - return indexes - - for key, value in style.items(): - if not key.startswith(obj_type): - continue - - result = re.search(col_regex, key) - if result: - indexes = get_indexes_from_regex_match(result) - if self.col_idx in indexes: - obj_specific.update(value) - continue - - result = re.search(row_regex, key) - if result: - indexes = get_indexes_from_regex_match(result) - if self.row_idx in indexes: - obj_specific.update(value) - continue - - result = re.search(field_regex, key) - if result: - col_idx, row_idx = get_indexes_from_regex_match( - result, True - ) - if self.col_idx == col_idx and self.row_idx == row_idx: - obj_specific.update(value) - - output = {} - output.update(base) - output.update(obj_specific) - output.update(name_specific) - - return output - - @property - def style(self): - return self.get_style_for_obj_type(self.obj_type) - - @property - def item_pos_x(self): - if self.parent.obj_type == "main_frame": - return int(self._pos_x) - return 0 - - @property - def item_pos_y(self): - if self.parent.obj_type == "main_frame": - return int(self._pos_y) - return 0 - - @property - def content_pos_x(self): - pos_x = self.item_pos_x - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - - pos_x += margin_left - - return pos_x - - @property - def content_pos_y(self): - pos_y = self.item_pos_y - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - return pos_y + margin_top - - @property - def value_pos_x(self): - pos_x = int(self.content_pos_x) - padding = self.style["padding"] - padding_left = self.style.get("padding-left") - if padding_left is None: - padding_left = padding - - pos_x += padding_left - - return pos_x - - @property - def value_pos_y(self): - pos_y = int(self.content_pos_y) - padding = self.style["padding"] - padding_top = self.style.get("padding-top") - if padding_top is None: - padding_top = padding - - pos_y += padding_top - - return pos_y - - @property - def value_pos_start(self): - return (self.value_pos_x, self.value_pos_y) - - @property - def value_pos_end(self): - pos_x, pos_y = self.value_pos_start - pos_x += self.width() - pos_y += self.height() - return (pos_x, pos_y) - - @property - def content_pos_start(self): - return (self.content_pos_x, self.content_pos_y) - - @property - def content_pos_end(self): - pos_x, pos_y = self.content_pos_start - pos_x += self.content_width() - pos_y += self.content_height() - return (pos_x, pos_y) - - def value_width(self): - raise NotImplementedError( - "Attribute is not implemented <{}>".format( - self.__class__.__name__ - ) - ) - - def value_height(self): - raise NotImplementedError( - "Attribute is not implemented for <{}>".format( - self.__class__.__name__ - ) - ) - - def content_width(self): - width = self.value_width() - padding = self.style["padding"] - padding_left = self.style.get("padding-left") - if padding_left is None: - padding_left = padding - - padding_right = self.style.get("padding-right") - if padding_right is None: - padding_right = padding - - return width + padding_left + padding_right - - def content_height(self): - height = self.value_height() - padding = self.style["padding"] - padding_top = self.style.get("padding-top") - if padding_top is None: - padding_top = padding - - padding_bottom = self.style.get("padding-bottom") - if padding_bottom is None: - padding_bottom = padding - - return height + padding_top + padding_bottom - - def width(self): - width = self.content_width() - - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - margin_right = self.style.get("margin-right") or margin - - return width + margin_left + margin_right - - def height(self): - height = self.content_height() - - margin = self.style["margin"] - margin_top = self.style.get("margin-top") or margin - margin_bottom = self.style.get("margin-bottom") or margin - - return height + margin_bottom + margin_top - - def add_item(self, item): - self.items[item.id] = item - item.fill_data_format() - - - def reset(self): - for item in self.items.values(): - item.reset() - - -class MainFrame(BaseObj): - - obj_type = "main_frame" - available_parents = [None] - - def __init__( - self, width, height, destination_path, fill_data={}, *args, **kwargs - ): - kwargs["parent"] = None - super(MainFrame, self).__init__(*args, **kwargs) - self._width = width - self._height = height - self.dst_path = destination_path - self._fill_data = fill_data - self.fill_data_format() - - def fill_data_format(self): - if re.match(self.fill_data_regex, self.dst_path): - self.dst_path = self.dst_path.format(**self.fill_data) - - @property - def fill_data(self): - return self._fill_data - - def value_width(self): - width = 0 - for item in self.items.values(): - width += item.width() - return width - - def value_height(self): - height = 0 - for item in self.items.values(): - height += item.height() - return height - - def width(self): - return self._width - - def height(self): - return self._height - - def draw(self, path=None): - dir_path = os.path.dirname(self.dst_path) - if not os.path.exists(dir_path): - os.makedirs(dir_path) - - bg_color = self.style["bg-color"] - image = Image.new("RGB", (self.width(), self.height()), color=bg_color) - drawer = ImageDraw.Draw(image) - for item in self.items.values(): - item.draw(image, drawer) - - image.save(self.dst_path) - self.reset() - - def collect_data(self): - output = {} - output["width"] = self.width() - output["height"] = self.height() - output["slate_path"] = self.dst_path - - placeholders = self.find_item(obj_type="placeholder") - placeholders_data = [] - for placeholder in placeholders: - placeholders_data.append(placeholder.collect_data()) - - output["placeholders"] = placeholders_data - - return output - - -class Layer(BaseObj): - obj_type = "layer" - available_parents = ["main_frame", "layer"] - - # Direction can be 0=vertical/ 1=horizontal - def __init__(self, direction=0, *args, **kwargs): - super(Layer, self).__init__(*args, **kwargs) - self._direction = direction - - @property - def item_pos_x(self): - if self.parent.obj_type == self.obj_type: - pos_x = self.parent.child_pos_x(self.id) - elif self.parent.obj_type == "main_frame": - pos_x = self._pos_x - else: - pos_x = self.parent.value_pos_x - return int(pos_x) - - @property - def item_pos_y(self): - if self.parent.obj_type == self.obj_type: - pos_y = self.parent.child_pos_y(self.id) - elif self.parent.obj_type == "main_frame": - pos_y = self._pos_y - else: - pos_y = self.parent.value_pos_y - - return int(pos_y) - - @property - def direction(self): - if self._direction not in (0, 1): - print( - "Direction must be 0 or 1 (0 is horizontal / 1 is vertical)!" - ) - return 0 - return self._direction - - def child_pos_x(self, item_id): - pos_x = self.value_pos_x - alignment_hor = self.style["alignment-horizontal"].lower() - - item = None - for id, _item in self.items.items(): - if item_id == id: - item = _item - break - - if self.direction == 1: - for id, _item in self.items.items(): - if item_id == id: - break - - pos_x += _item.width() - if _item.obj_type not in ["image", "placeholder"]: - pos_x += 1 - - else: - if alignment_hor in ["center", "centre"]: - pos_x += (self.content_width() - item.content_width()) / 2 - - elif alignment_hor == "right": - pos_x += self.content_width() - item.content_width() - - else: - margin = self.style["margin"] - margin_left = self.style.get("margin-left") or margin - pos_x += margin_left - - return int(pos_x) - - def child_pos_y(self, item_id): - pos_y = self.value_pos_y - alignment_ver = self.style["alignment-horizontal"].lower() - - item = None - for id, _item in self.items.items(): - if item_id == id: - item = _item - break - - if self.direction != 1: - for id, item in self.items.items(): - if item_id == id: - break - pos_y += item.height() - if item.obj_type not in ["image", "placeholder"]: - pos_y += 1 - - else: - if alignment_ver in ["center", "centre"]: - pos_y += (self.content_height() - item.content_height()) / 2 - - elif alignment_ver == "bottom": - pos_y += self.content_height() - item.content_height() - - return int(pos_y) - - def value_height(self): - height = 0 - for item in self.items.values(): - if self.direction == 1: - if height > item.height(): - continue - # times 1 because won't get object pointer but number - height = item.height() - else: - height += item.height() - - # TODO this is not right - min_height = self.style.get("min-height") - if min_height and min_height > height: - return min_height - return height - - def value_width(self): - width = 0 - for item in self.items.values(): - if self.direction == 0: - if width > item.width(): - continue - # times 1 because won't get object pointer but number - width = item.width() - else: - if self.name == "LeftSide": - print(item, item.width()) - width += item.width() - - min_width = self.style.get("min-width") - if min_width and min_width > width: - return min_width - return width - - def draw(self, image, drawer): - for item in self.items.values(): - item.draw(image, drawer) - - -class BaseItem(BaseObj): - available_parents = ["main_frame", "layer"] - - @property - def item_pos_x(self): - if self.parent.obj_type == "main_frame": - return self._pos_x - return self.parent.child_pos_x(self.id) - - @property - def item_pos_y(self): - if self.parent.obj_type == "main_frame": - return self._pos_y - return self.parent.child_pos_y(self.id) - - def add_item(self, *args, **kwargs): - raise Exception("Can't add item to an item, use layers instead.") - - def draw(self, image, drawer): - raise NotImplementedError( - "Method `draw` is not implemented for <{}>".format( - self.__clas__.__name__ - ) - ) - - -class ItemImage(BaseItem): - obj_type = "image" - - def __init__(self, image_path, *args, **kwargs): - super(ItemImage, self).__init__(*args, **kwargs) - self.image_path = image_path - - def fill_data_format(self): - if re.match(self.fill_data_regex, self.image_path): - self.image_path = self.image_path.format(**self.fill_data) - - def draw(self, image, drawer): - source_image = Image.open(os.path.normpath(self.image_path)) - paste_image = source_image.resize( - (self.value_width(), self.value_height()), - Image.ANTIALIAS - ) - image.paste( - paste_image, - (self.value_pos_x, self.value_pos_y) - ) - - def value_width(self): - return int(self.style["width"]) - - def value_height(self): - return int(self.style["height"]) - - -class ItemRectangle(BaseItem): - obj_type = "rectangle" - - def draw(self, image, drawer): - bg_color = self.style["bg-color"] - fill = self.style.get("fill", False) - kwargs = {} - if fill: - kwargs["fill"] = bg_color - else: - kwargs["outline"] = bg_color - - start_pos_x = self.value_pos_x - start_pos_y = self.value_pos_y - end_pos_x = start_pos_x + self.value_width() - end_pos_y = start_pos_y + self.value_height() - drawer.rectangle( - ( - (start_pos_x, start_pos_y), - (end_pos_x, end_pos_y) - ), - **kwargs - ) - - def value_width(self): - return int(self.style["width"]) - - def value_height(self): - return int(self.style["height"]) - - -class ItemPlaceHolder(BaseItem): - obj_type = "placeholder" - - def __init__(self, image_path, *args, **kwargs): - self.image_path = image_path - super(ItemPlaceHolder, self).__init__(*args, **kwargs) - - def fill_data_format(self): - if re.match(self.fill_data_regex, self.image_path): - self.image_path = self.image_path.format(**self.fill_data) - - def draw(self, image, drawer): - bg_color = self.style["bg-color"] - - kwargs = {} - if bg_color != "tranparent": - kwargs["fill"] = bg_color - - start_pos_x = self.value_pos_x - start_pos_y = self.value_pos_y - end_pos_x = start_pos_x + self.value_width() - end_pos_y = start_pos_y + self.value_height() - - drawer.rectangle( - ( - (start_pos_x, start_pos_y), - (end_pos_x, end_pos_y) - ), - **kwargs - ) - - def value_width(self): - return int(self.style["width"]) - - def value_height(self): - return int(self.style["height"]) - - def collect_data(self): - return { - "pos_x": self.value_pos_x, - "pos_y": self.value_pos_y, - "width": self.value_width(), - "height": self.value_height(), - "path": self.image_path - } - - -class ItemText(BaseItem): - obj_type = "text" - - def __init__(self, value, *args, **kwargs): - super(ItemText, self).__init__(*args, **kwargs) - self.value = value - - def draw(self, image, drawer): - bg_color = self.style["bg-color"] - if bg_color and bg_color.lower() != "transparent": - # TODO border outline styles - drawer.rectangle( - (self.content_pos_start, self.content_pos_end), - fill=bg_color, - outline=None - ) - - font_color = self.style["font-color"] - font_family = self.style["font-family"] - font_size = self.style["font-size"] - font_bold = self.style.get("font-bold", False) - font_italic = self.style.get("font-italic", False) - - font = FontFactory.get_font( - font_family, font_size, font_italic, font_bold - ) - drawer.text( - self.value_pos_start, - self.value, - font=font, - fill=font_color - ) - - def value_width(self): - font_family = self.style["font-family"] - font_size = self.style["font-size"] - font_bold = self.style.get("font-bold", False) - font_italic = self.style.get("font-italic", False) - - font = FontFactory.get_font( - font_family, font_size, font_italic, font_bold - ) - width = font.getsize(self.value)[0] - return int(width) - - def value_height(self): - font_family = self.style["font-family"] - font_size = self.style["font-size"] - font_bold = self.style.get("font-bold", False) - font_italic = self.style.get("font-italic", False) - - font = FontFactory.get_font( - font_family, font_size, font_italic, font_bold - ) - height = font.getsize(self.value)[1] - return int(height) - - -class ItemTable(BaseItem): - - obj_type = "table" - - def __init__(self, values, use_alternate_color=False, *args, **kwargs): - - self.values_by_cords = None - self.prepare_values(values) - - super(ItemTable, self).__init__(*args, **kwargs) - self.size_values = None - self.calculate_sizes() - - self.use_alternate_color = use_alternate_color - - def add_item(self, item): - if item.obj_type == "table-item": - return - super(ItemTable, self).add_item(item) - - def fill_data_format(self): - for item in self.values: - item.fill_data_format() - - def prepare_values(self, _values): - values = [] - values_by_cords = [] - row_count = 0 - col_count = 0 - for row in _values: - row_count += 1 - if len(row) > col_count: - col_count = len(row) - - for row_idx in range(row_count): - values_by_cords.append([]) - for col_idx in range(col_count): - values_by_cords[row_idx].append([]) - if col_idx <= len(_values[row_idx]) - 1: - col = _values[row_idx][col_idx] - else: - col = "" - - col_item = TableField(row_idx, col_idx, col, parent=self) - values_by_cords[row_idx][col_idx] = col_item - values.append(col_item) - - self.values = values - self.values_by_cords = values_by_cords - - def calculate_sizes(self): - row_heights = [] - col_widths = [] - for row_idx, row in enumerate(self.values_by_cords): - row_heights.append(0) - for col_idx, col_item in enumerate(row): - if len(col_widths) < col_idx + 1: - col_widths.append(0) - - _width = col_widths[col_idx] - item_width = col_item.width() - if _width < item_width: - col_widths[col_idx] = item_width - - _height = row_heights[row_idx] - item_height = col_item.height() - if _height < item_height: - row_heights[row_idx] = item_height - - self.size_values = (row_heights, col_widths) - - def draw(self, image, drawer): - bg_color = self.style["bg-color"] - if bg_color and bg_color.lower() != "transparent": - # TODO border outline styles - drawer.rectangle( - (self.content_pos_start, self.content_pos_end), - fill=bg_color, - outline=None - ) - - for value in self.values: - value.draw(image, drawer) - - def value_width(self): - row_heights, col_widths = self.size_values - width = 0 - for _width in col_widths: - width += _width - - if width != 0: - width -= 1 - return width - - def value_height(self): - row_heights, col_widths = self.size_values - height = 0 - for _height in row_heights: - height += _height - - if height != 0: - height -= 1 - return height - - def content_pos_info_by_cord(self, row_idx, col_idx): - row_heights, col_widths = self.size_values - pos_x = int(self.value_pos_x) - pos_y = int(self.value_pos_y) - width = 0 - height = 0 - for idx, value in enumerate(col_widths): - if col_idx == idx: - width = value - break - pos_x += value - - for idx, value in enumerate(row_heights): - if row_idx == idx: - height = value - break - pos_y += value - - return (pos_x, pos_y, width, height) - - -class TableField(BaseItem): - - obj_type = "table-item" - available_parents = ["table"] - ellide_text = "..." - - def __init__(self, row_idx, col_idx, value, *args, **kwargs): - super(TableField, self).__init__(*args, **kwargs) - self.row_idx = row_idx - self.col_idx = col_idx - self.value = value - - def recalculate_by_width(self, value, max_width): - padding = self.style["padding"] - padding_left = self.style.get("padding-left") - if padding_left is None: - padding_left = padding - - padding_right = self.style.get("padding-right") - if padding_right is None: - padding_right = padding - - max_width -= (padding_left + padding_right) - - if not value: - return "" - - word_wrap = self.style.get("word-wrap") - ellide = self.style.get("ellide") - max_lines = self.style.get("max-lines") - - font_family = self.style["font-family"] - font_size = self.style["font-size"] - font_bold = self.style.get("font-bold", False) - font_italic = self.style.get("font-italic", False) - - font = FontFactory.get_font( - font_family, font_size, font_italic, font_bold - ) - val_width = font.getsize(value)[0] - if val_width <= max_width: - return value - - if not ellide and not word_wrap: - # TODO logging - print(( - "Can't draw text because is too long with" - " `word-wrap` and `ellide` turned off" - )) - return "" - - elif ellide and not word_wrap: - max_lines = 1 - - words = [word for word in value.split()] - words_len = len(words) - lines = [] - last_index = None - while True: - start_index = 0 - if last_index is not None: - start_index = int(last_index) + 1 - - line = "" - for idx in range(start_index, words_len): - _word = words[idx] - connector = " " - if line == "": - connector = "" - - _line = connector.join([line, _word]) - _line_width = font.getsize(_line)[0] - if _line_width > max_width: - break - line = _line - last_index = idx - - if line: - lines.append(line) - - if last_index == words_len - 1: - break - - elif last_index is None: - if ellide: - line = "" - for idx, char in enumerate(words[idx]): - _line = line + char + self.ellide_text - _line_width = font.getsize(_line)[0] - if _line_width > max_width: - if idx == 0: - line = _line - break - line = line + char - - lines.append(line) - # TODO logging - print("Font size is too big.") - break - - output = "" - if not lines: - return output - - over_max_lines = (max_lines and len(lines) > max_lines) - if not over_max_lines: - return "\n".join([line for line in lines]) - - lines = [lines[idx] for idx in range(max_lines)] - if not ellide: - return "\n".join(lines) - - last_line = lines[-1] - last_line_width = font.getsize(last_line + self.ellide_text)[0] - if last_line_width <= max_width: - lines[-1] += self.ellide_text - return "\n".join([line for line in lines]) - - last_line_words = last_line.split() - if len(last_line_words) == 1: - if max_lines > 1: - # TODO try previous line? - lines[-1] = self.ellide_text - return "\n".join([line for line in lines]) - - line = "" - for idx, word in enumerate(last_line_words): - _line = line + word + self.ellide_text - _line_width = font.getsize(_line)[0] - if _line_width > max_width: - if idx == 0: - line = _line - break - line = _line - lines[-1] = line - - return "\n".join([line for line in lines]) - - line = "" - for idx, _word in enumerate(last_line_words): - connector = " " - if line == "": - connector = "" - - _line = connector.join([line, _word + self.ellide_text]) - _line_width = font.getsize(_line)[0] - - if _line_width <= max_width: - line = connector.join([line, _word]) - continue - - if idx != 0: - line += self.ellide_text - break - - if max_lines != 1: - # TODO try previous line? - line = self.ellide_text - break - - for idx, char in enumerate(_word): - _line = line + char + self.ellide_text - _line_width = font.getsize(_line)[0] - if _line_width > max_width: - if idx == 0: - line = _line - break - line = line + char - break - - lines[-1] = line - - return "\n".join([line for line in lines]) - - def fill_data_format(self): - value = self.value - if re.match(self.fill_data_regex, value): - value = value.format(**self.fill_data) - - self.orig_value = value - - max_width = self.style.get("max-width") - max_width = self.style.get("width") or max_width - if max_width: - value = self.recalculate_by_width(value, max_width) - - self.value = value - - def content_width(self): - width = self.style.get("width") - if width: - return int(width) - return super(TableField, self).content_width() - - def content_height(self): - return super(TableField, self).content_height() - - def value_width(self): - if not self.value: - return 0 - - font_family = self.style["font-family"] - font_size = self.style["font-size"] - font_bold = self.style.get("font-bold", False) - font_italic = self.style.get("font-italic", False) - - font = FontFactory.get_font( - font_family, font_size, font_italic, font_bold - ) - width = font.getsize_multiline(self.value)[0] + 1 - - min_width = self.style.get("min-height") - if min_width and min_width > width: - width = min_width - - return int(width) - - def value_height(self): - if not self.value: - return 0 - - height = self.style.get("height") - if height: - return int(height) - - font_family = self.style["font-family"] - font_size = self.style["font-size"] - font_bold = self.style.get("font-bold", False) - font_italic = self.style.get("font-italic", False) - - font = FontFactory.get_font( - font_family, font_size, font_italic, font_bold - ) - height = font.getsize_multiline(self.value)[1] + 1 - - min_height = self.style.get("min-height") - if min_height and min_height > height: - height = min_height - - return int(height) - - @property - def item_pos_x(self): - pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) - ) - return pos_x - - @property - def item_pos_y(self): - pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) - ) - return pos_y - - @property - def value_pos_x(self): - pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) - ) - alignment_hor = self.style["alignment-horizontal"].lower() - if alignment_hor in ["center", "centre"]: - pos_x += (width - self.value_width()) / 2 - - elif alignment_hor == "right": - pos_x += width - self.value_width() - - else: - padding = self.style["padding"] - padding_left = self.style.get("padding-left") - if padding_left is None: - padding_left = padding - - pos_x += padding_left - - return int(pos_x) - - @property - def value_pos_y(self): - pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) - ) - - alignment_ver = self.style["alignment-vertical"].lower() - if alignment_ver in ["center", "centre"]: - pos_y += (height - self.value_height()) / 2 - - elif alignment_ver == "bottom": - pos_y += height - self.value_height() - - else: - padding = self.style["padding"] - padding_top = self.style.get("padding-top") - if padding_top is None: - padding_top = padding - - pos_y += padding_top - - return int(pos_y) - - def draw(self, image, drawer): - pos_x, pos_y, width, height = ( - self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) - ) - pos_start = (pos_x, pos_y) - pos_end = (pos_x + width, pos_y + height) - bg_color = self.style["bg-color"] - if self.parent.use_alternate_color and (self.row_idx % 2) == 1: - bg_color = self.style["bg-alter-color"] - - if bg_color and bg_color.lower() != "transparent": - # TODO border outline styles - drawer.rectangle( - (pos_start, pos_end), - fill=bg_color, - outline=None - ) - - font_color = self.style["font-color"] - font_family = self.style["font-family"] - font_size = self.style["font-size"] - font_bold = self.style.get("font-bold", False) - font_italic = self.style.get("font-italic", False) - - font = FontFactory.get_font( - font_family, font_size, font_italic, font_bold - ) - - alignment_hor = self.style["alignment-horizontal"].lower() - if alignment_hor == "centre": - alignment_hor = "center" - - drawer.multiline_text( - self.value_pos_start, - self.value, - font=font, - fill=font_color, - align=alignment_hor - ) - - -class FontFactory: - fonts = None - default = None - - @classmethod - def get_font(cls, family, font_size=None, italic=False, bold=False): - if cls.fonts is None: - cls.load_fonts() - - styles = [] - if bold: - styles.append("Bold") - - if italic: - styles.append("Italic") - - if not styles: - styles.append("Regular") - - style = " ".join(styles) - family = family.lower() - family_styles = cls.fonts.get(family) - if not family_styles: - return cls.default - - font = family_styles.get(style) - if font: - if font_size: - font = font.font_variant(size=font_size) - return font - - # Return first found - for font in family_styles: - if font_size: - font = font.font_variant(size=font_size) - return font - - return cls.default - - @classmethod - def load_fonts(cls): - - cls.default = ImageFont.load_default() - - available_font_ext = [".ttf", ".ttc"] - dirs = [] - if sys.platform == "win32": - # check the windows font repository - # NOTE: must use uppercase WINDIR, to work around bugs in - # 1.5.2's os.environ.get() - windir = os.environ.get("WINDIR") - if windir: - dirs.append(os.path.join(windir, "fonts")) - - elif sys.platform in ("linux", "linux2"): - lindirs = os.environ.get("XDG_DATA_DIRS", "") - if not lindirs: - # According to the freedesktop spec, XDG_DATA_DIRS should - # default to /usr/share - lindirs = "/usr/share" - dirs += [ - os.path.join(lindir, "fonts") for lindir in lindirs.split(":") - ] - - elif sys.platform == "darwin": - dirs += [ - "/Library/Fonts", - "/System/Library/Fonts", - os.path.expanduser("~/Library/Fonts") - ] - - available_fonts = collections.defaultdict(dict) - for directory in dirs: - for walkroot, walkdir, walkfilenames in os.walk(directory): - for walkfilename in walkfilenames: - ext = os.path.splitext(walkfilename)[1] - if ext.lower() not in available_font_ext: - continue - - fontpath = os.path.join(walkroot, walkfilename) - font_obj = ImageFont.truetype(fontpath) - family = font_obj.font.family.lower() - style = font_obj.font.style - available_fonts[family][style] = font_obj - - cls.fonts = available_fonts - -def main(): - cur_folder = os.path.dirname(os.path.abspath(__file__)) - # input_json = os.path.join(cur_folder, "netflix_v01.json") - # input_json = os.path.join(cur_folder, "netflix_v02.json") - input_json = os.path.join(cur_folder, "netflix_v03.json") - with open(input_json) as json_file: - slate_data = json.load(json_file) - - fill_data = { - "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v03.png", - "project": { - "name": "Project name" - }, - "intent": "WIP", - "version_name": "mei_101_001_0020_slate_NFX_v001", - "date": "2019-08-09", - "shot_type": "2d comp", - "submission_note": "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production.", - "thumbnail_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", - "color_bar_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", - "vendor": "DAZZLE", - "shot_name": "SLATE_SIMPLE", - "frame_start": 1001, - "frame_end": 1004, - "duration": 3 - } - width = slate_data["width"] - height = slate_data["height"] - dst_path = slate_data["destination_path"] - style = slate_data.get("style") or {} - - main = MainFrame(width, height, dst_path, fill_data, style=style) - - load_queue = Queue() - for item in slate_data["items"]: - load_queue.put((item, main)) - - while not load_queue.empty(): - item_data, parent = load_queue.get() - - item_type = item_data["type"].lower() - item_style = item_data.get("style", {}) - item_name = item_data.get("name") - - pos_x = None - pos_y = None - if parent.obj_type == "main_frame": - pos_x = item_data.get("pos_x", {}) - pos_y = item_data.get("pos_y", {}) - - kwargs = { - "parent": parent, - "style": item_style, - "name": item_name, - "pos_x": pos_x, - "pos_y": pos_y - } - - if item_type == "layer": - direction = item_data.get("direction", 0) - item_obj = Layer(direction, **kwargs) - for item in item_data.get("items", []): - load_queue.put((item, item_obj)) - - elif item_type == "table": - use_alternate_color = item_data.get("use_alternate_color", False) - values = item_data.get("values") or [] - ItemTable(values, use_alternate_color, **kwargs) - - elif item_type == "image": - path = item_data["path"] - ItemImage(path, **kwargs) - - elif item_type == "rectangle": - ItemRectangle(**kwargs) - - elif item_type == "placeholder": - path = item_data["path"] - ItemPlaceHolder(path, **kwargs) - - else: - # TODO logging - print( - "Slate item not implemented <{}> - skipping".format(item_type) - ) - - main.draw() - print(main.collect_data()) - print("*** Finished") - - -if __name__ == "__main__": - main() diff --git a/pype/scripts/slate/default_style.json b/pype/scripts/slate/default_style.json deleted file mode 100644 index c0f1006a4a..0000000000 --- a/pype/scripts/slate/default_style.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "*": { - "font-family": "arial", - "font-size": 26, - "font-color": "#ffffff", - "font-bold": false, - "font-italic": false, - "bg-color": "#777777", - "bg-alter-color": "#666666", - "__alignment-vertical__values": ["left", "center", "right"], - "alignment-vertical": "left", - "__alignment-horizontal__values": ["top", "center", "bottom"], - "alignment-horizontal": "top", - "__padding__variants": [ - "padding-top", "padding-right", "padding-bottom", "padding-left" - ], - "padding": 0, - "__margin__variants": [ - "margin-top", "margin-right", "margin-bottom", "margin-left" - ], - "margin": 0, - }, - "layer": { - - }, - "image": { - - }, - "text": { - - }, - "table": { - - }, - "table-item": { - - }, - "table-item-col-0": { - - }, - "#MyName": { - - } -} diff --git a/pype/scripts/slate/lib.py b/pype/scripts/slate/lib.py new file mode 100644 index 0000000000..ca3c0f2e41 --- /dev/null +++ b/pype/scripts/slate/lib.py @@ -0,0 +1,114 @@ +import os +import json +from queue import Queue + +# --- Lines for debug purpose --------------------------------- +import sys +sys.path.append(r"C:\Users\Public\pype_env2\Lib\site-packages") +# ------------------------------------------------------------- + +from slate_base.main_frame import MainFrame +from slate_base.layer import Layer +from slate_base.items import ( + ItemTable, ItemImage, ItemRectangle, ItemPlaceHolder +) + + +def main(fill_data): + cur_folder = os.path.dirname(os.path.abspath(__file__)) + input_json = os.path.join(cur_folder, "netflix_v03.json") + with open(input_json) as json_file: + slate_data = json.load(json_file) + + + width = slate_data["width"] + height = slate_data["height"] + dst_path = slate_data["destination_path"] + style = slate_data.get("style") or {} + + main = MainFrame(width, height, dst_path, fill_data, style=style) + + load_queue = Queue() + for item in slate_data["items"]: + load_queue.put((item, main)) + + while not load_queue.empty(): + item_data, parent = load_queue.get() + + item_type = item_data["type"].lower() + item_style = item_data.get("style", {}) + item_name = item_data.get("name") + + pos_x = item_data.get("pos_x") + pos_y = item_data.get("pos_y") + if parent.obj_type != "main_frame": + if pos_x or pos_y: + # TODO logging + self.log.warning(( + "You have specified `pos_x` and `pos_y` but won't be used." + " Possible only if parent of an item is `main_frame`." + )) + pos_x = None + pos_y = None + + kwargs = { + "parent": parent, + "style": item_style, + "name": item_name, + "pos_x": pos_x, + "pos_y": pos_y + } + + if item_type == "layer": + direction = item_data.get("direction", 0) + item_obj = Layer(direction, **kwargs) + for item in item_data.get("items", []): + load_queue.put((item, item_obj)) + + elif item_type == "table": + use_alternate_color = item_data.get("use_alternate_color", False) + values = item_data.get("values") or [] + ItemTable(values, use_alternate_color, **kwargs) + + elif item_type == "image": + path = item_data["path"] + ItemImage(path, **kwargs) + + elif item_type == "rectangle": + ItemRectangle(**kwargs) + + elif item_type == "placeholder": + path = item_data["path"] + ItemPlaceHolder(path, **kwargs) + + else: + # TODO logging + self.log.warning( + "Slate item not implemented <{}> - skipping".format(item_type) + ) + + main.draw() + print("Slate creation finished") + + +if __name__ == "__main__": + fill_data = { + "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v03.png", + "project": { + "name": "Project name" + }, + "intent": "WIP", + "version_name": "mei_101_001_0020_slate_NFX_v001", + "date": "2019-08-09", + "shot_type": "2d comp", + "submission_note": "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production.", + "thumbnail_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", + "color_bar_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", + "vendor": "DAZZLE", + "shot_name": "SLATE_SIMPLE", + "frame_start": 1001, + "frame_end": 1004, + "duration": 3 + } + main(fill_data) + # raise NotImplementedError("Slates don't have Implemented args running") diff --git a/pype/scripts/slate/slate.py b/pype/scripts/slate/slate.py deleted file mode 100644 index 4e66f68d21..0000000000 --- a/pype/scripts/slate/slate.py +++ /dev/null @@ -1,355 +0,0 @@ -import textwrap -from PIL import Image, ImageFont, ImageDraw, ImageEnhance, ImageColor - - -class TableDraw: - def __init__( - self, image_width, image_height, - rel_pos_x, rel_pos_y, rel_width, - col_fonts=None, col_font_colors=None, - default_font=None, default_font_color=None, - col_alignments=None, rel_col_widths=None, bg_color=None, - alter_bg_color=None, pad=20, - pad_top=None, pad_bottom=None, pad_left=None, pad_right=None - ): - self.image_width = image_width - - pos_x = image_width * rel_pos_x - pos_y = image_height * rel_pos_y - width = image_width * rel_width - - self.pos_x_start = pos_x - self.pos_y_start = pos_y - self.pos_x_end = pos_x + width - self.width = width - - self.rel_col_widths = list(rel_col_widths) - self._col_widths = None - self._col_alignments = col_alignments - - if bg_color and isinstance(bg_color, str): - bg_color = ImageColor.getrgb(bg_color) - - if alter_bg_color and isinstance(alter_bg_color, str): - alter_bg_color = ImageColor.getrgb(alter_bg_color) - - self._bg_color = bg_color - self._alter_bg_color = alter_bg_color - - self.alter_use = False - - if col_fonts: - _col_fonts = [] - for col_font in col_fonts: - font_name, font_size = col_font - font = ImageFont.truetype(font_name, font_size) - _col_fonts.append(font) - col_fonts = _col_fonts - - self._col_fonts = col_fonts - - self._col_font_colors = col_font_colors - - if not default_font: - default_font = ImageFont.truetype("times", 26) - self.default_font = default_font - - if not default_font_color: - default_font_color = "#ffffff" - self.default_font_color = default_font_color - - self.texts = [] - - if pad is None: - pad = 5 - - _pad_top = pad - if pad_top is not None: - _pad_top = pad_top - - _pad_bottom = pad - if pad_bottom is not None: - _pad_bottom = pad_bottom - - _pad_left = pad - if pad_left is not None: - _pad_left = pad_left - - _pad_right = pad - if pad_right is not None: - _pad_right = pad_right - - self.pad_top = _pad_top - self.pad_bottom = _pad_bottom - self.pad_left = _pad_left - self.pad_right = _pad_right - - @property - def col_widths(self): - if self._col_widths is None: - sum_width = 0 - for w in self.rel_col_widths: - sum_width += w - - one_piece = self.width / sum_width - self._col_widths = [] - for w in self.rel_col_widths: - self._col_widths.append(one_piece * w) - - return self._col_widths - - @property - def col_fonts(self): - if self._col_fonts is None: - self._col_fonts = [] - for _ in range(len(self.col_widths)): - self._col_fonts.append(self.default_font) - - elif len(self._col_fonts) < len(self.col_widths): - if isinstance(self._col_fonts, tuple): - self._col_fonts = list(self._col_fonts) - - while len(self._col_fonts) < len(self.col_widths): - self._col_fonts.append(self.default_font) - - return self._col_fonts - - @property - def col_font_colors(self): - if self._col_font_colors is None: - self._col_font_colors = [] - for _ in range(len(self.col_widths)): - self._col_font_colors.append(self.default_font_color) - - elif len(self._col_font_colors) < len(self.col_widths): - if isinstance(self._col_font_colors, tuple): - self._col_font_colors = list(self._col_font_colors) - - while len(self._col_font_colors) < len(self.col_widths): - self._col_font_colors.append(self.default_font_color) - - return self._col_font_colors - - @property - def col_alignments(self): - if self._col_alignments is None: - self._col_alignments = [] - for _ in range(len(self.col_widths)): - self._col_alignments.append("left") - - elif len(self._col_alignments) < len(self.col_widths): - if isinstance(self._col_alignments, tuple): - self._col_alignments = list(self._col_alignments) - - while len(self._col_alignments) < len(self.col_widths): - self._col_alignments.append("left") - - return self._col_alignments - - @property - def bg_color(self): - if self.alter_use is True: - value = self.alter_bg_color - self.alter_use = False - else: - value = self._bg_color - self.alter_use = True - return value - - @property - def alter_bg_color(self): - if self._alter_bg_color: - return self._alter_bg_color - return self.bg_color - - def add_texts(self, texts): - if isinstance(texts, str): - texts = [texts] - - for text in texts: - if isinstance(text, str): - text = [text] - - if len(text) > len(self.rel_col_widths): - for _ in (len(text) - len(self.rel_col_widths)): - self.rel_col_widths.append(1) - for _t in self.texts: - _t.append("") - - self.texts.append(text) - - def draw(self, drawer): - y_pos = self.pos_y_start - for texts in self.texts: - max_height = None - cols_data = [] - for _idx, col in enumerate(texts): - width = self.col_widths[_idx] - font = self.col_fonts[_idx] - lines, line_height = self.lines_height_by_width( - drawer, col, width - self.pad_left - self.pad_right, font - ) - row_height = line_height * len(lines) - if max_height is None or row_height > max_height: - max_height = row_height - - cols_data.append({ - "lines": lines, - "line_height": line_height - }) - - drawer.rectangle( - ( - (self.pos_x_start, y_pos), - ( - self.pos_x_end, - y_pos + max_height + self.pad_top + self.pad_bottom - ) - ), - fill=self.bg_color - ) - - pos_x_start = self.pos_x_start + self.pad_left - for col, col_data in enumerate(cols_data): - lines = col_data["lines"] - line_height = col_data["line_height"] - alignment = self.col_alignments[col] - x_offset = self.col_widths[col] - font = self.col_fonts[col] - font_color = self.col_font_colors[col] - for idx, line_data in enumerate(lines): - line = line_data["text"] - line_width = line_data["width"] - if alignment == "left": - x_start = pos_x_start + self.pad_left - elif alignment == "right": - x_start = ( - pos_x_start + x_offset - line_width - - self.pad_right - self.pad_left - ) - else: - # TODO else - x_start = pos_x_start + self.pad_left - - drawer.text( - ( - x_start, - y_pos + (idx * line_height) + self.pad_top - ), - line, - font=font, - fill=font_color - ) - pos_x_start += x_offset - - y_pos += max_height + self.pad_top + self.pad_bottom - - def lines_height_by_width(self, drawer, text, width, font): - lines = [] - lines.append([part for part in text.split() if part]) - - line = 0 - while True: - thistext = lines[line] - line = line + 1 - if not thistext: - break - newline = [] - - while True: - _textwidth = drawer.textsize(" ".join(thistext), font)[0] - if ( - _textwidth <= width - ): - break - elif _textwidth > width and len(thistext) == 1: - # TODO raise error? - break - - val = thistext.pop(-1) - - if not val: - break - newline.insert(0, val) - - if len(newline) > 0: - lines.append(newline) - else: - break - - _lines = [] - height = None - for line_items in lines: - line = " ".join(line_items) - (width, _height) = drawer.textsize(line, font) - if height is None or height < _height: - height = _height - - _lines.append({ - "width": width, - "text": line - }) - - return (_lines, height) - - -width = 1920 -height = 1080 -# width = 800 -# height = 600 -bg_color_hex = "#242424" -bg_color = ImageColor.getrgb(bg_color_hex) - -base = Image.new('RGB', (width, height), color=bg_color) - -texts = [ - ("Version name:", "mei_101_001_0020_slate_NFX_v001"), - ("Date:", "2019-08-09"), - ("Shot Types:", "2d comp"), - # ("Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production.") -] -text_widths_rel = (2, 8) -col_alignments = ("right", "left") -fonts = (("arial", 20), ("times", 26)) -font_colors = ("#999999", "#ffffff") - -table_color_hex = "#212121" -table_alter_color_hex = "#272727" - -drawer = ImageDraw.Draw(base) -table_d = TableDraw( - width, height, - 0.1, 0.1, 0.5, - col_fonts=fonts, - col_font_colors=font_colors, - rel_col_widths=text_widths_rel, - col_alignments=col_alignments, - bg_color=table_color_hex, - alter_bg_color=table_alter_color_hex, - pad_top=20, pad_bottom=20, pad_left=5, pad_right=5 -) - -table_d.add_texts(texts) -table_d.draw(drawer) - -image_path = r"C:\Users\iLLiCiT\Desktop\Prace\Pillow\image.jpg" -image = Image.open(image_path) -img_width, img_height = image.size - -rel_image_width = 0.3 -rel_image_pos_x = 0.65 -rel_image_pos_y = 0.1 -image_pos_x = int(width * rel_image_pos_x) -image_pos_y = int(width * rel_image_pos_y) - -new_width = int(width * rel_image_width) -new_height = int(new_width * img_height / img_width) -image = image.resize((new_width, new_height), Image.ANTIALIAS) - -# mask = Image.new("L", image.size, 255) - -base.paste(image, (image_pos_x, image_pos_y)) -base.save(r"C:\Users\iLLiCiT\Desktop\Prace\Pillow\test{}x{}.jpg".format( - width, height -)) -base.show() diff --git a/pype/scripts/slate/slate_base/__init__.py b/pype/scripts/slate/slate_base/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pype/scripts/slate/slate_base/base.py b/pype/scripts/slate/slate_base/base.py new file mode 100644 index 0000000000..35a3b6af6d --- /dev/null +++ b/pype/scripts/slate/slate_base/base.py @@ -0,0 +1,373 @@ +import os +import re +import logging +import copy +import json +from uuid import uuid4 + + +def load_default_style(): + cur_folder = os.path.dirname(os.path.abspath(__file__)) + default_json_path = os.path.join(cur_folder, "default_style.json") + with open(default_json_path, "r") as _file: + data = _file.read() + return json.loads(data) + + +class BaseObj: + """Base Object for slates.""" + + obj_type = None + available_parents = [] + all_style_keys = [ + "font-family", "font-size", "font-color", "font-bold", "font-italic", + "bg-color", "bg-alter-color", + "alignment-horizontal", "alignment-vertical", + "padding", "padding-left", "padding-right", + "padding-top", "padding-bottom", + "margin", "margin-left", "margin-right", + "margin-top", "margin-bottom", "width", "height", + "fill", "word-wrap", "ellide", "max-lines" + ] + fill_data_regex = r"{[^}]+}" + + def __init__(self, parent, style={}, name=None, pos_x=None, pos_y=None): + if not self.obj_type: + raise NotImplementedError( + "Class don't have set object type <{}>".format( + self.__class__.__name__ + ) + ) + + parent_obj_type = None + if parent: + parent_obj_type = parent.obj_type + + if parent_obj_type not in self.available_parents: + expected_parents = ", ".join(self.available_parents) + raise Exception(( + "Invalid parent <{}> for <{}>. Expected <{}>" + ).format( + parent.__class__.__name__, self.obj_type, expected_parents + )) + + self.parent = parent + self._style = style + + self.id = uuid4() + self.name = name + self.items = {} + + self._pos_x = pos_x or 0 + self._pos_y = pos_y or 0 + + log_parts = [] + module = self.__class__.__module__ + if module and module != "__main__": + log_parts.append(module) + log_parts.append(self.__class__.__name__) + self.log = logging.getLogger(".".join(log_parts)) + + if parent: + parent.add_item(self) + + def fill_data_format(self): + return + + @property + def fill_data(self): + return self.parent.fill_data + + @property + def main_style(self): + return load_default_style() + + def height(self): + raise NotImplementedError( + "Attribute `height` is not implemented for <{}>".format( + self.__clas__.__name__ + ) + ) + + def width(self): + raise NotImplementedError( + "Attribute `width` is not implemented for <{}>".format( + self.__clas__.__name__ + ) + ) + + def collect_data(self): + return None + + def find_item(self, obj_type=None, name=None): + obj_type_fits = False + name_fits = False + if obj_type is None or self.obj_type == obj_type: + obj_type_fits = True + + if name is None or self.name != name: + name_fits = True + + output = [] + if obj_type_fits and name_fits: + output.append(self) + + if not self.items: + return output + + for item in self.items.values(): + output.extend( + item.find_item(obj_type=obj_type, name=name) + ) + return output + + @property + def full_style(self): + if self.parent is not None: + style = dict(val for val in self.parent.full_style.items()) + else: + style = self.main_style + + for key, value in self._style.items(): + if key in self.all_style_keys: + # TODO which variant is right? + style[self.obj_type][key] = value + # style["*"][key] = value + else: + if key not in style: + style[key] = {} + + if isinstance(style[key], dict): + style[key].update(value) + else: + style[key] = value + + return style + + def get_style_for_obj_type(self, obj_type, style=None): + if not style: + style = copy.deepcopy(self.full_style) + + base = style.get("*") or {} + obj_specific = style.get(obj_type) or {} + name_specific = {} + if self.name: + name = str(self.name) + if not name.startswith("#"): + name = "#" + name + name_specific = style.get(name) or {} + + if obj_type == "table-item": + col_regex = r"table-item-col\[([\d\-, ]+)*\]" + row_regex = r"table-item-row\[([\d\-, ]+)*\]" + field_regex = ( + r"table-item-field\[(([ ]+)?\d+([ ]+)?:([ ]+)?\d+([ ]+)?)*\]" + ) + # STRICT field regex (not allowed spaces) + # fild_regex = r"table-item-field\[(\d+:\d+)*\]" + + def get_indexes_from_regex_match(result, field=False): + group = result.group(1) + indexes = [] + if field: + return [ + int(part.strip()) for part in group.strip().split(":") + ] + + parts = group.strip().split(",") + for part in parts: + part = part.strip() + if "-" not in part: + indexes.append(int(part)) + continue + + sub_parts = [ + int(sub.strip()) for sub in part.split("-") + ] + if len(sub_parts) != 2: + # TODO logging + self.log.warning("Invalid range '{}'".format(part)) + continue + + for idx in range(sub_parts[0], sub_parts[1]+1): + indexes.append(idx) + return indexes + + for key, value in style.items(): + if not key.startswith(obj_type): + continue + + result = re.search(col_regex, key) + if result: + indexes = get_indexes_from_regex_match(result) + if self.col_idx in indexes: + obj_specific.update(value) + continue + + result = re.search(row_regex, key) + if result: + indexes = get_indexes_from_regex_match(result) + if self.row_idx in indexes: + obj_specific.update(value) + continue + + result = re.search(field_regex, key) + if result: + col_idx, row_idx = get_indexes_from_regex_match( + result, True + ) + if self.col_idx == col_idx and self.row_idx == row_idx: + obj_specific.update(value) + + output = {} + output.update(base) + output.update(obj_specific) + output.update(name_specific) + + return output + + @property + def style(self): + return self.get_style_for_obj_type(self.obj_type) + + @property + def item_pos_x(self): + if self.parent.obj_type == "main_frame": + return int(self._pos_x) + return 0 + + @property + def item_pos_y(self): + if self.parent.obj_type == "main_frame": + return int(self._pos_y) + return 0 + + @property + def content_pos_x(self): + pos_x = self.item_pos_x + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + + pos_x += margin_left + + return pos_x + + @property + def content_pos_y(self): + pos_y = self.item_pos_y + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + return pos_y + margin_top + + @property + def value_pos_x(self): + pos_x = int(self.content_pos_x) + padding = self.style["padding"] + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + + pos_x += padding_left + + return pos_x + + @property + def value_pos_y(self): + pos_y = int(self.content_pos_y) + padding = self.style["padding"] + padding_top = self.style.get("padding-top") + if padding_top is None: + padding_top = padding + + pos_y += padding_top + + return pos_y + + @property + def value_pos_start(self): + return (self.value_pos_x, self.value_pos_y) + + @property + def value_pos_end(self): + pos_x, pos_y = self.value_pos_start + pos_x += self.width() + pos_y += self.height() + return (pos_x, pos_y) + + @property + def content_pos_start(self): + return (self.content_pos_x, self.content_pos_y) + + @property + def content_pos_end(self): + pos_x, pos_y = self.content_pos_start + pos_x += self.content_width() + pos_y += self.content_height() + return (pos_x, pos_y) + + def value_width(self): + raise NotImplementedError( + "Attribute is not implemented <{}>".format( + self.__class__.__name__ + ) + ) + + def value_height(self): + raise NotImplementedError( + "Attribute is not implemented for <{}>".format( + self.__class__.__name__ + ) + ) + + def content_width(self): + width = self.value_width() + padding = self.style["padding"] + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + + padding_right = self.style.get("padding-right") + if padding_right is None: + padding_right = padding + + return width + padding_left + padding_right + + def content_height(self): + height = self.value_height() + padding = self.style["padding"] + padding_top = self.style.get("padding-top") + if padding_top is None: + padding_top = padding + + padding_bottom = self.style.get("padding-bottom") + if padding_bottom is None: + padding_bottom = padding + + return height + padding_top + padding_bottom + + def width(self): + width = self.content_width() + + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + margin_right = self.style.get("margin-right") or margin + + return width + margin_left + margin_right + + def height(self): + height = self.content_height() + + margin = self.style["margin"] + margin_top = self.style.get("margin-top") or margin + margin_bottom = self.style.get("margin-bottom") or margin + + return height + margin_bottom + margin_top + + def add_item(self, item): + self.items[item.id] = item + item.fill_data_format() + + + def reset(self): + for item in self.items.values(): + item.reset() diff --git a/pype/scripts/slate/slate_base/default_style.json b/pype/scripts/slate/slate_base/default_style.json new file mode 100644 index 0000000000..d0748846a5 --- /dev/null +++ b/pype/scripts/slate/slate_base/default_style.json @@ -0,0 +1,58 @@ +{ + "*": { + "font-family": "arial", + "font-size": 26, + "font-color": "#ffffff", + "font-bold": false, + "font-italic": false, + "bg-color": "#0077ff", + "alignment-horizontal": "left", + "alignment-vertical": "top", + "word-wrap": true, + "ellide": true, + "max-lines": null + }, + "layer": { + "padding": 0, + "margin": 0 + }, + "rectangle": { + "padding": 0, + "margin": 0, + "fill": true + }, + "image": { + "padding": 0, + "margin": 0, + "fill": true + }, + "placeholder": { + "padding": 0, + "margin": 0, + "fill": true + }, + "main_frame": { + "padding": 0, + "margin": 0, + "bg-color": "#252525" + }, + "table": { + "padding": 0, + "margin": 0, + "bg-color": "transparent" + }, + "table-item": { + "padding": 0, + "margin": 0, + "bg-color": "#212121", + "bg-alter-color": "#272727", + "font-color": "#dcdcdc", + "font-bold": false, + "font-italic": false, + "alignment-horizontal": "left", + "alignment-vertical": "top", + "word-wrap": false, + "ellide": true, + "max-lines": 1 + } +} diff --git a/pype/scripts/slate/slate_base/font_factory.py b/pype/scripts/slate/slate_base/font_factory.py new file mode 100644 index 0000000000..77df9a40a7 --- /dev/null +++ b/pype/scripts/slate/slate_base/font_factory.py @@ -0,0 +1,93 @@ +import os +import sys +import collections + +from PIL import ImageFont + + +class FontFactory: + fonts = None + default = None + + @classmethod + def get_font(cls, family, font_size=None, italic=False, bold=False): + if cls.fonts is None: + cls.load_fonts() + + styles = [] + if bold: + styles.append("Bold") + + if italic: + styles.append("Italic") + + if not styles: + styles.append("Regular") + + style = " ".join(styles) + family = family.lower() + family_styles = cls.fonts.get(family) + if not family_styles: + return cls.default + + font = family_styles.get(style) + if font: + if font_size: + font = font.font_variant(size=font_size) + return font + + # Return first found + for font in family_styles: + if font_size: + font = font.font_variant(size=font_size) + return font + + return cls.default + + @classmethod + def load_fonts(cls): + + cls.default = ImageFont.load_default() + + available_font_ext = [".ttf", ".ttc"] + dirs = [] + if sys.platform == "win32": + # check the windows font repository + # NOTE: must use uppercase WINDIR, to work around bugs in + # 1.5.2's os.environ.get() + windir = os.environ.get("WINDIR") + if windir: + dirs.append(os.path.join(windir, "fonts")) + + elif sys.platform in ("linux", "linux2"): + lindirs = os.environ.get("XDG_DATA_DIRS", "") + if not lindirs: + # According to the freedesktop spec, XDG_DATA_DIRS should + # default to /usr/share + lindirs = "/usr/share" + dirs += [ + os.path.join(lindir, "fonts") for lindir in lindirs.split(":") + ] + + elif sys.platform == "darwin": + dirs += [ + "/Library/Fonts", + "/System/Library/Fonts", + os.path.expanduser("~/Library/Fonts") + ] + + available_fonts = collections.defaultdict(dict) + for directory in dirs: + for walkroot, walkdir, walkfilenames in os.walk(directory): + for walkfilename in walkfilenames: + ext = os.path.splitext(walkfilename)[1] + if ext.lower() not in available_font_ext: + continue + + fontpath = os.path.join(walkroot, walkfilename) + font_obj = ImageFont.truetype(fontpath) + family = font_obj.font.family.lower() + style = font_obj.font.style + available_fonts[family][style] = font_obj + + cls.fonts = available_fonts diff --git a/pype/scripts/slate/slate_base/items.py b/pype/scripts/slate/slate_base/items.py new file mode 100644 index 0000000000..ea31443f80 --- /dev/null +++ b/pype/scripts/slate/slate_base/items.py @@ -0,0 +1,666 @@ +import re +from PIL import Image + +from .base import BaseObj +from .font_factory import FontFactory + + +class BaseItem(BaseObj): + available_parents = ["main_frame", "layer"] + + @property + def item_pos_x(self): + if self.parent.obj_type == "main_frame": + return self._pos_x + return self.parent.child_pos_x(self.id) + + @property + def item_pos_y(self): + if self.parent.obj_type == "main_frame": + return self._pos_y + return self.parent.child_pos_y(self.id) + + def add_item(self, *args, **kwargs): + raise Exception("Can't add item to an item, use layers instead.") + + def draw(self, image, drawer): + raise NotImplementedError( + "Method `draw` is not implemented for <{}>".format( + self.__clas__.__name__ + ) + ) + + +class ItemImage(BaseItem): + obj_type = "image" + + def __init__(self, image_path, *args, **kwargs): + super(ItemImage, self).__init__(*args, **kwargs) + self.image_path = image_path + + def fill_data_format(self): + if re.match(self.fill_data_regex, self.image_path): + self.image_path = self.image_path.format(**self.fill_data) + + def draw(self, image, drawer): + source_image = Image.open(os.path.normpath(self.image_path)) + paste_image = source_image.resize( + (self.value_width(), self.value_height()), + Image.ANTIALIAS + ) + image.paste( + paste_image, + (self.value_pos_x, self.value_pos_y) + ) + + def value_width(self): + return int(self.style["width"]) + + def value_height(self): + return int(self.style["height"]) + + +class ItemRectangle(BaseItem): + obj_type = "rectangle" + + def draw(self, image, drawer): + bg_color = self.style["bg-color"] + fill = self.style.get("fill", False) + kwargs = {} + if fill: + kwargs["fill"] = bg_color + else: + kwargs["outline"] = bg_color + + start_pos_x = self.value_pos_x + start_pos_y = self.value_pos_y + end_pos_x = start_pos_x + self.value_width() + end_pos_y = start_pos_y + self.value_height() + drawer.rectangle( + ( + (start_pos_x, start_pos_y), + (end_pos_x, end_pos_y) + ), + **kwargs + ) + + def value_width(self): + return int(self.style["width"]) + + def value_height(self): + return int(self.style["height"]) + + +class ItemPlaceHolder(BaseItem): + obj_type = "placeholder" + + def __init__(self, image_path, *args, **kwargs): + self.image_path = image_path + super(ItemPlaceHolder, self).__init__(*args, **kwargs) + + def fill_data_format(self): + if re.match(self.fill_data_regex, self.image_path): + self.image_path = self.image_path.format(**self.fill_data) + + def draw(self, image, drawer): + bg_color = self.style["bg-color"] + + kwargs = {} + if bg_color != "tranparent": + kwargs["fill"] = bg_color + + start_pos_x = self.value_pos_x + start_pos_y = self.value_pos_y + end_pos_x = start_pos_x + self.value_width() + end_pos_y = start_pos_y + self.value_height() + + drawer.rectangle( + ( + (start_pos_x, start_pos_y), + (end_pos_x, end_pos_y) + ), + **kwargs + ) + + def value_width(self): + return int(self.style["width"]) + + def value_height(self): + return int(self.style["height"]) + + def collect_data(self): + return { + "pos_x": self.value_pos_x, + "pos_y": self.value_pos_y, + "width": self.value_width(), + "height": self.value_height(), + "path": self.image_path + } + + +class ItemText(BaseItem): + obj_type = "text" + + def __init__(self, value, *args, **kwargs): + super(ItemText, self).__init__(*args, **kwargs) + self.value = value + + def draw(self, image, drawer): + bg_color = self.style["bg-color"] + if bg_color and bg_color.lower() != "transparent": + # TODO border outline styles + drawer.rectangle( + (self.content_pos_start, self.content_pos_end), + fill=bg_color, + outline=None + ) + + font_color = self.style["font-color"] + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + drawer.text( + self.value_pos_start, + self.value, + font=font, + fill=font_color + ) + + def value_width(self): + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + width = font.getsize(self.value)[0] + return int(width) + + def value_height(self): + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + height = font.getsize(self.value)[1] + return int(height) + + +class ItemTable(BaseItem): + + obj_type = "table" + + def __init__(self, values, use_alternate_color=False, *args, **kwargs): + + self.values_by_cords = None + self.prepare_values(values) + + super(ItemTable, self).__init__(*args, **kwargs) + self.size_values = None + self.calculate_sizes() + + self.use_alternate_color = use_alternate_color + + def add_item(self, item): + if item.obj_type == "table-item": + return + super(ItemTable, self).add_item(item) + + def fill_data_format(self): + for item in self.values: + item.fill_data_format() + + def prepare_values(self, _values): + values = [] + values_by_cords = [] + row_count = 0 + col_count = 0 + for row in _values: + row_count += 1 + if len(row) > col_count: + col_count = len(row) + + for row_idx in range(row_count): + values_by_cords.append([]) + for col_idx in range(col_count): + values_by_cords[row_idx].append([]) + if col_idx <= len(_values[row_idx]) - 1: + col = _values[row_idx][col_idx] + else: + col = "" + + col_item = TableField(row_idx, col_idx, col, parent=self) + values_by_cords[row_idx][col_idx] = col_item + values.append(col_item) + + self.values = values + self.values_by_cords = values_by_cords + + def calculate_sizes(self): + row_heights = [] + col_widths = [] + for row_idx, row in enumerate(self.values_by_cords): + row_heights.append(0) + for col_idx, col_item in enumerate(row): + if len(col_widths) < col_idx + 1: + col_widths.append(0) + + _width = col_widths[col_idx] + item_width = col_item.width() + if _width < item_width: + col_widths[col_idx] = item_width + + _height = row_heights[row_idx] + item_height = col_item.height() + if _height < item_height: + row_heights[row_idx] = item_height + + self.size_values = (row_heights, col_widths) + + def draw(self, image, drawer): + bg_color = self.style["bg-color"] + if bg_color and bg_color.lower() != "transparent": + # TODO border outline styles + drawer.rectangle( + (self.content_pos_start, self.content_pos_end), + fill=bg_color, + outline=None + ) + + for value in self.values: + value.draw(image, drawer) + + def value_width(self): + row_heights, col_widths = self.size_values + width = 0 + for _width in col_widths: + width += _width + + if width != 0: + width -= 1 + return width + + def value_height(self): + row_heights, col_widths = self.size_values + height = 0 + for _height in row_heights: + height += _height + + if height != 0: + height -= 1 + return height + + def content_pos_info_by_cord(self, row_idx, col_idx): + row_heights, col_widths = self.size_values + pos_x = int(self.value_pos_x) + pos_y = int(self.value_pos_y) + width = 0 + height = 0 + for idx, value in enumerate(col_widths): + if col_idx == idx: + width = value + break + pos_x += value + + for idx, value in enumerate(row_heights): + if row_idx == idx: + height = value + break + pos_y += value + + return (pos_x, pos_y, width, height) + + +class TableField(BaseItem): + + obj_type = "table-item" + available_parents = ["table"] + ellide_text = "..." + + def __init__(self, row_idx, col_idx, value, *args, **kwargs): + super(TableField, self).__init__(*args, **kwargs) + self.row_idx = row_idx + self.col_idx = col_idx + self.value = value + + def recalculate_by_width(self, value, max_width): + padding = self.style["padding"] + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + + padding_right = self.style.get("padding-right") + if padding_right is None: + padding_right = padding + + max_width -= (padding_left + padding_right) + + if not value: + return "" + + word_wrap = self.style.get("word-wrap") + ellide = self.style.get("ellide") + max_lines = self.style.get("max-lines") + + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + val_width = font.getsize(value)[0] + if val_width <= max_width: + return value + + if not ellide and not word_wrap: + # TODO logging + self.log.warning(( + "Can't draw text because is too long with" + " `word-wrap` and `ellide` turned off <{}>" + ).format(value)) + return "" + + elif ellide and not word_wrap: + max_lines = 1 + + words = [word for word in value.split()] + words_len = len(words) + lines = [] + last_index = None + while True: + start_index = 0 + if last_index is not None: + start_index = int(last_index) + 1 + + line = "" + for idx in range(start_index, words_len): + _word = words[idx] + connector = " " + if line == "": + connector = "" + + _line = connector.join([line, _word]) + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + break + line = _line + last_index = idx + + if line: + lines.append(line) + + if last_index == words_len - 1: + break + + elif last_index is None: + add_message = "" + if ellide: + add_message = " String was shortened to `{}`." + line = "" + for idx, char in enumerate(words[idx]): + _line = line + char + self.ellide_text + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + if idx == 0: + line = _line + break + line = line + char + + lines.append(line) + # TODO logging + self.log.warning(( + "Font size is too big.{} <{}>" + ).format(add_message, value)) + break + + output = "" + if not lines: + return output + + over_max_lines = (max_lines and len(lines) > max_lines) + if not over_max_lines: + return "\n".join([line for line in lines]) + + lines = [lines[idx] for idx in range(max_lines)] + if not ellide: + return "\n".join(lines) + + last_line = lines[-1] + last_line_width = font.getsize(last_line + self.ellide_text)[0] + if last_line_width <= max_width: + lines[-1] += self.ellide_text + return "\n".join([line for line in lines]) + + last_line_words = last_line.split() + if len(last_line_words) == 1: + if max_lines > 1: + # TODO try previous line? + lines[-1] = self.ellide_text + return "\n".join([line for line in lines]) + + line = "" + for idx, word in enumerate(last_line_words): + _line = line + word + self.ellide_text + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + if idx == 0: + line = _line + break + line = _line + lines[-1] = line + + return "\n".join([line for line in lines]) + + line = "" + for idx, _word in enumerate(last_line_words): + connector = " " + if line == "": + connector = "" + + _line = connector.join([line, _word + self.ellide_text]) + _line_width = font.getsize(_line)[0] + + if _line_width <= max_width: + line = connector.join([line, _word]) + continue + + if idx != 0: + line += self.ellide_text + break + + if max_lines != 1: + # TODO try previous line? + line = self.ellide_text + break + + for idx, char in enumerate(_word): + _line = line + char + self.ellide_text + _line_width = font.getsize(_line)[0] + if _line_width > max_width: + if idx == 0: + line = _line + break + line = line + char + break + + lines[-1] = line + + return "\n".join([line for line in lines]) + + def fill_data_format(self): + value = self.value + if re.match(self.fill_data_regex, value): + value = value.format(**self.fill_data) + + self.orig_value = value + + max_width = self.style.get("max-width") + max_width = self.style.get("width") or max_width + if max_width: + value = self.recalculate_by_width(value, max_width) + + self.value = value + + def content_width(self): + width = self.style.get("width") + if width: + return int(width) + return super(TableField, self).content_width() + + def content_height(self): + return super(TableField, self).content_height() + + def value_width(self): + if not self.value: + return 0 + + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + width = font.getsize_multiline(self.value)[0] + 1 + + min_width = self.style.get("min-height") + if min_width and min_width > width: + width = min_width + + return int(width) + + def value_height(self): + if not self.value: + return 0 + + height = self.style.get("height") + if height: + return int(height) + + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + height = font.getsize_multiline(self.value)[1] + 1 + + min_height = self.style.get("min-height") + if min_height and min_height > height: + height = min_height + + return int(height) + + @property + def item_pos_x(self): + pos_x, pos_y, width, height = ( + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) + ) + return pos_x + + @property + def item_pos_y(self): + pos_x, pos_y, width, height = ( + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) + ) + return pos_y + + @property + def value_pos_x(self): + pos_x, pos_y, width, height = ( + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) + ) + alignment_hor = self.style["alignment-horizontal"].lower() + if alignment_hor in ["center", "centre"]: + pos_x += (width - self.value_width()) / 2 + + elif alignment_hor == "right": + pos_x += width - self.value_width() + + else: + padding = self.style["padding"] + padding_left = self.style.get("padding-left") + if padding_left is None: + padding_left = padding + + pos_x += padding_left + + return int(pos_x) + + @property + def value_pos_y(self): + pos_x, pos_y, width, height = ( + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) + ) + + alignment_ver = self.style["alignment-vertical"].lower() + if alignment_ver in ["center", "centre"]: + pos_y += (height - self.value_height()) / 2 + + elif alignment_ver == "bottom": + pos_y += height - self.value_height() + + else: + padding = self.style["padding"] + padding_top = self.style.get("padding-top") + if padding_top is None: + padding_top = padding + + pos_y += padding_top + + return int(pos_y) + + def draw(self, image, drawer): + pos_x, pos_y, width, height = ( + self.parent.content_pos_info_by_cord(self.row_idx, self.col_idx) + ) + pos_start = (pos_x, pos_y) + pos_end = (pos_x + width, pos_y + height) + bg_color = self.style["bg-color"] + if self.parent.use_alternate_color and (self.row_idx % 2) == 1: + bg_color = self.style["bg-alter-color"] + + if bg_color and bg_color.lower() != "transparent": + # TODO border outline styles + drawer.rectangle( + (pos_start, pos_end), + fill=bg_color, + outline=None + ) + + font_color = self.style["font-color"] + font_family = self.style["font-family"] + font_size = self.style["font-size"] + font_bold = self.style.get("font-bold", False) + font_italic = self.style.get("font-italic", False) + + font = FontFactory.get_font( + font_family, font_size, font_italic, font_bold + ) + + alignment_hor = self.style["alignment-horizontal"].lower() + if alignment_hor == "centre": + alignment_hor = "center" + + drawer.multiline_text( + self.value_pos_start, + self.value, + font=font, + fill=font_color, + align=alignment_hor + ) diff --git a/pype/scripts/slate/slate_base/layer.py b/pype/scripts/slate/slate_base/layer.py new file mode 100644 index 0000000000..ea3a3de53e --- /dev/null +++ b/pype/scripts/slate/slate_base/layer.py @@ -0,0 +1,139 @@ +from .base import BaseObj + + +class Layer(BaseObj): + obj_type = "layer" + available_parents = ["main_frame", "layer"] + + # Direction can be 0=vertical/ 1=horizontal + def __init__(self, direction=0, *args, **kwargs): + super(Layer, self).__init__(*args, **kwargs) + self._direction = direction + + @property + def item_pos_x(self): + if self.parent.obj_type == self.obj_type: + pos_x = self.parent.child_pos_x(self.id) + elif self.parent.obj_type == "main_frame": + pos_x = self._pos_x + else: + pos_x = self.parent.value_pos_x + return int(pos_x) + + @property + def item_pos_y(self): + if self.parent.obj_type == self.obj_type: + pos_y = self.parent.child_pos_y(self.id) + elif self.parent.obj_type == "main_frame": + pos_y = self._pos_y + else: + pos_y = self.parent.value_pos_y + + return int(pos_y) + + @property + def direction(self): + if self._direction not in (0, 1): + self.log.warning(( + "Direction of Layer must be 0 or 1 " + "(0 is horizontal / 1 is vertical)! Setting to 0." + )) + return 0 + return self._direction + + def child_pos_x(self, item_id): + pos_x = self.value_pos_x + alignment_hor = self.style["alignment-horizontal"].lower() + + item = None + for id, _item in self.items.items(): + if item_id == id: + item = _item + break + + if self.direction == 1: + for id, _item in self.items.items(): + if item_id == id: + break + + pos_x += _item.width() + if _item.obj_type not in ["image", "placeholder"]: + pos_x += 1 + + else: + if alignment_hor in ["center", "centre"]: + pos_x += (self.content_width() - item.content_width()) / 2 + + elif alignment_hor == "right": + pos_x += self.content_width() - item.content_width() + + else: + margin = self.style["margin"] + margin_left = self.style.get("margin-left") or margin + pos_x += margin_left + + return int(pos_x) + + def child_pos_y(self, item_id): + pos_y = self.value_pos_y + alignment_ver = self.style["alignment-horizontal"].lower() + + item = None + for id, _item in self.items.items(): + if item_id == id: + item = _item + break + + if self.direction != 1: + for id, item in self.items.items(): + if item_id == id: + break + pos_y += item.height() + if item.obj_type not in ["image", "placeholder"]: + pos_y += 1 + + else: + if alignment_ver in ["center", "centre"]: + pos_y += (self.content_height() - item.content_height()) / 2 + + elif alignment_ver == "bottom": + pos_y += self.content_height() - item.content_height() + + return int(pos_y) + + def value_height(self): + height = 0 + for item in self.items.values(): + if self.direction == 1: + if height > item.height(): + continue + # times 1 because won't get object pointer but number + height = item.height() + else: + height += item.height() + + # TODO this is not right + min_height = self.style.get("min-height") + if min_height and min_height > height: + return min_height + return height + + def value_width(self): + width = 0 + for item in self.items.values(): + if self.direction == 0: + if width > item.width(): + continue + # times 1 because won't get object pointer but number + width = item.width() + else: + width += item.width() + + min_width = self.style.get("min-width") + if min_width and min_width > width: + return min_width + return width + + def draw(self, image, drawer): + for item in self.items.values(): + item.draw(image, drawer) diff --git a/pype/scripts/slate/slate_base/main_frame.py b/pype/scripts/slate/slate_base/main_frame.py new file mode 100644 index 0000000000..837e752aae --- /dev/null +++ b/pype/scripts/slate/slate_base/main_frame.py @@ -0,0 +1,77 @@ +import os +import re +from PIL import Image, ImageDraw + +from .base import BaseObj + + +class MainFrame(BaseObj): + + obj_type = "main_frame" + available_parents = [None] + + def __init__( + self, width, height, destination_path, fill_data={}, *args, **kwargs + ): + kwargs["parent"] = None + super(MainFrame, self).__init__(*args, **kwargs) + self._width = width + self._height = height + self.dst_path = destination_path + self._fill_data = fill_data + self.fill_data_format() + + def fill_data_format(self): + if re.match(self.fill_data_regex, self.dst_path): + self.dst_path = self.dst_path.format(**self.fill_data) + + @property + def fill_data(self): + return self._fill_data + + def value_width(self): + width = 0 + for item in self.items.values(): + width += item.width() + return width + + def value_height(self): + height = 0 + for item in self.items.values(): + height += item.height() + return height + + def width(self): + return self._width + + def height(self): + return self._height + + def draw(self, path=None): + dir_path = os.path.dirname(self.dst_path) + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + bg_color = self.style["bg-color"] + image = Image.new("RGB", (self.width(), self.height()), color=bg_color) + drawer = ImageDraw.Draw(image) + for item in self.items.values(): + item.draw(image, drawer) + + image.save(self.dst_path) + self.reset() + + def collect_data(self): + output = {} + output["width"] = self.width() + output["height"] = self.height() + output["slate_path"] = self.dst_path + + placeholders = self.find_item(obj_type="placeholder") + placeholders_data = [] + for placeholder in placeholders: + placeholders_data.append(placeholder.collect_data()) + + output["placeholders"] = placeholders_data + + return output diff --git a/pype/scripts/slate/style_schema.json b/pype/scripts/slate/style_schema.json deleted file mode 100644 index dab6151df5..0000000000 --- a/pype/scripts/slate/style_schema.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "alignment": { - "description": "Alignment of item", - "type": "string", - "enum": ["left", "right", "center"], - "example": "left" - }, - "font-family": { - "description": "Font type", - "type": "string", - "example": "Arial" - }, - "font-size": { - "description": "Font size", - "type": "integer", - "example": 26 - }, - "font-color": { - "description": "Font color", - "type": "string", - "regex": ( - "^[#]{1}[a-fA-F0-9]{1}$" - " | ^[#]{1}[a-fA-F0-9]{3}$" - " | ^[#]{1}[a-fA-F0-9]{6}$" - ), - "example": "#ffffff" - }, - "font-bold": { - "description": "Font boldness", - "type": "boolean", - "example": True - }, - "bg-color": { - "description": "Background color, None means transparent", - "type": ["string", None], - "regex": ( - "^[#]{1}[a-fA-F0-9]{1}$" - " | ^[#]{1}[a-fA-F0-9]{3}$" - " | ^[#]{1}[a-fA-F0-9]{6}$" - ), - "example": "#333" - }, - "bg-alter-color": None, -} From f54a3e981a12ff078fa70b0e6f8fa7f5e55a79a2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 17:07:59 +0100 Subject: [PATCH 33/99] created basic slate usage --- pype/scripts/slate/lib.py | 87 ++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/pype/scripts/slate/lib.py b/pype/scripts/slate/lib.py index ca3c0f2e41..750046269e 100644 --- a/pype/scripts/slate/lib.py +++ b/pype/scripts/slate/lib.py @@ -1,26 +1,46 @@ import os import json +import logging from queue import Queue -# --- Lines for debug purpose --------------------------------- -import sys -sys.path.append(r"C:\Users\Public\pype_env2\Lib\site-packages") -# ------------------------------------------------------------- - -from slate_base.main_frame import MainFrame -from slate_base.layer import Layer -from slate_base.items import ( +from .slate_base.main_frame import MainFrame +from .slate_base.layer import Layer +from .slate_base.items import ( ItemTable, ItemImage, ItemRectangle, ItemPlaceHolder ) +from pypeapp import config -def main(fill_data): - cur_folder = os.path.dirname(os.path.abspath(__file__)) - input_json = os.path.join(cur_folder, "netflix_v03.json") - with open(input_json) as json_file: - slate_data = json.load(json_file) +log = logging.getLogger(__name__) +RequiredSlateKeys = ["width", "height", "destination_path"] + + +def create_slates(fill_data, slate_name): + presets = config.get_presets() + slate_presets = ( + presets + .get("tools", {}) + .get("slates") + ) or {} + slate_data = slate_presets.get(slate_name) + + if not slate_data: + log.error("Slate data of <{}> does not exists.") + return False + + missing_keys = [] + for key in RequiredSlateKeys: + if key not in slate_data: + missing_keys.append("`{}`".format(key)) + + if missing_keys: + log.error("Slate data of <{}> miss required keys: {}".format( + slate_name, ", ".join(missing_keys) + )) + return False + width = slate_data["width"] height = slate_data["height"] dst_path = slate_data["destination_path"] @@ -44,7 +64,7 @@ def main(fill_data): if parent.obj_type != "main_frame": if pos_x or pos_y: # TODO logging - self.log.warning(( + log.warning(( "You have specified `pos_x` and `pos_y` but won't be used." " Possible only if parent of an item is `main_frame`." )) @@ -83,32 +103,43 @@ def main(fill_data): else: # TODO logging - self.log.warning( - "Slate item not implemented <{}> - skipping".format(item_type) + log.warning( + "Not implemented object type `{}` - skipping".format(item_type) ) main.draw() - print("Slate creation finished") + log.debug("Slate creation finished") -if __name__ == "__main__": +def example(): fill_data = { - "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v03.png", + "destination_path": "PATH/TO/OUTPUT/FILE", "project": { - "name": "Project name" + "name": "Testing project" }, "intent": "WIP", - "version_name": "mei_101_001_0020_slate_NFX_v001", + "version_name": "seq01_sh0100_compositing_v01", "date": "2019-08-09", "shot_type": "2d comp", - "submission_note": "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production.", - "thumbnail_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", - "color_bar_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", - "vendor": "DAZZLE", - "shot_name": "SLATE_SIMPLE", + "submission_note": ( + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit." + " Aenean commodo ligula eget dolor. Aenean massa." + " Cum sociis natoque penatibus et magnis dis parturient montes," + " nascetur ridiculus mus. Donec quam felis, ultricies nec," + " pellentesque eu, pretium quis, sem. Nulla consequat massa quis" + " enim. Donec pede justo, fringilla vel," + " aliquet nec, vulputate eget, arcu." + ), + "thumbnail_path": "PATH/TO/THUMBNAIL/FILE", + "color_bar_path": "PATH/TO/COLOR/BAR/FILE", + "vendor": "Our Studio", + "shot_name": "sh0100", "frame_start": 1001, "frame_end": 1004, "duration": 3 } - main(fill_data) - # raise NotImplementedError("Slates don't have Implemented args running") + create_slates(fill_data, "example_HD") + + +if __name__ == "__main__": + raise NotImplementedError("Slates don't have Implemented args running") From 5b0e8fe8d0dac8c8e8a70e3557cc53a866932c7f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 17:08:17 +0100 Subject: [PATCH 34/99] remove netflix examples --- pype/scripts/slate/netflix_v01.json | 201 -------------------------- pype/scripts/slate/netflix_v02.json | 213 ---------------------------- pype/scripts/slate/netflix_v03.json | 213 ---------------------------- 3 files changed, 627 deletions(-) delete mode 100644 pype/scripts/slate/netflix_v01.json delete mode 100644 pype/scripts/slate/netflix_v02.json delete mode 100644 pype/scripts/slate/netflix_v03.json diff --git a/pype/scripts/slate/netflix_v01.json b/pype/scripts/slate/netflix_v01.json deleted file mode 100644 index 9a5974839d..0000000000 --- a/pype/scripts/slate/netflix_v01.json +++ /dev/null @@ -1,201 +0,0 @@ -{ - "width": 1920, - "height": 1080, - "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v01.png", - "style": { - "*": { - "font-family": "arial", - "font-size": 26, - "font-color": "#ffffff", - "font-bold": false, - "font-italic": false, - "bg-color": "#0077ff", - "alignment-horizontal": "left", - "alignment-vertical": "top", - "padding": 0, - "margin": 0 - }, - "layer": { - "padding": 0, - "margin": 0 - }, - "rectangle": { - "padding": 0, - "margin": 0, - "bg-color": "#E9324B", - "fill": true - }, - "main_frame": { - "bg-color": "#252525" - }, - "table": { - "bg-color": "transparent" - }, - "table-item": { - "bg-color": "#212121", - "bg-alter-color": "#272727", - "font-color": "#dcdcdc", - "font-bold": false, - "font-italic": false, - "padding": 5, - "padding-bottom": 10, - "alignment-horizontal": "left", - "alignment-vertical": "top", - "word-wrap": false, - "ellide": true, - "max-lines": 1 - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "font-bold": true, - "ellide": false, - "word-wrap": true, - "max-lines": null - }, - "table-item-col[1]": { - "font-size": 40, - "padding-left": 10 - }, - "#colorbar": { - "bg-color": "#9932CC" - } - }, - "items": [{ - "type": "layer", - "name": "LeftSide", - "pos_x": 27, - "pos_y": 27, - "style": { - "width": 1094, - "height": 1000 - }, - "items": [{ - "type": "layer", - "direction": 1, - "style": { - "table-item": { - "bg-color": "transparent" - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "alignment-horizontal": "right" - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "font-bold": true, - "font-size": 40 - } - }, - "items": [{ - "type": "table", - "values": [ - ["Show:", "First Contact"] - ], - "style": { - "table-item-field[0:0]": { - "width": 150 - }, - "table-item-field[1:0]": { - "width": 580 - } - } - }, { - "type": "table", - "values": [ - ["Submitting For:", "SAMPLE"] - ], - "style": { - "table-item-field[0:0]": { - "width": 160 - }, - "table-item-field[1:0]": { - "width": 218, - "alignment-horizontal": "right" - } - } - }] - }, { - "type": "rectangle", - "style": { - "bg-color": "#bc1015", - "width": 1108, - "height": 5, - "fill": true - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["Version name:", "mei_101_001_0020_slate_NFX_v001"], - ["Date:", "2019-08-09"], - ["Shot Types:", "2d comp"], - ["Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production."] - ], - "style": { - "table-item": { - "padding-bottom": 20 - }, - "table-item-field[1:0]": { - "font-bold": true - }, - "table-item-field[1:3]": { - "word-wrap": true, - "ellide": true, - "max-lines": 2 - }, - "table-item-col[0]": { - "alignment-horizontal": "right", - "width": 150 - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "width": 958 - } - } - }] - }, { - "type": "layer", - "name": "RightSide", - "pos_x": 1164, - "pos_y": 26, - "items": [{ - "type": "placeholder", - "name": "thumbnail", - "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", - "style": { - "width": 730, - "height": 412 - } - }, { - "type": "placeholder", - "name": "colorbar", - "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", - "return_data": true, - "style": { - "width": 730, - "height": 55 - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["Vendor:", "DAZZLE"], - ["Shot Name:", "SLATE_SIMPLE"], - ["Frames:", "0 - 1 (2)"] - ], - "style": { - "table-item-col[0]": { - "alignment-horizontal": "left", - "width": 200 - }, - "table-item-col[1]": { - "alignment-horizontal": "right", - "width": 530, - "font-size": 30 - } - } - }] - }] -} diff --git a/pype/scripts/slate/netflix_v02.json b/pype/scripts/slate/netflix_v02.json deleted file mode 100644 index f373ed8134..0000000000 --- a/pype/scripts/slate/netflix_v02.json +++ /dev/null @@ -1,213 +0,0 @@ -{ - "width": 1920, - "height": 1080, - "destination_path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/netflix_output_v02.png", - "style": { - "*": { - "font-family": "arial", - "font-size": 26, - "font-color": "#ffffff", - "font-bold": false, - "font-italic": false, - "bg-color": "#0077ff", - "alignment-horizontal": "left", - "alignment-vertical": "top" - }, - "layer": { - "padding": 0, - "margin": 0 - }, - "rectangle": { - "padding": 0, - "margin": 0, - "bg-color": "#E9324B", - "fill": true - }, - "main_frame": { - "padding": 0, - "margin": 0, - "bg-color": "#252525" - }, - "table": { - "padding": 0, - "margin": 0, - "bg-color": "transparent" - }, - "table-item": { - "padding": 5, - "padding-bottom": 10, - "margin": 0, - "bg-color": "#212121", - "bg-alter-color": "#272727", - "font-color": "#dcdcdc", - "font-bold": false, - "font-italic": false, - "alignment-horizontal": "left", - "alignment-vertical": "top", - "word-wrap": false, - "ellide": true, - "max-lines": 1 - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "font-bold": true, - "ellide": false, - "word-wrap": true, - "max-lines": null - }, - "table-item-col[1]": { - "font-size": 40, - "padding-left": 10 - }, - "#colorbar": { - "bg-color": "#9932CC" - } - }, - "items": [{ - "type": "layer", - "direction": 1, - "name": "MainLayer", - "style": { - "#MainLayer": { - "width": 1094, - "height": 1000, - "margin": 25, - "padding": 0 - }, - "#LeftSide": { - "margin-right": 25 - } - }, - "items": [{ - "type": "layer", - "name": "LeftSide", - "items": [{ - "type": "layer", - "direction": 1, - "style": { - "table-item": { - "bg-color": "transparent", - "padding-bottom": 20 - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "alignment-horizontal": "right" - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "font-bold": true, - "font-size": 40 - } - }, - "items": [{ - "type": "table", - "values": [ - ["Show:", "First Contact"] - ], - "style": { - "table-item-field[0:0]": { - "width": 150 - }, - "table-item-field[1:0]": { - "width": 580 - } - } - }, { - "type": "table", - "values": [ - ["Submitting For:", "SAMPLE"] - ], - "style": { - "table-item-field[0:0]": { - "width": 160 - }, - "table-item-field[1:0]": { - "width": 218, - "alignment-horizontal": "right" - } - } - }] - }, { - "type": "rectangle", - "style": { - "bg-color": "#bc1015", - "width": 1108, - "height": 5, - "fill": true - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["Version name:", "mei_101_001_0020_slate_NFX_v001"], - ["Date:", "2019-08-09"], - ["Shot Types:", "2d comp"], - ["Submission Note:", "Submitting as and example with all MEI fields filled out. As well as the additional fields Shot description, Episode, Scene, and Version # that were requested by production."] - ], - "style": { - "table-item": { - "padding-bottom": 20 - }, - "table-item-field[1:0]": { - "font-bold": true - }, - "table-item-field[1:3]": { - "word-wrap": true, - "ellide": true, - "max-lines": 2 - }, - "table-item-col[0]": { - "alignment-horizontal": "right", - "width": 150 - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "width": 958 - } - } - }] - }, { - "type": "layer", - "name": "RightSide", - "items": [{ - "type": "placeholder", - "name": "thumbnail", - "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/birds.png", - "style": { - "width": 730, - "height": 412 - } - }, { - "type": "placeholder", - "name": "colorbar", - "path": "C:/Users/jakub.trllo/Desktop/Tests/files/image/kitten.jpg", - "return_data": true, - "style": { - "width": 730, - "height": 55 - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["Vendor:", "DAZZLE"], - ["Shot Name:", "SLATE_SIMPLE"], - ["Frames:", "0 - 1 (2)"] - ], - "style": { - "table-item-col[0]": { - "alignment-horizontal": "left", - "width": 200 - }, - "table-item-col[1]": { - "alignment-horizontal": "right", - "width": 530, - "font-size": 30 - } - } - }] - }] - }] -} diff --git a/pype/scripts/slate/netflix_v03.json b/pype/scripts/slate/netflix_v03.json deleted file mode 100644 index 975cb60133..0000000000 --- a/pype/scripts/slate/netflix_v03.json +++ /dev/null @@ -1,213 +0,0 @@ -{ - "width": 1920, - "height": 1080, - "destination_path": "{destination_path}", - "style": { - "*": { - "font-family": "arial", - "font-size": 26, - "font-color": "#ffffff", - "font-bold": false, - "font-italic": false, - "bg-color": "#0077ff", - "alignment-horizontal": "left", - "alignment-vertical": "top" - }, - "layer": { - "padding": 0, - "margin": 0 - }, - "rectangle": { - "padding": 0, - "margin": 0, - "bg-color": "#E9324B", - "fill": true - }, - "main_frame": { - "padding": 0, - "margin": 0, - "bg-color": "#252525" - }, - "table": { - "padding": 0, - "margin": 0, - "bg-color": "transparent" - }, - "table-item": { - "padding": 5, - "padding-bottom": 10, - "margin": 0, - "bg-color": "#212121", - "bg-alter-color": "#272727", - "font-color": "#dcdcdc", - "font-bold": false, - "font-italic": false, - "alignment-horizontal": "left", - "alignment-vertical": "top", - "word-wrap": false, - "ellide": true, - "max-lines": 1 - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "font-bold": true, - "ellide": false, - "word-wrap": true, - "max-lines": null - }, - "table-item-col[1]": { - "font-size": 40, - "padding-left": 10 - }, - "#colorbar": { - "bg-color": "#9932CC" - } - }, - "items": [{ - "type": "layer", - "direction": 1, - "name": "MainLayer", - "style": { - "#MainLayer": { - "width": 1094, - "height": 1000, - "margin": 25, - "padding": 0 - }, - "#LeftSide": { - "margin-right": 25 - } - }, - "items": [{ - "type": "layer", - "name": "LeftSide", - "items": [{ - "type": "layer", - "direction": 1, - "style": { - "table-item": { - "bg-color": "transparent", - "padding-bottom": 20 - }, - "table-item-col[0]": { - "font-size": 20, - "font-color": "#898989", - "alignment-horizontal": "right" - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "font-bold": true, - "font-size": 40 - } - }, - "items": [{ - "type": "table", - "values": [ - ["Show:", "{project[name]}"] - ], - "style": { - "table-item-field[0:0]": { - "width": 150 - }, - "table-item-field[1:0]": { - "width": 580 - } - } - }, { - "type": "table", - "values": [ - ["Submitting For:", "{intent}"] - ], - "style": { - "table-item-field[0:0]": { - "width": 160 - }, - "table-item-field[1:0]": { - "width": 218, - "alignment-horizontal": "right" - } - } - }] - }, { - "type": "rectangle", - "style": { - "bg-color": "#bc1015", - "width": 1108, - "height": 5, - "fill": true - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["Version name:", "{version_name}"], - ["Date:", "{date}"], - ["Shot Types:", "{shot_type}"], - ["Submission Note:", "{submission_note}"] - ], - "style": { - "table-item": { - "padding-bottom": 20 - }, - "table-item-field[1:0]": { - "font-bold": true - }, - "table-item-field[1:3]": { - "word-wrap": true, - "ellide": true, - "max-lines": 4 - }, - "table-item-col[0]": { - "alignment-horizontal": "right", - "width": 150 - }, - "table-item-col[1]": { - "alignment-horizontal": "left", - "width": 958 - } - } - }] - }, { - "type": "layer", - "name": "RightSide", - "items": [{ - "type": "placeholder", - "name": "thumbnail", - "path": "{thumbnail_path}", - "style": { - "width": 730, - "height": 412 - } - }, { - "type": "placeholder", - "name": "colorbar", - "path": "{color_bar_path}", - "return_data": true, - "style": { - "width": 730, - "height": 55 - } - }, { - "type": "table", - "use_alternate_color": true, - "values": [ - ["Vendor:", "{vendor}"], - ["Shot Name:", "{shot_name}"], - ["Frames:", "{frame_start} - {frame_end} ({duration})"] - ], - "style": { - "table-item-col[0]": { - "alignment-horizontal": "left", - "width": 200 - }, - "table-item-col[1]": { - "alignment-horizontal": "right", - "width": 530, - "font-size": 30 - } - } - }] - }] - }] -} From b3f4673248a8ec06dbc44d6457dbb095688aaa20 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 17:09:16 +0100 Subject: [PATCH 35/99] renamed slate folder to slates folder --- pype/scripts/{slate => slates}/lib.py | 0 pype/scripts/{slate => slates}/slate_base/__init__.py | 0 pype/scripts/{slate => slates}/slate_base/base.py | 0 pype/scripts/{slate => slates}/slate_base/default_style.json | 0 pype/scripts/{slate => slates}/slate_base/font_factory.py | 0 pype/scripts/{slate => slates}/slate_base/items.py | 0 pype/scripts/{slate => slates}/slate_base/layer.py | 0 pype/scripts/{slate => slates}/slate_base/main_frame.py | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename pype/scripts/{slate => slates}/lib.py (100%) rename pype/scripts/{slate => slates}/slate_base/__init__.py (100%) rename pype/scripts/{slate => slates}/slate_base/base.py (100%) rename pype/scripts/{slate => slates}/slate_base/default_style.json (100%) rename pype/scripts/{slate => slates}/slate_base/font_factory.py (100%) rename pype/scripts/{slate => slates}/slate_base/items.py (100%) rename pype/scripts/{slate => slates}/slate_base/layer.py (100%) rename pype/scripts/{slate => slates}/slate_base/main_frame.py (100%) diff --git a/pype/scripts/slate/lib.py b/pype/scripts/slates/lib.py similarity index 100% rename from pype/scripts/slate/lib.py rename to pype/scripts/slates/lib.py diff --git a/pype/scripts/slate/slate_base/__init__.py b/pype/scripts/slates/slate_base/__init__.py similarity index 100% rename from pype/scripts/slate/slate_base/__init__.py rename to pype/scripts/slates/slate_base/__init__.py diff --git a/pype/scripts/slate/slate_base/base.py b/pype/scripts/slates/slate_base/base.py similarity index 100% rename from pype/scripts/slate/slate_base/base.py rename to pype/scripts/slates/slate_base/base.py diff --git a/pype/scripts/slate/slate_base/default_style.json b/pype/scripts/slates/slate_base/default_style.json similarity index 100% rename from pype/scripts/slate/slate_base/default_style.json rename to pype/scripts/slates/slate_base/default_style.json diff --git a/pype/scripts/slate/slate_base/font_factory.py b/pype/scripts/slates/slate_base/font_factory.py similarity index 100% rename from pype/scripts/slate/slate_base/font_factory.py rename to pype/scripts/slates/slate_base/font_factory.py diff --git a/pype/scripts/slate/slate_base/items.py b/pype/scripts/slates/slate_base/items.py similarity index 100% rename from pype/scripts/slate/slate_base/items.py rename to pype/scripts/slates/slate_base/items.py diff --git a/pype/scripts/slate/slate_base/layer.py b/pype/scripts/slates/slate_base/layer.py similarity index 100% rename from pype/scripts/slate/slate_base/layer.py rename to pype/scripts/slates/slate_base/layer.py diff --git a/pype/scripts/slate/slate_base/main_frame.py b/pype/scripts/slates/slate_base/main_frame.py similarity index 100% rename from pype/scripts/slate/slate_base/main_frame.py rename to pype/scripts/slates/slate_base/main_frame.py From 16bc4dcc6098dd1e514aac06851a83630a7fdfef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Jan 2020 17:16:02 +0100 Subject: [PATCH 36/99] exchange col_idx and row_idx for field style --- pype/scripts/slates/slate_base/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/scripts/slates/slate_base/base.py b/pype/scripts/slates/slate_base/base.py index 35a3b6af6d..35ef46769c 100644 --- a/pype/scripts/slates/slate_base/base.py +++ b/pype/scripts/slates/slate_base/base.py @@ -213,7 +213,7 @@ class BaseObj: result = re.search(field_regex, key) if result: - col_idx, row_idx = get_indexes_from_regex_match( + row_idx, col_idx = get_indexes_from_regex_match( result, True ) if self.col_idx == col_idx and self.row_idx == row_idx: From f72a19457bafe003ebc1c7a5deb12fdfbeb414bc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Jan 2020 10:28:54 +0100 Subject: [PATCH 37/99] added example for testing --- pype/scripts/slates/lib.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pype/scripts/slates/lib.py b/pype/scripts/slates/lib.py index 750046269e..a73f87e82f 100644 --- a/pype/scripts/slates/lib.py +++ b/pype/scripts/slates/lib.py @@ -1,5 +1,3 @@ -import os -import json import logging from queue import Queue @@ -112,6 +110,10 @@ def create_slates(fill_data, slate_name): def example(): + # import sys + # sys.append(r"PATH/TO/PILLOW/PACKAGE") + # sys.append(r"PATH/TO/PYPE-SETUP") + fill_data = { "destination_path": "PATH/TO/OUTPUT/FILE", "project": { @@ -142,4 +144,4 @@ def example(): if __name__ == "__main__": - raise NotImplementedError("Slates don't have Implemented args running") + example() From 631942327edc0469834c5cbb2e328a4caae0586e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 4 Feb 2020 10:43:51 +0000 Subject: [PATCH 38/99] Creation and publishing of a rig in Blender --- pype/blender/plugin.py | 7 +++ pype/plugins/blender/create/create_rig.py | 32 +++++++++++++ pype/plugins/blender/publish/collect_rig.py | 53 +++++++++++++++++++++ pype/plugins/blender/publish/extract_rig.py | 47 ++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 pype/plugins/blender/create/create_rig.py create mode 100644 pype/plugins/blender/publish/collect_rig.py create mode 100644 pype/plugins/blender/publish/extract_rig.py diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index ad5a259785..eaa429c989 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -17,6 +17,13 @@ def model_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: name = f"{namespace}:{name}" return name +def rig_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: + """Return a consistent name for a rig asset.""" + name = f"{asset}_{subset}" + if namespace: + name = f"{namespace}:{name}" + return name + class AssetLoader(api.Loader): """A basic AssetLoader for Blender diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py new file mode 100644 index 0000000000..01eb524eef --- /dev/null +++ b/pype/plugins/blender/create/create_rig.py @@ -0,0 +1,32 @@ +"""Create a rig asset.""" + +import bpy + +from avalon import api +from avalon.blender import Creator, lib + + +class CreateRig(Creator): + """Artist-friendly rig with controls to direct motion""" + + name = "rigMain" + label = "Rig" + family = "rig" + icon = "cube" + + def process(self): + import pype.blender + + asset = self.data["asset"] + subset = self.data["subset"] + name = pype.blender.plugin.rig_name(asset, subset) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + self.data['task'] = api.Session.get('AVALON_TASK') + lib.imprint(collection, self.data) + + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): + collection.objects.link(obj) + + return collection diff --git a/pype/plugins/blender/publish/collect_rig.py b/pype/plugins/blender/publish/collect_rig.py new file mode 100644 index 0000000000..a4b30541f6 --- /dev/null +++ b/pype/plugins/blender/publish/collect_rig.py @@ -0,0 +1,53 @@ +import typing +from typing import Generator + +import bpy + +import avalon.api +import pyblish.api +from avalon.blender.pipeline import AVALON_PROPERTY + + +class CollectRig(pyblish.api.ContextPlugin): + """Collect the data of a rig.""" + + hosts = ["blender"] + label = "Collect Rig" + order = pyblish.api.CollectorOrder + + @staticmethod + def get_rig_collections() -> Generator: + """Return all 'rig' collections. + + Check if the family is 'rig' and if it doesn't have the + representation set. If the representation is set, it is a loaded rig + and we don't want to publish it. + """ + for collection in bpy.data.collections: + avalon_prop = collection.get(AVALON_PROPERTY) or dict() + if (avalon_prop.get('family') == 'rig' + and not avalon_prop.get('representation')): + yield collection + + def process(self, context): + """Collect the rigs from the current Blender scene.""" + collections = self.get_rig_collections() + for collection in collections: + avalon_prop = collection[AVALON_PROPERTY] + asset = avalon_prop['asset'] + family = avalon_prop['family'] + subset = avalon_prop['subset'] + task = avalon_prop['task'] + name = f"{asset}_{subset}" + instance = context.create_instance( + name=name, + family=family, + families=[family], + subset=subset, + asset=asset, + task=task, + ) + members = list(collection.objects) + members.append(collection) + instance[:] = members + self.log.debug(instance.data) diff --git a/pype/plugins/blender/publish/extract_rig.py b/pype/plugins/blender/publish/extract_rig.py new file mode 100644 index 0000000000..8a3c83d07c --- /dev/null +++ b/pype/plugins/blender/publish/extract_rig.py @@ -0,0 +1,47 @@ +import os +import avalon.blender.workio + +import pype.api + + +class ExtractRig(pype.api.Extractor): + """Extract as rig.""" + + label = "Rig" + hosts = ["blender"] + families = ["rig"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.blend" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction..") + + # Just save the file to a temporary location. At least for now it's no + # problem to have (possibly) extra stuff in the file. + avalon.blender.workio.save_file(filepath, copy=True) + # + # # Store reference for integration + # if "files" not in instance.data: + # instance.data["files"] = list() + # + # # instance.data["files"].append(filename) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'blend', + 'ext': 'blend', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + + self.log.info("Extracted instance '%s' to: %s", instance.name, representation) From fd6bdc9aa5bb48a3ca252e493281d26f45120470 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 4 Feb 2020 10:45:50 +0000 Subject: [PATCH 39/99] Loading and removal of rigs from Blender scenes --- pype/blender/plugin.py | 20 ++ pype/plugins/blender/load/load_rig.py | 337 ++++++++++++++++++++++++++ 2 files changed, 357 insertions(+) create mode 100644 pype/plugins/blender/load/load_rig.py diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index eaa429c989..c85e6df990 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -24,6 +24,26 @@ def rig_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: name = f"{namespace}:{name}" return name +def create_blender_context( obj: Optional[bpy.types.Object] = None ): + """Create a new Blender context. If an object is passed as + parameter, it is set as selected and active. + """ + for win in bpy.context.window_manager.windows: + for area in win.screen.areas: + if area.type == 'VIEW_3D': + for region in area.regions: + if region.type == 'WINDOW': + override_context = { + 'window': win, + 'screen': win.screen, + 'area': area, + 'region': region, + 'scene': bpy.context.scene, + 'active_object': obj, + 'selected_objects': [obj] + } + return override_context + raise Exception( "Could not create a custom Blender context." ) class AssetLoader(api.Loader): """A basic AssetLoader for Blender diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py new file mode 100644 index 0000000000..f3c9e49f53 --- /dev/null +++ b/pype/plugins/blender/load/load_rig.py @@ -0,0 +1,337 @@ +"""Load a rig asset in Blender.""" + +import logging +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +import avalon.blender.pipeline +import bpy +import pype.blender +from avalon import api + +logger = logging.getLogger("pype").getChild("blender").getChild("load_model") + +class BlendRigLoader(pype.blender.AssetLoader): + """Load rigs from a .blend file. + + Because they come from a .blend file we can simply link the collection that + contains the model. There is no further need to 'containerise' it. + + Warning: + Loading the same asset more then once is not properly supported at the + moment. + """ + + families = ["rig"] + representations = ["blend"] + + label = "Link Rig" + icon = "code-fork" + color = "orange" + + @staticmethod + def _get_lib_collection(name: str, libpath: Path) -> Optional[bpy.types.Collection]: + """Find the collection(s) with name, loaded from libpath. + + Note: + It is assumed that only 1 matching collection is found. + """ + for collection in bpy.data.collections: + if collection.name != name: + continue + if collection.library is None: + continue + if not collection.library.filepath: + continue + collection_lib_path = str(Path(bpy.path.abspath(collection.library.filepath)).resolve()) + normalized_libpath = str(Path(bpy.path.abspath(str(libpath))).resolve()) + if collection_lib_path == normalized_libpath: + return collection + return None + + @staticmethod + def _collection_contains_object( + collection: bpy.types.Collection, object: bpy.types.Object + ) -> bool: + """Check if the collection contains the object.""" + for obj in collection.objects: + if obj == object: + return True + return False + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.blender.plugin.rig_name(asset, subset) + container_name = pype.blender.plugin.rig_name( + asset, subset, namespace + ) + relative = bpy.context.preferences.filepaths.use_relative_paths + + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + container = bpy.data.collections[lib_container] + container.name = container_name + avalon.blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + override_context = pype.blender.plugin.create_blender_context() + bpy.ops.object.collection_instance_add( override_context, name = container_name + "_CON" ) + + override_context = pype.blender.plugin.create_blender_context( bpy.data.objects[container_name + "_CON"] ) + bpy.ops.object.make_override_library( override_context ) + bpy.ops.object.delete( override_context ) + + container_metadata = container.get( 'avalon' ) + + object_names_list = [] + + for c in bpy.data.collections: + + if c.name == container_name + "_CON" and c.library is None: + + for obj in c.objects: + + scene.collection.objects.link( obj ) + c.objects.unlink( obj ) + + if not obj.get("avalon"): + obj["avalon"] = dict() + + avalon_info = obj["avalon"] + avalon_info.update( { "container_name": container_name } ) + + object_names_list.append( obj.name ) + + bpy.data.collections.remove( c ) + + container_metadata["objects"] = object_names_list + + bpy.ops.object.select_all( action = 'DESELECT' ) + + nodes = list(container.objects) + nodes.append(container) + self[:] = nodes + return nodes + + def load(self, + context: dict, + name: Optional[str] = None, + namespace: Optional[str] = None, + options: Optional[Dict] = None) -> Optional[bpy.types.Collection]: + """Load asset via database + + Arguments: + context: Full parenthood of representation to load + name: Use pre-defined name + namespace: Use pre-defined namespace + options: Additional settings dictionary + """ + # TODO (jasper): make it possible to add the asset several times by + # just re-using the collection + assert Path(self.fname).exists(), f"{self.fname} doesn't exist." + + self.process_asset( + context=context, + name=name, + namespace=namespace, + options=options, + ) + + # Only containerise if anything was loaded by the Loader. + nodes = self[:] + if not nodes: + return None + + # Only containerise if it's not already a collection from a .blend file. + representation = context["representation"]["name"] + if representation != "blend": + from avalon.blender.pipeline import containerise + return containerise( + name=name, + namespace=namespace, + nodes=nodes, + context=context, + loader=self.__class__.__name__, + ) + + asset = context["asset"]["name"] + subset = context["subset"]["name"] + instance_name = pype.blender.plugin.rig_name(asset, subset, namespace) + + return self._get_instance_collection(instance_name, nodes) + + def update(self, container: Dict, representation: Dict): + """Update the loaded asset. + + This will remove all objects of the current collection, load the new + ones and add them to the collection. + If the objects of the collection are used in another collection they + will not be removed, only unlinked. Normally this should not be the + case though. + + Warning: + No nested collections are supported at the moment! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + logger.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert collection, ( + f"The asset is not loaded: {container['objectName']}" + ) + assert not (collection.children), ( + "Nested collections are not supported." + ) + assert libpath, ( + "No existing library file found for {container['objectName']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in pype.blender.plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + collection_libpath = ( + self._get_library_from_container(collection).filepath + ) + print( collection_libpath ) + normalized_collection_libpath = ( + str(Path(bpy.path.abspath(collection_libpath)).resolve()) + ) + print( normalized_collection_libpath ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + print( normalized_libpath ) + logger.debug( + "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_collection_libpath, + normalized_libpath, + ) + if normalized_collection_libpath == normalized_libpath: + logger.info("Library already loaded, not updating...") + return + # Let Blender's garbage collection take care of removing the library + # itself after removing the objects. + objects_to_remove = set() + collection_objects = list() + collection_objects[:] = collection.objects + for obj in collection_objects: + # Unlink every object + collection.objects.unlink(obj) + remove_obj = True + for coll in [ + coll for coll in bpy.data.collections + if coll != collection + ]: + if ( + coll.objects and + self._collection_contains_object(coll, obj) + ): + remove_obj = False + if remove_obj: + objects_to_remove.add(obj) + + for obj in objects_to_remove: + # Only delete objects that are not used elsewhere + bpy.data.objects.remove(obj) + + instance_empties = [ + obj for obj in collection.users_dupli_group + if obj.name in collection.name + ] + if instance_empties: + instance_empty = instance_empties[0] + container_name = instance_empty["avalon"]["container_name"] + + relative = bpy.context.preferences.filepaths.use_relative_paths + with bpy.data.libraries.load( + str(libpath), link=True, relative=relative + ) as (_, data_to): + data_to.collections = [container_name] + + new_collection = self._get_lib_collection(container_name, libpath) + if new_collection is None: + raise ValueError( + "A matching collection '{container_name}' " + "should have been found in: {libpath}" + ) + + for obj in new_collection.objects: + collection.objects.link(obj) + bpy.data.collections.remove(new_collection) + # Update the representation on the collection + avalon_prop = collection[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_prop["representation"] = str(representation["_id"]) + + def remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (avalon-core:container-1.0): Container to remove, + from `host.ls()`. + + Returns: + bool: Whether the container was deleted. + + Warning: + No nested collections are supported at the moment! + """ + + print( container["objectName"] ) + + collection = bpy.data.collections.get( + container["objectName"] + ) + if not collection: + return False + assert not (collection.children), ( + "Nested collections are not supported." + ) + instance_objects = list(collection.objects) + + data = collection.get( "avalon" ) + object_names = data["objects"] + + for obj in instance_objects: + bpy.data.objects.remove(obj) + + for name in object_names: + bpy.data.objects.remove( bpy.data.objects[name] ) + + bpy.data.collections.remove(collection) + + return True From e88fc5cb4891f0de74822551258687d5e29226ce Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 4 Feb 2020 11:49:38 +0000 Subject: [PATCH 40/99] We now use references to objects in the metadata instead of names --- pype/plugins/blender/load/load_rig.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index f3c9e49f53..70cf6e781a 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -107,7 +107,7 @@ class BlendRigLoader(pype.blender.AssetLoader): container_metadata = container.get( 'avalon' ) - object_names_list = [] + objects_list = [] for c in bpy.data.collections: @@ -124,11 +124,11 @@ class BlendRigLoader(pype.blender.AssetLoader): avalon_info = obj["avalon"] avalon_info.update( { "container_name": container_name } ) - object_names_list.append( obj.name ) + objects_list.append( obj ) bpy.data.collections.remove( c ) - container_metadata["objects"] = object_names_list + container_metadata["objects"] = objects_list bpy.ops.object.select_all( action = 'DESELECT' ) @@ -324,13 +324,13 @@ class BlendRigLoader(pype.blender.AssetLoader): instance_objects = list(collection.objects) data = collection.get( "avalon" ) - object_names = data["objects"] + objects = data["objects"] for obj in instance_objects: bpy.data.objects.remove(obj) - for name in object_names: - bpy.data.objects.remove( bpy.data.objects[name] ) + for obj in objects: + bpy.data.objects.remove( obj ) bpy.data.collections.remove(collection) From c123ed757ced7a0fc7bd9b82ce4824e03561573d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 4 Feb 2020 15:31:44 +0000 Subject: [PATCH 41/99] Improved handling of rigs using avalon container for metadata only --- pype/plugins/blender/load/load_rig.py | 46 +++++++++++---------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 70cf6e781a..b348f99728 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -81,12 +81,7 @@ class BlendRigLoader(pype.blender.AssetLoader): ) relative = bpy.context.preferences.filepaths.use_relative_paths - with bpy.data.libraries.load( - libpath, link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] - - scene = bpy.context.scene + bpy.data.collections.new( lib_container ) container = bpy.data.collections[lib_container] container.name = container_name @@ -98,35 +93,34 @@ class BlendRigLoader(pype.blender.AssetLoader): self.__class__.__name__, ) - override_context = pype.blender.plugin.create_blender_context() - bpy.ops.object.collection_instance_add( override_context, name = container_name + "_CON" ) - - override_context = pype.blender.plugin.create_blender_context( bpy.data.objects[container_name + "_CON"] ) - bpy.ops.object.make_override_library( override_context ) - bpy.ops.object.delete( override_context ) - container_metadata = container.get( 'avalon' ) objects_list = [] - for c in bpy.data.collections: + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (data_from, data_to): - if c.name == container_name + "_CON" and c.library is None: + data_to.collections = [lib_container] - for obj in c.objects: + scene = bpy.context.scene - scene.collection.objects.link( obj ) - c.objects.unlink( obj ) + models = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] + armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] - if not obj.get("avalon"): - obj["avalon"] = dict() + for obj in models + armatures: - avalon_info = obj["avalon"] - avalon_info.update( { "container_name": container_name } ) + scene.collection.objects.link( obj ) - objects_list.append( obj ) + obj = obj.make_local() - bpy.data.collections.remove( c ) + if not obj.get("avalon"): + + obj["avalon"] = dict() + + avalon_info = obj["avalon"] + avalon_info.update( { "container_name": container_name } ) + objects_list.append( obj ) container_metadata["objects"] = objects_list @@ -321,14 +315,10 @@ class BlendRigLoader(pype.blender.AssetLoader): assert not (collection.children), ( "Nested collections are not supported." ) - instance_objects = list(collection.objects) data = collection.get( "avalon" ) objects = data["objects"] - for obj in instance_objects: - bpy.data.objects.remove(obj) - for obj in objects: bpy.data.objects.remove( obj ) From 88b9ceccab25d1785ab73849d83b8ba7c21e8f98 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 4 Feb 2020 15:45:17 +0000 Subject: [PATCH 42/99] More comments for clarity --- pype/plugins/blender/load/load_rig.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index b348f99728..75aa515c66 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -105,10 +105,13 @@ class BlendRigLoader(pype.blender.AssetLoader): scene = bpy.context.scene - models = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] + meshes = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] - for obj in models + armatures: + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in meshes + armatures: scene.collection.objects.link( obj ) @@ -122,6 +125,7 @@ class BlendRigLoader(pype.blender.AssetLoader): avalon_info.update( { "container_name": container_name } ) objects_list.append( obj ) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list bpy.ops.object.select_all( action = 'DESELECT' ) From fc1779387149902705034f9d2101e7cf5a1acf72 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 5 Feb 2020 15:20:15 +0000 Subject: [PATCH 43/99] Implemented update for rigs --- pype/plugins/blender/load/load_rig.py | 124 +++++++++++++------------- 1 file changed, 64 insertions(+), 60 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 75aa515c66..294366c41b 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -93,14 +93,14 @@ class BlendRigLoader(pype.blender.AssetLoader): self.__class__.__name__, ) - container_metadata = container.get( 'avalon' ) + container_metadata = container.get( avalon.blender.pipeline.AVALON_PROPERTY ) - objects_list = [] + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container with bpy.data.libraries.load( libpath, link=True, relative=relative ) as (data_from, data_to): - data_to.collections = [lib_container] scene = bpy.context.scene @@ -108,6 +108,8 @@ class BlendRigLoader(pype.blender.AssetLoader): meshes = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] + objects_list = [] + # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. @@ -117,17 +119,19 @@ class BlendRigLoader(pype.blender.AssetLoader): obj = obj.make_local() - if not obj.get("avalon"): + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): - obj["avalon"] = dict() + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj["avalon"] + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] avalon_info.update( { "container_name": container_name } ) objects_list.append( obj ) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list + bpy.data.collections.remove( bpy.data.collections[lib_container] ) + bpy.ops.object.select_all( action = 'DESELECT' ) nodes = list(container.objects) @@ -198,6 +202,7 @@ class BlendRigLoader(pype.blender.AssetLoader): collection = bpy.data.collections.get( container["objectName"] ) + libpath = Path(api.get_representation_path(representation)) extension = libpath.suffix.lower() @@ -222,18 +227,14 @@ class BlendRigLoader(pype.blender.AssetLoader): assert extension in pype.blender.plugin.VALID_EXTENSIONS, ( f"Unsupported file: {libpath}" ) - collection_libpath = ( - self._get_library_from_container(collection).filepath - ) - print( collection_libpath ) + + collection_libpath = container["libpath"] normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) - print( normalized_collection_libpath ) normalized_libpath = ( str(Path(bpy.path.abspath(str(libpath))).resolve()) ) - print( normalized_libpath ) logger.debug( "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", normalized_collection_libpath, @@ -242,58 +243,63 @@ class BlendRigLoader(pype.blender.AssetLoader): if normalized_collection_libpath == normalized_libpath: logger.info("Library already loaded, not updating...") return - # Let Blender's garbage collection take care of removing the library - # itself after removing the objects. - objects_to_remove = set() - collection_objects = list() - collection_objects[:] = collection.objects - for obj in collection_objects: - # Unlink every object - collection.objects.unlink(obj) - remove_obj = True - for coll in [ - coll for coll in bpy.data.collections - if coll != collection - ]: - if ( - coll.objects and - self._collection_contains_object(coll, obj) - ): - remove_obj = False - if remove_obj: - objects_to_remove.add(obj) - for obj in objects_to_remove: - # Only delete objects that are not used elsewhere - bpy.data.objects.remove(obj) + # Get the armature of the rig + armatures = [ obj for obj in container["objects"] if obj.type == 'ARMATURE' ] + assert( len( armatures ) == 1 ) - instance_empties = [ - obj for obj in collection.users_dupli_group - if obj.name in collection.name - ] - if instance_empties: - instance_empty = instance_empties[0] - container_name = instance_empty["avalon"]["container_name"] + action = armatures[0].animation_data.action + + for obj in container["objects"]: + bpy.data.objects.remove( obj ) + + lib_container = container["lib_container"] relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( str(libpath), link=True, relative=relative ) as (_, data_to): - data_to.collections = [container_name] + data_to.collections = [lib_container] - new_collection = self._get_lib_collection(container_name, libpath) - if new_collection is None: - raise ValueError( - "A matching collection '{container_name}' " - "should have been found in: {libpath}" - ) + scene = bpy.context.scene - for obj in new_collection.objects: - collection.objects.link(obj) - bpy.data.collections.remove(new_collection) - # Update the representation on the collection - avalon_prop = collection[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_prop["representation"] = str(representation["_id"]) + meshes = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] + armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] + objects_list = [] + + assert( len( armatures ) == 1 ) + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in meshes + armatures: + + scene.collection.objects.link( obj ) + + obj = obj.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update( { "container_name": collection.name } ) + objects_list.append( obj ) + + if obj.type == 'ARMATURE' and action is not None: + + obj.animation_data.action = action + + collection_metadata = collection.get(avalon.blender.pipeline.AVALON_PROPERTY) + + # Save the list of objects in the metadata container + collection_metadata["objects"] = objects_list + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + bpy.data.collections.remove( bpy.data.collections[lib_container] ) + + bpy.ops.object.select_all( action = 'DESELECT' ) def remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. @@ -309,8 +315,6 @@ class BlendRigLoader(pype.blender.AssetLoader): No nested collections are supported at the moment! """ - print( container["objectName"] ) - collection = bpy.data.collections.get( container["objectName"] ) @@ -320,12 +324,12 @@ class BlendRigLoader(pype.blender.AssetLoader): "Nested collections are not supported." ) - data = collection.get( "avalon" ) - objects = data["objects"] + collection_metadata = collection.get( avalon.blender.pipeline.AVALON_PROPERTY ) + objects = collection_metadata["objects"] for obj in objects: bpy.data.objects.remove( obj ) - bpy.data.collections.remove(collection) + bpy.data.collections.remove( collection ) return True From 4bd4c6e811f52a4cfc74551d53b3a9fde2e857a3 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 5 Feb 2020 16:49:27 +0000 Subject: [PATCH 44/99] The data in the objects is made local as well. Not having this would cause problems with the keyframing of shape keys and custom data. --- pype/plugins/blender/load/load_rig.py | 28 +++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 294366c41b..fa0e1c52b2 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -119,6 +119,8 @@ class BlendRigLoader(pype.blender.AssetLoader): obj = obj.make_local() + obj.data.make_local() + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() @@ -228,7 +230,9 @@ class BlendRigLoader(pype.blender.AssetLoader): f"Unsupported file: {libpath}" ) - collection_libpath = container["libpath"] + collection_metadata = collection.get(avalon.blender.pipeline.AVALON_PROPERTY) + + collection_libpath = collection_metadata["libpath"] normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) @@ -245,15 +249,19 @@ class BlendRigLoader(pype.blender.AssetLoader): return # Get the armature of the rig - armatures = [ obj for obj in container["objects"] if obj.type == 'ARMATURE' ] + armatures = [ obj for obj in collection_metadata["objects"] if obj.type == 'ARMATURE' ] assert( len( armatures ) == 1 ) action = armatures[0].animation_data.action - for obj in container["objects"]: - bpy.data.objects.remove( obj ) + for obj in collection_metadata["objects"]: - lib_container = container["lib_container"] + if obj.type == 'ARMATURE': + bpy.data.armatures.remove( obj.data ) + elif obj.type == 'MESH': + bpy.data.meshes.remove( obj.data ) + + lib_container = collection_metadata["lib_container"] relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( @@ -278,6 +286,8 @@ class BlendRigLoader(pype.blender.AssetLoader): obj = obj.make_local() + obj.data.make_local() + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() @@ -290,8 +300,6 @@ class BlendRigLoader(pype.blender.AssetLoader): obj.animation_data.action = action - collection_metadata = collection.get(avalon.blender.pipeline.AVALON_PROPERTY) - # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list collection_metadata["libpath"] = str(libpath) @@ -328,7 +336,11 @@ class BlendRigLoader(pype.blender.AssetLoader): objects = collection_metadata["objects"] for obj in objects: - bpy.data.objects.remove( obj ) + + if obj.type == 'ARMATURE': + bpy.data.armatures.remove( obj.data ) + elif obj.type == 'MESH': + bpy.data.meshes.remove( obj.data ) bpy.data.collections.remove( collection ) From 6f6482a58b4aa328af07ccaae9c2e66ccf7ecc7f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 7 Feb 2020 11:39:14 +0000 Subject: [PATCH 45/99] Rig is linked in a collection, instead of the generic scene collection --- pype/plugins/blender/load/load_rig.py | 79 ++++++++++++++++----------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index fa0e1c52b2..7ea131a54c 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -12,6 +12,7 @@ from avalon import api logger = logging.getLogger("pype").getChild("blender").getChild("load_model") + class BlendRigLoader(pype.blender.AssetLoader): """Load rigs from a .blend file. @@ -44,8 +45,10 @@ class BlendRigLoader(pype.blender.AssetLoader): continue if not collection.library.filepath: continue - collection_lib_path = str(Path(bpy.path.abspath(collection.library.filepath)).resolve()) - normalized_libpath = str(Path(bpy.path.abspath(str(libpath))).resolve()) + collection_lib_path = str( + Path(bpy.path.abspath(collection.library.filepath)).resolve()) + normalized_libpath = str( + Path(bpy.path.abspath(str(libpath))).resolve()) if collection_lib_path == normalized_libpath: return collection return None @@ -81,7 +84,7 @@ class BlendRigLoader(pype.blender.AssetLoader): ) relative = bpy.context.preferences.filepaths.use_relative_paths - bpy.data.collections.new( lib_container ) + bpy.data.collections.new(lib_container) container = bpy.data.collections[lib_container] container.name = container_name @@ -93,7 +96,8 @@ class BlendRigLoader(pype.blender.AssetLoader): self.__class__.__name__, ) - container_metadata = container.get( avalon.blender.pipeline.AVALON_PROPERTY ) + container_metadata = container.get( + avalon.blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container @@ -105,8 +109,13 @@ class BlendRigLoader(pype.blender.AssetLoader): scene = bpy.context.scene - meshes = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] - armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] + scene.collection.children.link(bpy.data.collections[lib_container]) + + rig_container = scene.collection.children[lib_container].make_local() + + meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] + armatures = [ + obj for obj in rig_container.objects if obj.type == 'ARMATURE'] objects_list = [] @@ -115,8 +124,6 @@ class BlendRigLoader(pype.blender.AssetLoader): # when it is made local. for obj in meshes + armatures: - scene.collection.objects.link( obj ) - obj = obj.make_local() obj.data.make_local() @@ -126,15 +133,13 @@ class BlendRigLoader(pype.blender.AssetLoader): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update( { "container_name": container_name } ) - objects_list.append( obj ) + avalon_info.update({"container_name": container_name}) + objects_list.append(obj) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list - bpy.data.collections.remove( bpy.data.collections[lib_container] ) - - bpy.ops.object.select_all( action = 'DESELECT' ) + bpy.ops.object.select_all(action='DESELECT') nodes = list(container.objects) nodes.append(container) @@ -230,7 +235,8 @@ class BlendRigLoader(pype.blender.AssetLoader): f"Unsupported file: {libpath}" ) - collection_metadata = collection.get(avalon.blender.pipeline.AVALON_PROPERTY) + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] normalized_collection_libpath = ( @@ -249,20 +255,23 @@ class BlendRigLoader(pype.blender.AssetLoader): return # Get the armature of the rig - armatures = [ obj for obj in collection_metadata["objects"] if obj.type == 'ARMATURE' ] - assert( len( armatures ) == 1 ) + armatures = [obj for obj in collection_metadata["objects"] + if obj.type == 'ARMATURE'] + assert(len(armatures) == 1) action = armatures[0].animation_data.action for obj in collection_metadata["objects"]: if obj.type == 'ARMATURE': - bpy.data.armatures.remove( obj.data ) + bpy.data.armatures.remove(obj.data) elif obj.type == 'MESH': - bpy.data.meshes.remove( obj.data ) + bpy.data.meshes.remove(obj.data) lib_container = collection_metadata["lib_container"] + bpy.data.collections.remove(bpy.data.collections[lib_container]) + relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( str(libpath), link=True, relative=relative @@ -271,19 +280,22 @@ class BlendRigLoader(pype.blender.AssetLoader): scene = bpy.context.scene - meshes = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] - armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] + scene.collection.children.link(bpy.data.collections[lib_container]) + + rig_container = scene.collection.children[lib_container].make_local() + + meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] + armatures = [ + obj for obj in rig_container.objects if obj.type == 'ARMATURE'] objects_list = [] - assert( len( armatures ) == 1 ) + assert(len(armatures) == 1) # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. for obj in meshes + armatures: - scene.collection.objects.link( obj ) - obj = obj.make_local() obj.data.make_local() @@ -293,8 +305,8 @@ class BlendRigLoader(pype.blender.AssetLoader): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update( { "container_name": collection.name } ) - objects_list.append( obj ) + avalon_info.update({"container_name": collection.name}) + objects_list.append(obj) if obj.type == 'ARMATURE' and action is not None: @@ -305,9 +317,7 @@ class BlendRigLoader(pype.blender.AssetLoader): collection_metadata["libpath"] = str(libpath) collection_metadata["representation"] = str(representation["_id"]) - bpy.data.collections.remove( bpy.data.collections[lib_container] ) - - bpy.ops.object.select_all( action = 'DESELECT' ) + bpy.ops.object.select_all(action='DESELECT') def remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. @@ -332,16 +342,19 @@ class BlendRigLoader(pype.blender.AssetLoader): "Nested collections are not supported." ) - collection_metadata = collection.get( avalon.blender.pipeline.AVALON_PROPERTY ) + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] for obj in objects: if obj.type == 'ARMATURE': - bpy.data.armatures.remove( obj.data ) + bpy.data.armatures.remove(obj.data) elif obj.type == 'MESH': - bpy.data.meshes.remove( obj.data ) - - bpy.data.collections.remove( collection ) + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + bpy.data.collections.remove(collection) return True From 65104882db7a44ba3d5e213aae9f7cee0c572c93 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 7 Feb 2020 15:09:21 +0000 Subject: [PATCH 46/99] Changed handling of models for consistency --- pype/plugins/blender/load/load_model.py | 196 ++++++++++++------------ pype/plugins/blender/load/load_rig.py | 39 +---- 2 files changed, 98 insertions(+), 137 deletions(-) diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index bd6db17650..bb9f2250be 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -12,7 +12,6 @@ from avalon import api logger = logging.getLogger("pype").getChild("blender").getChild("load_model") - class BlendModelLoader(pype.blender.AssetLoader): """Load models from a .blend file. @@ -31,36 +30,6 @@ class BlendModelLoader(pype.blender.AssetLoader): icon = "code-fork" color = "orange" - @staticmethod - def _get_lib_collection(name: str, libpath: Path) -> Optional[bpy.types.Collection]: - """Find the collection(s) with name, loaded from libpath. - - Note: - It is assumed that only 1 matching collection is found. - """ - for collection in bpy.data.collections: - if collection.name != name: - continue - if collection.library is None: - continue - if not collection.library.filepath: - continue - collection_lib_path = str(Path(bpy.path.abspath(collection.library.filepath)).resolve()) - normalized_libpath = str(Path(bpy.path.abspath(str(libpath))).resolve()) - if collection_lib_path == normalized_libpath: - return collection - return None - - @staticmethod - def _collection_contains_object( - collection: bpy.types.Collection, object: bpy.types.Object - ) -> bool: - """Check if the collection contains the object.""" - for obj in collection.objects: - if obj == object: - return True - return False - def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, options: Optional[Dict] = None @@ -82,25 +51,8 @@ class BlendModelLoader(pype.blender.AssetLoader): ) relative = bpy.context.preferences.filepaths.use_relative_paths - with bpy.data.libraries.load( - libpath, link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] - - scene = bpy.context.scene - instance_empty = bpy.data.objects.new( - container_name, None - ) - if not instance_empty.get("avalon"): - instance_empty["avalon"] = dict() - avalon_info = instance_empty["avalon"] - avalon_info.update({"container_name": container_name}) - scene.collection.objects.link(instance_empty) - instance_empty.instance_type = 'COLLECTION' - container = bpy.data.collections[lib_container] + container = bpy.data.collections.new(lib_container) container.name = container_name - instance_empty.instance_collection = container - container.make_local() avalon.blender.pipeline.containerise_existing( container, name, @@ -109,9 +61,47 @@ class BlendModelLoader(pype.blender.AssetLoader): self.__class__.__name__, ) + container_metadata = container.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + rig_container = scene.collection.children[lib_container].make_local() + + objects_list = [] + + for obj in rig_container.objects: + + obj = obj.make_local() + + obj.data.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + objects_list.append(obj) + + # Save the list of objects in the metadata container + container_metadata["objects"] = objects_list + + bpy.ops.object.select_all(action='DESELECT') + nodes = list(container.objects) nodes.append(container) - nodes.append(instance_empty) self[:] = nodes return nodes @@ -154,9 +144,11 @@ class BlendModelLoader(pype.blender.AssetLoader): assert extension in pype.blender.plugin.VALID_EXTENSIONS, ( f"Unsupported file: {libpath}" ) - collection_libpath = ( - self._get_library_from_container(collection).filepath - ) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + collection_libpath = collection_metadata["libpath"] normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) @@ -171,58 +163,52 @@ class BlendModelLoader(pype.blender.AssetLoader): if normalized_collection_libpath == normalized_libpath: logger.info("Library already loaded, not updating...") return - # Let Blender's garbage collection take care of removing the library - # itself after removing the objects. - objects_to_remove = set() - collection_objects = list() - collection_objects[:] = collection.objects - for obj in collection_objects: - # Unlink every object - collection.objects.unlink(obj) - remove_obj = True - for coll in [ - coll for coll in bpy.data.collections - if coll != collection - ]: - if ( - coll.objects and - self._collection_contains_object(coll, obj) - ): - remove_obj = False - if remove_obj: - objects_to_remove.add(obj) - for obj in objects_to_remove: - # Only delete objects that are not used elsewhere - bpy.data.objects.remove(obj) + for obj in collection_metadata["objects"]: - instance_empties = [ - obj for obj in collection.users_dupli_group - if obj.name in collection.name - ] - if instance_empties: - instance_empty = instance_empties[0] - container_name = instance_empty["avalon"]["container_name"] + bpy.data.meshes.remove(obj.data) + + lib_container = collection_metadata["lib_container"] + + bpy.data.collections.remove(bpy.data.collections[lib_container]) relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( str(libpath), link=True, relative=relative ) as (_, data_to): - data_to.collections = [container_name] + data_to.collections = [lib_container] - new_collection = self._get_lib_collection(container_name, libpath) - if new_collection is None: - raise ValueError( - "A matching collection '{container_name}' " - "should have been found in: {libpath}" - ) + scene = bpy.context.scene - for obj in new_collection.objects: - collection.objects.link(obj) - bpy.data.collections.remove(new_collection) - # Update the representation on the collection - avalon_prop = collection[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_prop["representation"] = str(representation["_id"]) + scene.collection.children.link(bpy.data.collections[lib_container]) + + rig_container = scene.collection.children[lib_container].make_local() + + objects_list = [] + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in rig_container.objects: + + obj = obj.make_local() + + obj.data.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": collection.name}) + objects_list.append(obj) + + # Save the list of objects in the metadata container + collection_metadata["objects"] = objects_list + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + bpy.ops.object.select_all(action='DESELECT') def remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. @@ -245,10 +231,17 @@ class BlendModelLoader(pype.blender.AssetLoader): assert not (collection.children), ( "Nested collections are not supported." ) - instance_parents = list(collection.users_dupli_group) - instance_objects = list(collection.objects) - for obj in instance_objects + instance_parents: - bpy.data.objects.remove(obj) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + + for obj in objects: + + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) bpy.data.collections.remove(collection) return True @@ -281,7 +274,8 @@ class CacheModelLoader(pype.blender.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - raise NotImplementedError("Loading of Alembic files is not yet implemented.") + raise NotImplementedError( + "Loading of Alembic files is not yet implemented.") # TODO (jasper): implement Alembic import. libpath = self.fname diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 7ea131a54c..8593440624 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -31,38 +31,6 @@ class BlendRigLoader(pype.blender.AssetLoader): icon = "code-fork" color = "orange" - @staticmethod - def _get_lib_collection(name: str, libpath: Path) -> Optional[bpy.types.Collection]: - """Find the collection(s) with name, loaded from libpath. - - Note: - It is assumed that only 1 matching collection is found. - """ - for collection in bpy.data.collections: - if collection.name != name: - continue - if collection.library is None: - continue - if not collection.library.filepath: - continue - collection_lib_path = str( - Path(bpy.path.abspath(collection.library.filepath)).resolve()) - normalized_libpath = str( - Path(bpy.path.abspath(str(libpath))).resolve()) - if collection_lib_path == normalized_libpath: - return collection - return None - - @staticmethod - def _collection_contains_object( - collection: bpy.types.Collection, object: bpy.types.Object - ) -> bool: - """Check if the collection contains the object.""" - for obj in collection.objects: - if obj == object: - return True - return False - def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, options: Optional[Dict] = None @@ -84,9 +52,7 @@ class BlendRigLoader(pype.blender.AssetLoader): ) relative = bpy.context.preferences.filepaths.use_relative_paths - bpy.data.collections.new(lib_container) - - container = bpy.data.collections[lib_container] + container = bpy.data.collections.new(lib_container) container.name = container_name avalon.blender.pipeline.containerise_existing( container, @@ -104,7 +70,7 @@ class BlendRigLoader(pype.blender.AssetLoader): with bpy.data.libraries.load( libpath, link=True, relative=relative - ) as (data_from, data_to): + ) as (_, data_to): data_to.collections = [lib_container] scene = bpy.context.scene @@ -134,6 +100,7 @@ class BlendRigLoader(pype.blender.AssetLoader): avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) + objects_list.append(obj) # Save the list of objects in the metadata container From 951dcfca3e7f9c02e8d878fea4d693f950b7fada Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 10 Feb 2020 14:45:50 +0000 Subject: [PATCH 47/99] Code optimization --- pype/blender/plugin.py | 13 ++---- pype/plugins/blender/create/create_model.py | 2 +- pype/plugins/blender/create/create_rig.py | 4 +- pype/plugins/blender/load/load_model.py | 15 +++--- pype/plugins/blender/load/load_rig.py | 51 +-------------------- 5 files changed, 16 insertions(+), 69 deletions(-) diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index c85e6df990..b441714c0d 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -10,15 +10,8 @@ from avalon import api VALID_EXTENSIONS = [".blend"] -def model_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: - """Return a consistent name for a model asset.""" - name = f"{asset}_{subset}" - if namespace: - name = f"{namespace}:{name}" - return name - -def rig_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: - """Return a consistent name for a rig asset.""" +def asset_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: + """Return a consistent name for an asset.""" name = f"{asset}_{subset}" if namespace: name = f"{namespace}:{name}" @@ -149,7 +142,7 @@ class AssetLoader(api.Loader): asset = context["asset"]["name"] subset = context["subset"]["name"] - instance_name = model_name(asset, subset, namespace) + instance_name = asset_name(asset, subset, namespace) return self._get_instance_collection(instance_name, nodes) diff --git a/pype/plugins/blender/create/create_model.py b/pype/plugins/blender/create/create_model.py index 7301073f05..a3b2ffc55b 100644 --- a/pype/plugins/blender/create/create_model.py +++ b/pype/plugins/blender/create/create_model.py @@ -19,7 +19,7 @@ class CreateModel(Creator): asset = self.data["asset"] subset = self.data["subset"] - name = pype.blender.plugin.model_name(asset, subset) + name = pype.blender.plugin.asset_name(asset, subset) collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) self.data['task'] = api.Session.get('AVALON_TASK') diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index 01eb524eef..5d83fafdd3 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -12,14 +12,14 @@ class CreateRig(Creator): name = "rigMain" label = "Rig" family = "rig" - icon = "cube" + icon = "wheelchair" def process(self): import pype.blender asset = self.data["asset"] subset = self.data["subset"] - name = pype.blender.plugin.rig_name(asset, subset) + name = pype.blender.plugin.asset_name(asset, subset) collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) self.data['task'] = api.Session.get('AVALON_TASK') diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index bb9f2250be..cde4109a7c 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -12,6 +12,7 @@ from avalon import api logger = logging.getLogger("pype").getChild("blender").getChild("load_model") + class BlendModelLoader(pype.blender.AssetLoader): """Load models from a .blend file. @@ -45,8 +46,8 @@ class BlendModelLoader(pype.blender.AssetLoader): libpath = self.fname asset = context["asset"]["name"] subset = context["subset"]["name"] - lib_container = pype.blender.plugin.model_name(asset, subset) - container_name = pype.blender.plugin.model_name( + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( asset, subset, namespace ) relative = bpy.context.preferences.filepaths.use_relative_paths @@ -76,11 +77,11 @@ class BlendModelLoader(pype.blender.AssetLoader): scene.collection.children.link(bpy.data.collections[lib_container]) - rig_container = scene.collection.children[lib_container].make_local() + model_container = scene.collection.children[lib_container].make_local() objects_list = [] - for obj in rig_container.objects: + for obj in model_container.objects: obj = obj.make_local() @@ -182,14 +183,14 @@ class BlendModelLoader(pype.blender.AssetLoader): scene.collection.children.link(bpy.data.collections[lib_container]) - rig_container = scene.collection.children[lib_container].make_local() + model_container = scene.collection.children[lib_container].make_local() objects_list = [] # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. - for obj in rig_container.objects: + for obj in model_container.objects: obj = obj.make_local() @@ -283,7 +284,7 @@ class CacheModelLoader(pype.blender.AssetLoader): subset = context["subset"]["name"] # TODO (jasper): evaluate use of namespace which is 'alien' to Blender. lib_container = container_name = ( - pype.blender.plugin.model_name(asset, subset, namespace) + pype.blender.plugin.asset_name(asset, subset, namespace) ) relative = bpy.context.preferences.filepaths.use_relative_paths diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 8593440624..361850c51b 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -46,8 +46,8 @@ class BlendRigLoader(pype.blender.AssetLoader): libpath = self.fname asset = context["asset"]["name"] subset = context["subset"]["name"] - lib_container = pype.blender.plugin.rig_name(asset, subset) - container_name = pype.blender.plugin.rig_name( + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( asset, subset, namespace ) relative = bpy.context.preferences.filepaths.use_relative_paths @@ -113,53 +113,6 @@ class BlendRigLoader(pype.blender.AssetLoader): self[:] = nodes return nodes - def load(self, - context: dict, - name: Optional[str] = None, - namespace: Optional[str] = None, - options: Optional[Dict] = None) -> Optional[bpy.types.Collection]: - """Load asset via database - - Arguments: - context: Full parenthood of representation to load - name: Use pre-defined name - namespace: Use pre-defined namespace - options: Additional settings dictionary - """ - # TODO (jasper): make it possible to add the asset several times by - # just re-using the collection - assert Path(self.fname).exists(), f"{self.fname} doesn't exist." - - self.process_asset( - context=context, - name=name, - namespace=namespace, - options=options, - ) - - # Only containerise if anything was loaded by the Loader. - nodes = self[:] - if not nodes: - return None - - # Only containerise if it's not already a collection from a .blend file. - representation = context["representation"]["name"] - if representation != "blend": - from avalon.blender.pipeline import containerise - return containerise( - name=name, - namespace=namespace, - nodes=nodes, - context=context, - loader=self.__class__.__name__, - ) - - asset = context["asset"]["name"] - subset = context["subset"]["name"] - instance_name = pype.blender.plugin.rig_name(asset, subset, namespace) - - return self._get_instance_collection(instance_name, nodes) - def update(self, container: Dict, representation: Dict): """Update the loaded asset. From 2b6e90ffe00a44a7f9e972389a50c3c8b20fb972 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 10 Feb 2020 14:55:04 +0000 Subject: [PATCH 48/99] Creation, loading, and management of animations --- .../blender/create/create_animation.py | 30 ++ pype/plugins/blender/load/load_animation.py | 274 ++++++++++++++++++ .../blender/publish/collect_animation.py | 53 ++++ .../blender/publish/extract_animation.py | 47 +++ 4 files changed, 404 insertions(+) create mode 100644 pype/plugins/blender/create/create_animation.py create mode 100644 pype/plugins/blender/load/load_animation.py create mode 100644 pype/plugins/blender/publish/collect_animation.py create mode 100644 pype/plugins/blender/publish/extract_animation.py diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py new file mode 100644 index 0000000000..cfe569f918 --- /dev/null +++ b/pype/plugins/blender/create/create_animation.py @@ -0,0 +1,30 @@ +import bpy + +from avalon import api +from avalon.blender import Creator, lib + + +class CreateAnimation(Creator): + """Animation output for character rigs""" + + name = "animationMain" + label = "Animation" + family = "animation" + icon = "male" + + def process(self): + import pype.blender + + asset = self.data["asset"] + subset = self.data["subset"] + name = pype.blender.plugin.asset_name(asset, subset) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + self.data['task'] = api.Session.get('AVALON_TASK') + lib.imprint(collection, self.data) + + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): + collection.objects.link(obj) + + return collection diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py new file mode 100644 index 0000000000..5b527e1717 --- /dev/null +++ b/pype/plugins/blender/load/load_animation.py @@ -0,0 +1,274 @@ +"""Load an animation in Blender.""" + +import logging +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +import avalon.blender.pipeline +import bpy +import pype.blender +from avalon import api + +logger = logging.getLogger("pype").getChild("blender").getChild("load_model") + + +class BlendAnimationLoader(pype.blender.AssetLoader): + """Load animations from a .blend file. + + Because they come from a .blend file we can simply link the collection that + contains the model. There is no further need to 'containerise' it. + + Warning: + Loading the same asset more then once is not properly supported at the + moment. + """ + + families = ["animation"] + representations = ["blend"] + + label = "Link Animation" + icon = "code-fork" + color = "orange" + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( + asset, subset, namespace + ) + relative = bpy.context.preferences.filepaths.use_relative_paths + + container = bpy.data.collections.new(lib_container) + container.name = container_name + avalon.blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = container.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + animation_container = scene.collection.children[lib_container].make_local() + + meshes = [obj for obj in animation_container.objects if obj.type == 'MESH'] + armatures = [ + obj for obj in animation_container.objects if obj.type == 'ARMATURE'] + + objects_list = [] + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in meshes + armatures: + + obj = obj.make_local() + + obj.data.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + objects_list.append(obj) + + # Save the list of objects in the metadata container + container_metadata["objects"] = objects_list + + bpy.ops.object.select_all(action='DESELECT') + + nodes = list(container.objects) + nodes.append(container) + self[:] = nodes + return nodes + + def update(self, container: Dict, representation: Dict): + """Update the loaded asset. + + This will remove all objects of the current collection, load the new + ones and add them to the collection. + If the objects of the collection are used in another collection they + will not be removed, only unlinked. Normally this should not be the + case though. + + Warning: + No nested collections are supported at the moment! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + logger.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert collection, ( + f"The asset is not loaded: {container['objectName']}" + ) + assert not (collection.children), ( + "Nested collections are not supported." + ) + assert libpath, ( + "No existing library file found for {container['objectName']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in pype.blender.plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + collection_libpath = collection_metadata["libpath"] + normalized_collection_libpath = ( + str(Path(bpy.path.abspath(collection_libpath)).resolve()) + ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + logger.debug( + "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_collection_libpath, + normalized_libpath, + ) + if normalized_collection_libpath == normalized_libpath: + logger.info("Library already loaded, not updating...") + return + + # Get the armature of the rig + armatures = [obj for obj in collection_metadata["objects"] + if obj.type == 'ARMATURE'] + assert(len(armatures) == 1) + + for obj in collection_metadata["objects"]: + + if obj.type == 'ARMATURE': + bpy.data.armatures.remove(obj.data) + elif obj.type == 'MESH': + bpy.data.meshes.remove(obj.data) + + lib_container = collection_metadata["lib_container"] + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + relative = bpy.context.preferences.filepaths.use_relative_paths + with bpy.data.libraries.load( + str(libpath), link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + animation_container = scene.collection.children[lib_container].make_local() + + meshes = [obj for obj in animation_container.objects if obj.type == 'MESH'] + armatures = [ + obj for obj in animation_container.objects if obj.type == 'ARMATURE'] + objects_list = [] + + assert(len(armatures) == 1) + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in meshes + armatures: + + obj = obj.make_local() + + obj.data.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": collection.name}) + objects_list.append(obj) + + # Save the list of objects in the metadata container + collection_metadata["objects"] = objects_list + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + bpy.ops.object.select_all(action='DESELECT') + + def remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (avalon-core:container-1.0): Container to remove, + from `host.ls()`. + + Returns: + bool: Whether the container was deleted. + + Warning: + No nested collections are supported at the moment! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + if not collection: + return False + assert not (collection.children), ( + "Nested collections are not supported." + ) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + + for obj in objects: + + if obj.type == 'ARMATURE': + bpy.data.armatures.remove(obj.data) + elif obj.type == 'MESH': + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + bpy.data.collections.remove(collection) + + return True diff --git a/pype/plugins/blender/publish/collect_animation.py b/pype/plugins/blender/publish/collect_animation.py new file mode 100644 index 0000000000..9bc0b02227 --- /dev/null +++ b/pype/plugins/blender/publish/collect_animation.py @@ -0,0 +1,53 @@ +import typing +from typing import Generator + +import bpy + +import avalon.api +import pyblish.api +from avalon.blender.pipeline import AVALON_PROPERTY + + +class CollectAnimation(pyblish.api.ContextPlugin): + """Collect the data of an animation.""" + + hosts = ["blender"] + label = "Collect Animation" + order = pyblish.api.CollectorOrder + + @staticmethod + def get_animation_collections() -> Generator: + """Return all 'animation' collections. + + Check if the family is 'animation' and if it doesn't have the + representation set. If the representation is set, it is a loaded rig + and we don't want to publish it. + """ + for collection in bpy.data.collections: + avalon_prop = collection.get(AVALON_PROPERTY) or dict() + if (avalon_prop.get('family') == 'animation' + and not avalon_prop.get('representation')): + yield collection + + def process(self, context): + """Collect the animations from the current Blender scene.""" + collections = self.get_animation_collections() + for collection in collections: + avalon_prop = collection[AVALON_PROPERTY] + asset = avalon_prop['asset'] + family = avalon_prop['family'] + subset = avalon_prop['subset'] + task = avalon_prop['task'] + name = f"{asset}_{subset}" + instance = context.create_instance( + name=name, + family=family, + families=[family], + subset=subset, + asset=asset, + task=task, + ) + members = list(collection.objects) + members.append(collection) + instance[:] = members + self.log.debug(instance.data) diff --git a/pype/plugins/blender/publish/extract_animation.py b/pype/plugins/blender/publish/extract_animation.py new file mode 100644 index 0000000000..dbfe29af83 --- /dev/null +++ b/pype/plugins/blender/publish/extract_animation.py @@ -0,0 +1,47 @@ +import os +import avalon.blender.workio + +import pype.api + + +class ExtractAnimation(pype.api.Extractor): + """Extract as animation.""" + + label = "Animation" + hosts = ["blender"] + families = ["animation"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.blend" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction..") + + # Just save the file to a temporary location. At least for now it's no + # problem to have (possibly) extra stuff in the file. + avalon.blender.workio.save_file(filepath, copy=True) + # + # # Store reference for integration + # if "files" not in instance.data: + # instance.data["files"] = list() + # + # # instance.data["files"].append(filename) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'blend', + 'ext': 'blend', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + + self.log.info("Extracted instance '%s' to: %s", instance.name, representation) From 0a561382f2dcb716fc2c311c7b94ee712383ff26 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 12 Feb 2020 12:53:48 +0000 Subject: [PATCH 49/99] Fixed a problem where loaded assets were collected for publishing --- pype/plugins/blender/load/load_animation.py | 4 ++++ pype/plugins/blender/load/load_model.py | 4 ++++ pype/plugins/blender/load/load_rig.py | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index 5b527e1717..58a0e94665 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -103,6 +103,8 @@ class BlendAnimationLoader(pype.blender.AssetLoader): objects_list.append(obj) + animation_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -226,6 +228,8 @@ class BlendAnimationLoader(pype.blender.AssetLoader): avalon_info.update({"container_name": collection.name}) objects_list.append(obj) + animation_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list collection_metadata["libpath"] = str(libpath) diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index cde4109a7c..40d6c3434c 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -96,6 +96,8 @@ class BlendModelLoader(pype.blender.AssetLoader): objects_list.append(obj) + model_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -204,6 +206,8 @@ class BlendModelLoader(pype.blender.AssetLoader): avalon_info.update({"container_name": collection.name}) objects_list.append(obj) + model_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list collection_metadata["libpath"] = str(libpath) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 361850c51b..c19717cd82 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -103,6 +103,8 @@ class BlendRigLoader(pype.blender.AssetLoader): objects_list.append(obj) + rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -232,6 +234,8 @@ class BlendRigLoader(pype.blender.AssetLoader): obj.animation_data.action = action + rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list collection_metadata["libpath"] = str(libpath) From 09c9f66e4c79c1d7ed4b5185c912647be8e0825e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 19 Feb 2020 19:44:05 +0100 Subject: [PATCH 50/99] basic hook system --- pype/ftrack/lib/ftrack_app_handler.py | 5 +++++ pype/lib.py | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 9dc735987d..ea769ad167 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -273,6 +273,11 @@ class AppAction(BaseHandler): # Full path to executable launcher execfile = None + if application.get("launch_hook"): + hook = application.get("launch_hook") + self.log.info("launching hook: {}".format(hook)) + pypelib.execute_hook(application.get("launch_hook")) + if sys.platform == "win32": for ext in os.environ["PATHEXT"].split(os.pathsep): diff --git a/pype/lib.py b/pype/lib.py index 2235efa2f4..73bc16e97a 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -6,6 +6,8 @@ import contextlib import subprocess import inspect +import six + from avalon import io import avalon.api import avalon @@ -585,3 +587,21 @@ class CustomNone: def __repr__(self): """Representation of custom None.""" return "".format(str(self.identifier)) + + +def execute_hook(hook, **kwargs): + class_name = hook.split("/")[-1] + + abspath = os.path.join(os.getenv('PYPE_ROOT'), + 'repos', 'pype', **hook.split("/")[:-1]) + + try: + with open(abspath) as f: + six.exec_(f.read()) + + except Exception as exp: + log.exception("loading hook failed: {}".format(exp), + exc_info=True) + + hook_obj = globals()[class_name]() + hook_obj.execute(**kwargs) From 69c396ec3d3c3a3f5dbbfb9678e60e876bdc5a0e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 20 Feb 2020 17:35:38 +0100 Subject: [PATCH 51/99] polishing hooks --- pype/ftrack/lib/ftrack_app_handler.py | 8 ---- pype/hooks/unreal/unreal_prelaunch.py | 8 ++++ pype/lib.py | 54 +++++++++++++++++++++----- res/app_icons/ue4.png | Bin 0 -> 46503 bytes 4 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 pype/hooks/unreal/unreal_prelaunch.py create mode 100644 res/app_icons/ue4.png diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index ea769ad167..825a0a1985 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -256,14 +256,6 @@ class AppAction(BaseHandler): env = acre.merge(env, current_env=dict(os.environ)) env = acre.append(dict(os.environ), env) - - # - # tools_env = acre.get_tools(tools) - # env = acre.compute(dict(tools_env)) - # env = acre.merge(env, dict(os.environ)) - # os.environ = acre.append(dict(os.environ), env) - # os.environ = acre.compute(os.environ) - # Get path to execute st_temp_path = os.environ['PYPE_CONFIG'] os_plat = platform.system().lower() diff --git a/pype/hooks/unreal/unreal_prelaunch.py b/pype/hooks/unreal/unreal_prelaunch.py new file mode 100644 index 0000000000..05d95a0b2a --- /dev/null +++ b/pype/hooks/unreal/unreal_prelaunch.py @@ -0,0 +1,8 @@ +from pype.lib import PypeHook + + +class UnrealPrelaunch(PypeHook): + + def execute(**kwargs): + print("I am inside!!!") + pass diff --git a/pype/lib.py b/pype/lib.py index 73bc16e97a..d1062e468f 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1,10 +1,13 @@ import os +import sys +import types import re import logging import itertools import contextlib import subprocess import inspect +from abc import ABCMeta, abstractmethod import six @@ -123,7 +126,8 @@ def modified_environ(*remove, **update): is sure to work in all situations. :param remove: Environment variables to remove. - :param update: Dictionary of environment variables and values to add/update. + :param update: Dictionary of environment variables + and values to add/update. """ env = os.environ update = update or {} @@ -347,8 +351,8 @@ def switch_item(container, "parent": version["_id"]} ) - assert representation, ("Could not find representation in the database with" - " the name '%s'" % representation_name) + assert representation, ("Could not find representation in the database " + "with the name '%s'" % representation_name) avalon.api.switch(container, representation) @@ -481,7 +485,9 @@ def get_subsets(asset_name, """ Query subsets with filter on name. - The method will return all found subsets and its defined version and subsets. Version could be specified with number. Representation can be filtered. + The method will return all found subsets and its defined version + and subsets. Version could be specified with number. Representation + can be filtered. Arguments: asset_name (str): asset (shot) name @@ -498,8 +504,8 @@ def get_subsets(asset_name, asset_io = io.find_one({"type": "asset", "name": asset_name}) # check if anything returned - assert asset_io, "Asset not existing. \ - Check correct name: `{}`".format(asset_name) + assert asset_io, ( + "Asset not existing. Check correct name: `{}`").format(asset_name) # create subsets query filter filter_query = {"type": "subset", "parent": asset_io["_id"]} @@ -513,7 +519,9 @@ def get_subsets(asset_name, # query all assets subsets = [s for s in io.find(filter_query)] - assert subsets, "No subsets found. Check correct filter. Try this for start `r'.*'`: asset: `{}`".format(asset_name) + assert subsets, ("No subsets found. Check correct filter. " + "Try this for start `r'.*'`: " + "asset: `{}`").format(asset_name) output_dict = {} # Process subsets @@ -593,15 +601,43 @@ def execute_hook(hook, **kwargs): class_name = hook.split("/")[-1] abspath = os.path.join(os.getenv('PYPE_ROOT'), - 'repos', 'pype', **hook.split("/")[:-1]) + 'repos', 'pype', *hook.split("/")[:-1]) + + mod_name, mod_ext = os.path.splitext(os.path.basename(abspath)) + + if not mod_ext == ".py": + return + + module = types.ModuleType(mod_name) + module.__file__ = abspath + + log.info("-" * 80) + print(module) try: with open(abspath) as f: - six.exec_(f.read()) + six.exec_(f.read(), module.__dict__) + + sys.modules[abspath] = module except Exception as exp: log.exception("loading hook failed: {}".format(exp), exc_info=True) + from pprint import pprint + print("-" * 80) + pprint(dir(module)) + hook_obj = globals()[class_name]() hook_obj.execute(**kwargs) + + +@six.add_metaclass(ABCMeta) +class PypeHook: + + def __init__(self): + pass + + @abstractmethod + def execute(**kwargs): + pass diff --git a/res/app_icons/ue4.png b/res/app_icons/ue4.png new file mode 100644 index 0000000000000000000000000000000000000000..39201de6643767b8b24c821316b910e2bd41b1ba GIT binary patch literal 46503 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
>Di8y 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

r@KoO^&9yCj+{ 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 opy3f*F#kPbd<=|71&O Date: Fri, 21 Feb 2020 11:36:33 +0100 Subject: [PATCH 52/99] unreal project creation and launching --- pype/ftrack/lib/ftrack_app_handler.py | 9 +- pype/hooks/unreal/unreal_prelaunch.py | 80 ++++++- pype/lib.py | 33 ++- pype/unreal/__init__.py | 0 pype/unreal/lib.py | 305 ++++++++++++++++++++++++++ 5 files changed, 411 insertions(+), 16 deletions(-) create mode 100644 pype/unreal/__init__.py create mode 100644 pype/unreal/lib.py diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 825a0a1985..5dd33c1492 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -268,7 +268,14 @@ class AppAction(BaseHandler): if application.get("launch_hook"): hook = application.get("launch_hook") self.log.info("launching hook: {}".format(hook)) - pypelib.execute_hook(application.get("launch_hook")) + ret_val = pypelib.execute_hook( + application.get("launch_hook"), env=env) + if not ret_val: + return { + 'success': False, + 'message': "Hook didn't finish successfully {0}" + .format(self.label) + } if sys.platform == "win32": diff --git a/pype/hooks/unreal/unreal_prelaunch.py b/pype/hooks/unreal/unreal_prelaunch.py index 05d95a0b2a..83ba4bf8aa 100644 --- a/pype/hooks/unreal/unreal_prelaunch.py +++ b/pype/hooks/unreal/unreal_prelaunch.py @@ -1,8 +1,82 @@ +import logging +import os + from pype.lib import PypeHook +from pype.unreal import lib as unreal_lib +from pypeapp import Logger + +log = logging.getLogger(__name__) class UnrealPrelaunch(PypeHook): + """ + This hook will check if current workfile path has Unreal + project inside. IF not, it initialize it and finally it pass + path to the project by environment variable to Unreal launcher + shell script. + """ - def execute(**kwargs): - print("I am inside!!!") - pass + def __init__(self, logger=None): + if not logger: + self.log = Logger().get_logger(self.__class__.__name__) + else: + self.log = logger + + self.signature = "( {} )".format(self.__class__.__name__) + + def execute(self, *args, env: dict = None) -> bool: + if not env: + env = os.environ + asset = env["AVALON_ASSET"] + task = env["AVALON_TASK"] + workdir = env["AVALON_WORKDIR"] + engine_version = env["AVALON_APP_NAME"].split("_")[-1] + project_name = f"{asset}_{task}" + + # Unreal is sensitive about project names longer then 20 chars + if len(project_name) > 20: + self.log.warning((f"Project name exceed 20 characters " + f"[ {project_name} ]!")) + + # Unreal doesn't accept non alphabet characters at the start + # of the project name. This is because project name is then used + # in various places inside c++ code and there variable names cannot + # start with non-alpha. We append 'P' before project name to solve it. + # :scream: + if not project_name[:1].isalpha(): + self.log.warning(f"Project name doesn't start with alphabet " + f"character ({project_name}). Appending 'P'") + project_name = f"P{project_name}" + + project_path = os.path.join(workdir, project_name) + + self.log.info((f"{self.signature} requested UE4 version: " + f"[ {engine_version} ]")) + + detected = unreal_lib.get_engine_versions() + detected_str = ', '.join(detected.keys()) or 'none' + self.log.info((f"{self.signature} detected UE4 versions: " + f"[ {detected_str} ]")) + del(detected_str) + engine_version = ".".join(engine_version.split(".")[:2]) + if engine_version not in detected.keys(): + self.log.error((f"{self.signature} requested version not " + f"detected [ {engine_version} ]")) + return False + + os.makedirs(project_path, exist_ok=True) + + project_file = os.path.join(project_path, f"{project_name}.uproject") + if not os.path.isfile(project_file): + self.log.info((f"{self.signature} creating unreal " + f"project [ {project_name} ]")) + if env.get("AVALON_UNREAL_PLUGIN"): + os.environ["AVALON_UNREAL_PLUGIN"] = env.get("AVALON_UNREAL_PLUGIN") # noqa: E501 + unreal_lib.create_unreal_project(project_name, + engine_version, project_path) + + self.log.info((f"{self.signature} preparing unreal project ... ")) + unreal_lib.prepare_project(project_file, detected[engine_version]) + + env["PYPE_UNREAL_PROJECT_FILE"] = project_file + return True diff --git a/pype/lib.py b/pype/lib.py index d1062e468f..87c206e758 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -597,7 +597,20 @@ class CustomNone: return "".format(str(self.identifier)) -def execute_hook(hook, **kwargs): +def execute_hook(hook, *args, **kwargs): + """ + This will load hook file, instantiate class and call `execute` method + on it. Hook must be in a form: + + `$PYPE_ROOT/repos/pype/path/to/hook.py/HookClass` + + This will load `hook.py`, instantiate HookClass and then execute_hook + `execute(*args, **kwargs)` + + :param hook: path to hook class + :type hook: str + """ + class_name = hook.split("/")[-1] abspath = os.path.join(os.getenv('PYPE_ROOT'), @@ -606,14 +619,11 @@ def execute_hook(hook, **kwargs): mod_name, mod_ext = os.path.splitext(os.path.basename(abspath)) if not mod_ext == ".py": - return + return False module = types.ModuleType(mod_name) module.__file__ = abspath - log.info("-" * 80) - print(module) - try: with open(abspath) as f: six.exec_(f.read(), module.__dict__) @@ -623,13 +633,12 @@ def execute_hook(hook, **kwargs): except Exception as exp: log.exception("loading hook failed: {}".format(exp), exc_info=True) + return False - from pprint import pprint - print("-" * 80) - pprint(dir(module)) - - hook_obj = globals()[class_name]() - hook_obj.execute(**kwargs) + obj = getattr(module, class_name) + hook_obj = obj() + ret_val = hook_obj.execute(*args, **kwargs) + return ret_val @six.add_metaclass(ABCMeta) @@ -639,5 +648,5 @@ class PypeHook: pass @abstractmethod - def execute(**kwargs): + def execute(self, *args, **kwargs): pass diff --git a/pype/unreal/__init__.py b/pype/unreal/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py new file mode 100644 index 0000000000..8217e834a3 --- /dev/null +++ b/pype/unreal/lib.py @@ -0,0 +1,305 @@ +import os +import platform +import json +from distutils import dir_util +import subprocess + + +def get_engine_versions(): + """ + This will try to detect location and versions of installed Unreal Engine. + Location can be overridden by `UNREAL_ENGINE_LOCATION` environment + variable. + + Returns dictionary with version as a key and dir as value. + """ + try: + engine_locations = {} + root, dirs, files = next(os.walk(os.environ["UNREAL_ENGINE_LOCATION"])) + + for dir in dirs: + if dir.startswith("UE_"): + ver = dir.split("_")[1] + engine_locations[ver] = os.path.join(root, dir) + except KeyError: + # environment variable not set + pass + except OSError: + # specified directory doesn't exists + pass + + # if we've got something, terminate autodetection process + if engine_locations: + return engine_locations + + # else kick in platform specific detection + if platform.system().lower() == "windows": + return _win_get_engine_versions() + elif platform.system().lower() == "linux": + # on linux, there is no installation and getting Unreal Engine involves + # git clone. So we'll probably depend on `UNREAL_ENGINE_LOCATION`. + pass + elif platform.system().lower() == "darwin": + return _darwin_get_engine_version() + + return {} + + +def _win_get_engine_versions(): + """ + If engines are installed via Epic Games Launcher then there is: + `%PROGRAMDATA%/Epic/UnrealEngineLauncher/LauncherInstalled.dat` + This file is JSON file listing installed stuff, Unreal engines + are marked with `"AppName" = "UE_X.XX"`` like `UE_4.24` + """ + install_json_path = os.path.join( + os.environ.get("PROGRAMDATA"), + "Epic", + "UnrealEngineLauncher", + "LauncherInstalled.dat", + ) + + return _parse_launcher_locations(install_json_path) + + +def _darwin_get_engine_version(): + """ + It works the same as on Windows, just JSON file location is different. + """ + install_json_path = os.path.join( + os.environ.get("HOME"), + "Library", + "Application Support", + "Epic", + "UnrealEngineLauncher", + "LauncherInstalled.dat", + ) + + return _parse_launcher_locations(install_json_path) + + +def _parse_launcher_locations(install_json_path): + engine_locations = {} + if os.path.isfile(install_json_path): + with open(install_json_path, "r") as ilf: + try: + install_data = json.load(ilf) + except json.JSONDecodeError: + raise Exception( + "Invalid `LauncherInstalled.dat file. `" + "Cannot determine Unreal Engine location." + ) + + for installation in install_data.get("InstallationList", []): + if installation.get("AppName").startswith("UE_"): + ver = installation.get("AppName").split("_")[1] + engine_locations[ver] = installation.get("InstallLocation") + + return engine_locations + + +def create_unreal_project(project_name, ue_version, dir): + """ + This will create `.uproject` file at specified location. As there is no + way I know to create project via command line, this is easiest option. + Unreal project file is basically JSON file. If we find + `AVALON_UNREAL_PLUGIN` environment variable we assume this is location + of Avalon Integration Plugin and we copy its content to project folder + and enable this plugin. + """ + + if os.path.isdir(os.environ.get("AVALON_UNREAL_PLUGIN", "")): + # copy plugin to correct path under project + plugin_path = os.path.join(dir, "Plugins", "Avalon") + if not os.path.isdir(plugin_path): + os.makedirs(plugin_path, exist_ok=True) + dir_util._path_created = {} + dir_util.copy_tree(os.environ.get("AVALON_UNREAL_PLUGIN"), + plugin_path) + + data = { + "FileVersion": 3, + "EngineAssociation": ue_version, + "Category": "", + "Description": "", + "Modules": [ + { + "Name": project_name, + "Type": "Runtime", + "LoadingPhase": "Default", + "AdditionalDependencies": ["Engine"], + } + ], + "Plugins": [ + {"Name": "PythonScriptPlugin", "Enabled": True}, + {"Name": "EditorScriptingUtilities", "Enabled": True}, + {"Name": "Avalon", "Enabled": True}, + ], + } + + project_file = os.path.join(dir, "{}.uproject".format(project_name)) + with open(project_file, mode="w") as pf: + json.dump(data, pf, indent=4) + + +def prepare_project(project_file: str, engine_path: str): + """ + This function will add source files needed for project to be + rebuild along with the avalon integration plugin. + + There seems not to be automated way to do it from command line. + But there might be way to create at least those target and build files + by some generator. This needs more research as manually writing + those files is rather hackish. :skull_and_crossbones: + + :param project_file: path to .uproject file + :type project_file: str + :param engine_path: path to unreal engine associated with project + :type engine_path: str + """ + + project_name = os.path.splitext(os.path.basename(project_file))[0] + project_dir = os.path.dirname(project_file) + targets_dir = os.path.join(project_dir, "Source") + sources_dir = os.path.join(targets_dir, project_name) + + os.makedirs(sources_dir, exist_ok=True) + os.makedirs(os.path.join(project_dir, "Content"), exist_ok=True) + + module_target = ''' +using UnrealBuildTool; +using System.Collections.Generic; + +public class {0}Target : TargetRules +{{ + public {0}Target( TargetInfo Target) : base(Target) + {{ + Type = TargetType.Game; + ExtraModuleNames.AddRange( new string[] {{ "{0}" }} ); + }} +}} +'''.format(project_name) + + editor_module_target = ''' +using UnrealBuildTool; +using System.Collections.Generic; + +public class {0}EditorTarget : TargetRules +{{ + public {0}EditorTarget( TargetInfo Target) : base(Target) + {{ + Type = TargetType.Editor; + + ExtraModuleNames.AddRange( new string[] {{ "{0}" }} ); + }} +}} +'''.format(project_name) + + module_build = ''' +using UnrealBuildTool; +public class {0} : ModuleRules +{{ + public {0}(ReadOnlyTargetRules Target) : base(Target) + {{ + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + PublicDependencyModuleNames.AddRange(new string[] {{ "Core", + "CoreUObject", "Engine", "InputCore" }}); + PrivateDependencyModuleNames.AddRange(new string[] {{ }}); + }} +}} +'''.format(project_name) + + module_cpp = ''' +#include "{0}.h" +#include "Modules/ModuleManager.h" + +IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, {0}, "{0}" ); +'''.format(project_name) + + module_header = ''' +#pragma once +#include "CoreMinimal.h" +''' + + game_mode_cpp = ''' +#include "{0}GameModeBase.h" +'''.format(project_name) + + game_mode_h = ''' +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameModeBase.h" +#include "{0}GameModeBase.generated.h" + +UCLASS() +class {1}_API A{0}GameModeBase : public AGameModeBase +{{ + GENERATED_BODY() +}}; +'''.format(project_name, project_name.upper()) + + with open(os.path.join( + targets_dir, f"{project_name}.Target.cs"), mode="w") as f: + f.write(module_target) + + with open(os.path.join( + targets_dir, f"{project_name}Editor.Target.cs"), mode="w") as f: + f.write(editor_module_target) + + with open(os.path.join( + sources_dir, f"{project_name}.Build.cs"), mode="w") as f: + f.write(module_build) + + with open(os.path.join( + sources_dir, f"{project_name}.cpp"), mode="w") as f: + f.write(module_cpp) + + with open(os.path.join( + sources_dir, f"{project_name}.h"), mode="w") as f: + f.write(module_header) + + with open(os.path.join( + sources_dir, f"{project_name}GameModeBase.cpp"), mode="w") as f: + f.write(game_mode_cpp) + + with open(os.path.join( + sources_dir, f"{project_name}GameModeBase.h"), mode="w") as f: + f.write(game_mode_h) + + if platform.system().lower() == "windows": + u_build_tool = (f"{engine_path}/Engine/Binaries/DotNET/" + "UnrealBuildTool.exe") + u_header_tool = (f"{engine_path}/Engine/Binaries/Win64/" + f"UnrealHeaderTool.exe") + elif platform.system().lower() == "linux": + # WARNING: there is no UnrealBuildTool on linux? + u_build_tool = "" + u_header_tool = "" + elif platform.system().lower() == "darwin": + # WARNING: there is no UnrealBuildTool on Mac? + u_build_tool = "" + u_header_tool = "" + + u_build_tool = u_build_tool.replace("\\", "/") + u_header_tool = u_header_tool.replace("\\", "/") + + command1 = [u_build_tool, "-projectfiles", f"-project={project_file}", + "-progress"] + + subprocess.run(command1) + + command2 = [u_build_tool, f"-ModuleWithSuffix={project_name},3555" + "Win64", "Development", "-TargetType=Editor" + f'-Project="{project_file}"', f'"{project_file}"' + "-IgnoreJunk"] + + subprocess.run(command2) + + uhtmanifest = os.path.join(os.path.dirname(project_file), + f"{project_name}.uhtmanifest") + + command3 = [u_header_tool, f'"{project_file}"', f'"{uhtmanifest}"', + "-Unattended", "-WarningsAsErrors", "-installed"] + + subprocess.run(command3) From 1537b2ba571e0e614277907377e30f303d4b48c4 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 21 Feb 2020 21:06:30 +0100 Subject: [PATCH 53/99] switch to UnrealEnginePython --- pype/hooks/unreal/unreal_prelaunch.py | 1 + pype/unreal/lib.py | 49 ++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/pype/hooks/unreal/unreal_prelaunch.py b/pype/hooks/unreal/unreal_prelaunch.py index 83ba4bf8aa..cb3b6e8e64 100644 --- a/pype/hooks/unreal/unreal_prelaunch.py +++ b/pype/hooks/unreal/unreal_prelaunch.py @@ -79,4 +79,5 @@ class UnrealPrelaunch(PypeHook): unreal_lib.prepare_project(project_file, detected[engine_version]) env["PYPE_UNREAL_PROJECT_FILE"] = project_file + env["AVALON_CURRENT_UNREAL_ENGINE"] = detected[engine_version] return True diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py index 8217e834a3..6130b38764 100644 --- a/pype/unreal/lib.py +++ b/pype/unreal/lib.py @@ -1,3 +1,4 @@ +import sys import os import platform import json @@ -107,15 +108,52 @@ def create_unreal_project(project_name, ue_version, dir): of Avalon Integration Plugin and we copy its content to project folder and enable this plugin. """ + import git if os.path.isdir(os.environ.get("AVALON_UNREAL_PLUGIN", "")): # copy plugin to correct path under project - plugin_path = os.path.join(dir, "Plugins", "Avalon") - if not os.path.isdir(plugin_path): - os.makedirs(plugin_path, exist_ok=True) + plugins_path = os.path.join(dir, "Plugins") + avalon_plugin_path = os.path.join(plugins_path, "Avalon") + if not os.path.isdir(avalon_plugin_path): + os.makedirs(avalon_plugin_path, exist_ok=True) dir_util._path_created = {} dir_util.copy_tree(os.environ.get("AVALON_UNREAL_PLUGIN"), - plugin_path) + avalon_plugin_path) + + # If `PYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from there to + # support offline installation. + # Otherwise clone UnrealEnginePython to Plugins directory + # https://github.com/20tab/UnrealEnginePython.git + uep_path = os.path.join(plugins_path, "UnrealEnginePython") + if os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): + + os.makedirs(uep_path, exist_ok=True) + dir_util._path_created = {} + dir_util.copy_tree(os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), + uep_path) + else: + git.Repo.clone_from("https://github.com/20tab/UnrealEnginePython.git", + uep_path) + + # now we need to fix python path in: + # `UnrealEnginePython.Build.cs` + # to point to our python + with open(os.path.join( + uep_path, "Source", + "UnrealEnginePython", + "UnrealEnginePython.Build.cs"), mode="r") as f: + build_file = f.read() + + fix = build_file.replace( + 'private string pythonHome = "";', + 'private string pythonHome = "{}";'.format( + sys.base_prefix.replace("\\", "/"))) + + with open(os.path.join( + uep_path, "Source", + "UnrealEnginePython", + "UnrealEnginePython.Build.cs"), mode="w") as f: + f.write(fix) data = { "FileVersion": 3, @@ -134,6 +172,7 @@ def create_unreal_project(project_name, ue_version, dir): {"Name": "PythonScriptPlugin", "Enabled": True}, {"Name": "EditorScriptingUtilities", "Enabled": True}, {"Name": "Avalon", "Enabled": True}, + {"Name": "UnrealEnginePython", "Enabled": True} ], } @@ -296,6 +335,7 @@ class {1}_API A{0}GameModeBase : public AGameModeBase subprocess.run(command2) + """ uhtmanifest = os.path.join(os.path.dirname(project_file), f"{project_name}.uhtmanifest") @@ -303,3 +343,4 @@ class {1}_API A{0}GameModeBase : public AGameModeBase "-Unattended", "-WarningsAsErrors", "-installed"] subprocess.run(command3) + """ From 875ca5cd6fd4f0da67b2dba80897dd27b6505bda Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 24 Feb 2020 16:26:10 +0000 Subject: [PATCH 54/99] Extraction for blend files is now handled by a single class --- .../blender/publish/collect_current_file.py | 2 + ...{extract_animation.py => extract_blend.py} | 93 +++++++++---------- pype/plugins/blender/publish/extract_model.py | 47 ---------- pype/plugins/blender/publish/extract_rig.py | 47 ---------- 4 files changed, 48 insertions(+), 141 deletions(-) rename pype/plugins/blender/publish/{extract_animation.py => extract_blend.py} (86%) delete mode 100644 pype/plugins/blender/publish/extract_model.py delete mode 100644 pype/plugins/blender/publish/extract_rig.py diff --git a/pype/plugins/blender/publish/collect_current_file.py b/pype/plugins/blender/publish/collect_current_file.py index a097c72047..926d290b31 100644 --- a/pype/plugins/blender/publish/collect_current_file.py +++ b/pype/plugins/blender/publish/collect_current_file.py @@ -14,3 +14,5 @@ class CollectBlenderCurrentFile(pyblish.api.ContextPlugin): """Inject the current working file""" current_file = bpy.data.filepath context.data['currentFile'] = current_file + + assert current_file != '', "Current file is empty. Save the file before continuing." diff --git a/pype/plugins/blender/publish/extract_animation.py b/pype/plugins/blender/publish/extract_blend.py similarity index 86% rename from pype/plugins/blender/publish/extract_animation.py rename to pype/plugins/blender/publish/extract_blend.py index dbfe29af83..7e11e9ef8d 100644 --- a/pype/plugins/blender/publish/extract_animation.py +++ b/pype/plugins/blender/publish/extract_blend.py @@ -1,47 +1,46 @@ -import os -import avalon.blender.workio - -import pype.api - - -class ExtractAnimation(pype.api.Extractor): - """Extract as animation.""" - - label = "Animation" - hosts = ["blender"] - families = ["animation"] - optional = True - - def process(self, instance): - # Define extract output file path - - stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.blend" - filepath = os.path.join(stagingdir, filename) - - # Perform extraction - self.log.info("Performing extraction..") - - # Just save the file to a temporary location. At least for now it's no - # problem to have (possibly) extra stuff in the file. - avalon.blender.workio.save_file(filepath, copy=True) - # - # # Store reference for integration - # if "files" not in instance.data: - # instance.data["files"] = list() - # - # # instance.data["files"].append(filename) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'blend', - 'ext': 'blend', - 'files': filename, - "stagingDir": stagingdir, - } - instance.data["representations"].append(representation) - - - self.log.info("Extracted instance '%s' to: %s", instance.name, representation) +import os +import avalon.blender.workio + +import pype.api + + +class ExtractBlend(pype.api.Extractor): + """Extract a blend file.""" + + label = "Extract Blend" + hosts = ["blender"] + families = ["animation", "model", "rig"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.blend" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction..") + + # Just save the file to a temporary location. At least for now it's no + # problem to have (possibly) extra stuff in the file. + avalon.blender.workio.save_file(filepath, copy=True) + # + # # Store reference for integration + # if "files" not in instance.data: + # instance.data["files"] = list() + # + # # instance.data["files"].append(filename) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'blend', + 'ext': 'blend', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s", instance.name, representation) diff --git a/pype/plugins/blender/publish/extract_model.py b/pype/plugins/blender/publish/extract_model.py deleted file mode 100644 index 501c4d9d5c..0000000000 --- a/pype/plugins/blender/publish/extract_model.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -import avalon.blender.workio - -import pype.api - - -class ExtractModel(pype.api.Extractor): - """Extract as model.""" - - label = "Model" - hosts = ["blender"] - families = ["model"] - optional = True - - def process(self, instance): - # Define extract output file path - - stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.blend" - filepath = os.path.join(stagingdir, filename) - - # Perform extraction - self.log.info("Performing extraction..") - - # Just save the file to a temporary location. At least for now it's no - # problem to have (possibly) extra stuff in the file. - avalon.blender.workio.save_file(filepath, copy=True) - # - # # Store reference for integration - # if "files" not in instance.data: - # instance.data["files"] = list() - # - # # instance.data["files"].append(filename) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'blend', - 'ext': 'blend', - 'files': filename, - "stagingDir": stagingdir, - } - instance.data["representations"].append(representation) - - - self.log.info("Extracted instance '%s' to: %s", instance.name, representation) diff --git a/pype/plugins/blender/publish/extract_rig.py b/pype/plugins/blender/publish/extract_rig.py deleted file mode 100644 index 8a3c83d07c..0000000000 --- a/pype/plugins/blender/publish/extract_rig.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -import avalon.blender.workio - -import pype.api - - -class ExtractRig(pype.api.Extractor): - """Extract as rig.""" - - label = "Rig" - hosts = ["blender"] - families = ["rig"] - optional = True - - def process(self, instance): - # Define extract output file path - - stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.blend" - filepath = os.path.join(stagingdir, filename) - - # Perform extraction - self.log.info("Performing extraction..") - - # Just save the file to a temporary location. At least for now it's no - # problem to have (possibly) extra stuff in the file. - avalon.blender.workio.save_file(filepath, copy=True) - # - # # Store reference for integration - # if "files" not in instance.data: - # instance.data["files"] = list() - # - # # instance.data["files"].append(filename) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'blend', - 'ext': 'blend', - 'files': filename, - "stagingDir": stagingdir, - } - instance.data["representations"].append(representation) - - - self.log.info("Extracted instance '%s' to: %s", instance.name, representation) From f2733c0a1b05bff6afa7b21a00c84beb436ca31d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 24 Feb 2020 16:26:40 +0000 Subject: [PATCH 55/99] Implemented extraction to FBX files --- pype/plugins/blender/publish/extract_fbx.py | 71 +++++++++++ .../blender/publish/extract_fbx_animation.py | 118 ++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 pype/plugins/blender/publish/extract_fbx.py create mode 100644 pype/plugins/blender/publish/extract_fbx_animation.py diff --git a/pype/plugins/blender/publish/extract_fbx.py b/pype/plugins/blender/publish/extract_fbx.py new file mode 100644 index 0000000000..95466c1d2b --- /dev/null +++ b/pype/plugins/blender/publish/extract_fbx.py @@ -0,0 +1,71 @@ +import os +import avalon.blender.workio + +import pype.api + +import bpy + +class ExtractFBX(pype.api.Extractor): + """Extract as FBX.""" + + label = "Extract FBX" + hosts = ["blender"] + families = ["model", "rig"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.fbx" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction..") + + collections = [obj for obj in instance if type(obj) is bpy.types.Collection] + + assert len(collections) == 1, "There should be one and only one collection collected for this asset" + + old_active_layer_collection = bpy.context.view_layer.active_layer_collection + + # Get the layer collection from the collection we need to export. + # This is needed because in Blender you can only set the active + # collection with the layer collection, and there is no way to get + # the layer collection from the collection (but there is the vice versa). + layer_collections = [layer for layer in bpy.context.view_layer.layer_collection.children if layer.collection == collections[0]] + + assert len(layer_collections) == 1 + + bpy.context.view_layer.active_layer_collection = layer_collections[0] + + old_scale = bpy.context.scene.unit_settings.scale_length + + # We set the scale of the scene for the export + bpy.context.scene.unit_settings.scale_length = 0.01 + + # We export the fbx + bpy.ops.export_scene.fbx( + filepath=filepath, + use_active_collection=True, + mesh_smooth_type='FACE', + add_leaf_bones=False + ) + + bpy.context.view_layer.active_layer_collection = old_active_layer_collection + + bpy.context.scene.unit_settings.scale_length = old_scale + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s", + instance.name, representation) diff --git a/pype/plugins/blender/publish/extract_fbx_animation.py b/pype/plugins/blender/publish/extract_fbx_animation.py new file mode 100644 index 0000000000..bc088f8bb7 --- /dev/null +++ b/pype/plugins/blender/publish/extract_fbx_animation.py @@ -0,0 +1,118 @@ +import os +import avalon.blender.workio + +import pype.api + +import bpy +import bpy_extras +import bpy_extras.anim_utils + + +class ExtractAnimationFBX(pype.api.Extractor): + """Extract as animation.""" + + label = "Extract FBX" + hosts = ["blender"] + families = ["animation"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.fbx" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction..") + + collections = [obj for obj in instance if type(obj) is bpy.types.Collection] + + assert len(collections) == 1, "There should be one and only one collection collected for this asset" + + old_active_layer_collection = bpy.context.view_layer.active_layer_collection + + # Get the layer collection from the collection we need to export. + # This is needed because in Blender you can only set the active + # collection with the layer collection, and there is no way to get + # the layer collection from the collection (but there is the vice versa). + layer_collections = [layer for layer in bpy.context.view_layer.layer_collection.children if layer.collection == collections[0]] + + assert len(layer_collections) == 1 + + bpy.context.view_layer.active_layer_collection = layer_collections[0] + + old_scale = bpy.context.scene.unit_settings.scale_length + + # We set the scale of the scene for the export + bpy.context.scene.unit_settings.scale_length = 0.01 + + # We export all the objects in the collection + objects_to_export = collections[0].objects + + object_action_pairs = [] + original_actions = [] + + starting_frames = [] + ending_frames = [] + + # For each object, we make a copy of the current action + for obj in objects_to_export: + + curr_action = obj.animation_data.action + copy_action = curr_action.copy() + + object_action_pairs.append((obj, copy_action)) + original_actions.append(curr_action) + + curr_frame_range = curr_action.frame_range + + starting_frames.append( curr_frame_range[0] ) + ending_frames.append( curr_frame_range[1] ) + + # We compute the starting and ending frames + max_frame = min( starting_frames ) + min_frame = max( ending_frames ) + + # We bake the copy of the current action for each object + bpy_extras.anim_utils.bake_action_objects( + object_action_pairs, + frames=range(int(min_frame), int(max_frame)), + do_object=False, + do_clean=False + ) + + # We export the fbx + bpy.ops.export_scene.fbx( + filepath=filepath, + use_active_collection=True, + bake_anim_use_nla_strips=False, + bake_anim_use_all_actions=False, + add_leaf_bones=False + ) + + bpy.context.view_layer.active_layer_collection = old_active_layer_collection + + bpy.context.scene.unit_settings.scale_length = old_scale + + # We delete the baked action and set the original one back + for i in range(0, len(object_action_pairs)): + + object_action_pairs[i][0].animation_data.action = original_actions[i] + + object_action_pairs[i][1].user_clear() + bpy.data.actions.remove(object_action_pairs[i][1]) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s", + instance.name, representation) From 5a56df384d0cdb4fd3c19bf64841269a26b8c025 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 2 Mar 2020 12:25:15 +0100 Subject: [PATCH 56/99] set cpp project mode on demand --- pype/hooks/unreal/unreal_prelaunch.py | 12 +- pype/unreal/lib.py | 173 ++++++++++++++++++-------- 2 files changed, 127 insertions(+), 58 deletions(-) diff --git a/pype/hooks/unreal/unreal_prelaunch.py b/pype/hooks/unreal/unreal_prelaunch.py index cb3b6e8e64..efb5d9157b 100644 --- a/pype/hooks/unreal/unreal_prelaunch.py +++ b/pype/hooks/unreal/unreal_prelaunch.py @@ -42,7 +42,7 @@ class UnrealPrelaunch(PypeHook): # of the project name. This is because project name is then used # in various places inside c++ code and there variable names cannot # start with non-alpha. We append 'P' before project name to solve it. - # :scream: + # 😱 if not project_name[:1].isalpha(): self.log.warning(f"Project name doesn't start with alphabet " f"character ({project_name}). Appending 'P'") @@ -67,17 +67,17 @@ class UnrealPrelaunch(PypeHook): os.makedirs(project_path, exist_ok=True) project_file = os.path.join(project_path, f"{project_name}.uproject") + engine_path = detected[engine_version] if not os.path.isfile(project_file): self.log.info((f"{self.signature} creating unreal " f"project [ {project_name} ]")) if env.get("AVALON_UNREAL_PLUGIN"): os.environ["AVALON_UNREAL_PLUGIN"] = env.get("AVALON_UNREAL_PLUGIN") # noqa: E501 unreal_lib.create_unreal_project(project_name, - engine_version, project_path) - - self.log.info((f"{self.signature} preparing unreal project ... ")) - unreal_lib.prepare_project(project_file, detected[engine_version]) + engine_version, + project_path, + engine_path=engine_path) env["PYPE_UNREAL_PROJECT_FILE"] = project_file - env["AVALON_CURRENT_UNREAL_ENGINE"] = detected[engine_version] + env["AVALON_CURRENT_UNREAL_ENGINE"] = engine_path return True diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py index 6130b38764..be6314b09b 100644 --- a/pype/unreal/lib.py +++ b/pype/unreal/lib.py @@ -4,6 +4,7 @@ import platform import json from distutils import dir_util import subprocess +from pypeapp import config def get_engine_versions(): @@ -63,7 +64,7 @@ def _win_get_engine_versions(): return _parse_launcher_locations(install_json_path) -def _darwin_get_engine_version(): +def _darwin_get_engine_version() -> dict: """ It works the same as on Windows, just JSON file location is different. """ @@ -79,7 +80,16 @@ def _darwin_get_engine_version(): return _parse_launcher_locations(install_json_path) -def _parse_launcher_locations(install_json_path): +def _parse_launcher_locations(install_json_path: str) -> dict: + """ + This will parse locations from json file. + + :param install_json_path: path to `LauncherInstalled.dat` + :type install_json_path: str + :returns: returns dict with unreal engine versions as keys and + paths to those engine installations as value. + :rtype: dict + """ engine_locations = {} if os.path.isfile(install_json_path): with open(install_json_path, "r") as ilf: @@ -99,7 +109,11 @@ def _parse_launcher_locations(install_json_path): return engine_locations -def create_unreal_project(project_name, ue_version, dir): +def create_unreal_project(project_name: str, + ue_version: str, + pr_dir: str, + engine_path: str, + dev_mode: bool = False) -> None: """ This will create `.uproject` file at specified location. As there is no way I know to create project via command line, this is easiest option. @@ -107,12 +121,30 @@ def create_unreal_project(project_name, ue_version, dir): `AVALON_UNREAL_PLUGIN` environment variable we assume this is location of Avalon Integration Plugin and we copy its content to project folder and enable this plugin. + + :param project_name: project name + :type project_name: str + :param ue_version: unreal engine version (like 4.23) + :type ue_version: str + :param pr_dir: path to directory where project will be created + :type pr_dir: str + :param engine_path: Path to Unreal Engine installation + :type engine_path: str + :param dev_mode: Flag to trigger C++ style Unreal project needing + Visual Studio and other tools to compile plugins from + sources. This will trigger automatically if `Binaries` + directory is not found in plugin folders as this indicates + this is only source distribution of the plugin. Dev mode + is also set by preset file `unreal/project_setup.json` in + **PYPE_CONFIG**. + :type dev_mode: bool + :returns: None """ - import git + preset = config.get_presets()["unreal"]["project_setup"] if os.path.isdir(os.environ.get("AVALON_UNREAL_PLUGIN", "")): # copy plugin to correct path under project - plugins_path = os.path.join(dir, "Plugins") + plugins_path = os.path.join(pr_dir, "Plugins") avalon_plugin_path = os.path.join(plugins_path, "Avalon") if not os.path.isdir(avalon_plugin_path): os.makedirs(avalon_plugin_path, exist_ok=True) @@ -120,68 +152,105 @@ def create_unreal_project(project_name, ue_version, dir): dir_util.copy_tree(os.environ.get("AVALON_UNREAL_PLUGIN"), avalon_plugin_path) - # If `PYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from there to - # support offline installation. - # Otherwise clone UnrealEnginePython to Plugins directory - # https://github.com/20tab/UnrealEnginePython.git - uep_path = os.path.join(plugins_path, "UnrealEnginePython") - if os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): - - os.makedirs(uep_path, exist_ok=True) - dir_util._path_created = {} - dir_util.copy_tree(os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), - uep_path) - else: - git.Repo.clone_from("https://github.com/20tab/UnrealEnginePython.git", - uep_path) - - # now we need to fix python path in: - # `UnrealEnginePython.Build.cs` - # to point to our python - with open(os.path.join( - uep_path, "Source", - "UnrealEnginePython", - "UnrealEnginePython.Build.cs"), mode="r") as f: - build_file = f.read() - - fix = build_file.replace( - 'private string pythonHome = "";', - 'private string pythonHome = "{}";'.format( - sys.base_prefix.replace("\\", "/"))) - - with open(os.path.join( - uep_path, "Source", - "UnrealEnginePython", - "UnrealEnginePython.Build.cs"), mode="w") as f: - f.write(fix) + if (not os.path.isdir(os.path.join(avalon_plugin_path, "Binaries")) + or not os.path.join(avalon_plugin_path, "Intermediate")): + dev_mode = True + # data for project file data = { "FileVersion": 3, "EngineAssociation": ue_version, "Category": "", "Description": "", - "Modules": [ - { + "Plugins": [ + {"Name": "PythonScriptPlugin", "Enabled": True}, + {"Name": "EditorScriptingUtilities", "Enabled": True}, + {"Name": "Avalon", "Enabled": True} + ] + } + + if preset["install_unreal_python_engine"]: + # If `PYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from there to + # support offline installation. + # Otherwise clone UnrealEnginePython to Plugins directory + # https://github.com/20tab/UnrealEnginePython.git + uep_path = os.path.join(plugins_path, "UnrealEnginePython") + if os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): + + os.makedirs(uep_path, exist_ok=True) + dir_util._path_created = {} + dir_util.copy_tree( + os.environ.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), + uep_path) + else: + # WARNING: this will trigger dev_mode, because we need to compile + # this plugin. + dev_mode = True + import git + git.Repo.clone_from( + "https://github.com/20tab/UnrealEnginePython.git", + uep_path) + + data["Plugins"].append( + {"Name": "UnrealEnginePython", "Enabled": True}) + + if (not os.path.isdir(os.path.join(uep_path, "Binaries")) + or not os.path.join(uep_path, "Intermediate")): + dev_mode = True + + if dev_mode or preset["dev_mode"]: + # this will add project module and necessary source file to make it + # C++ project and to (hopefully) make Unreal Editor to compile all + # sources at start + + data["Modules"] = [{ "Name": project_name, "Type": "Runtime", "LoadingPhase": "Default", "AdditionalDependencies": ["Engine"], - } - ], - "Plugins": [ - {"Name": "PythonScriptPlugin", "Enabled": True}, - {"Name": "EditorScriptingUtilities", "Enabled": True}, - {"Name": "Avalon", "Enabled": True}, - {"Name": "UnrealEnginePython", "Enabled": True} - ], - } + }] - project_file = os.path.join(dir, "{}.uproject".format(project_name)) + if preset["install_unreal_python_engine"]: + # now we need to fix python path in: + # `UnrealEnginePython.Build.cs` + # to point to our python + with open(os.path.join( + uep_path, "Source", + "UnrealEnginePython", + "UnrealEnginePython.Build.cs"), mode="r") as f: + build_file = f.read() + + fix = build_file.replace( + 'private string pythonHome = "";', + 'private string pythonHome = "{}";'.format( + sys.base_prefix.replace("\\", "/"))) + + with open(os.path.join( + uep_path, "Source", + "UnrealEnginePython", + "UnrealEnginePython.Build.cs"), mode="w") as f: + f.write(fix) + + # write project file + project_file = os.path.join(pr_dir, "{}.uproject".format(project_name)) with open(project_file, mode="w") as pf: json.dump(data, pf, indent=4) + # ensure we have PySide installed in engine + # TODO: make it work for other platforms 🍎 🐧 + if platform.system().lower() == "windows": + python_path = os.path.join(engine_path, "Engine", "Binaries", + "ThirdParty", "Python", "Win64", + "python.exe") -def prepare_project(project_file: str, engine_path: str): + subprocess.run([python_path, "-m", + "pip", "install", "pyside"]) + + if dev_mode or preset["dev_mode"]: + _prepare_cpp_project(pr_dir, engine_path) + + +def _prepare_cpp_project(project_file: str, engine_path: str) -> None: """ This function will add source files needed for project to be rebuild along with the avalon integration plugin. From ec411a4b5dfd4b1bef1553236f7c0820de9581b2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 4 Mar 2020 17:00:00 +0000 Subject: [PATCH 57/99] Added 'action' family and small adjustments --- pype/plugins/blender/create/create_action.py | 38 +++ .../blender/create/create_animation.py | 2 + pype/plugins/blender/load/load_action.py | 295 ++++++++++++++++++ pype/plugins/blender/load/load_animation.py | 9 +- pype/plugins/blender/load/load_model.py | 4 + .../plugins/blender/publish/collect_action.py | 53 ++++ .../blender/publish/collect_animation.py | 2 +- pype/plugins/blender/publish/extract_blend.py | 2 +- pype/plugins/global/publish/integrate_new.py | 3 +- 9 files changed, 401 insertions(+), 7 deletions(-) create mode 100644 pype/plugins/blender/create/create_action.py create mode 100644 pype/plugins/blender/load/load_action.py create mode 100644 pype/plugins/blender/publish/collect_action.py diff --git a/pype/plugins/blender/create/create_action.py b/pype/plugins/blender/create/create_action.py new file mode 100644 index 0000000000..88ecebdfff --- /dev/null +++ b/pype/plugins/blender/create/create_action.py @@ -0,0 +1,38 @@ +"""Create an animation asset.""" + +import bpy + +from avalon import api +from avalon.blender import Creator, lib + + +class CreateAction(Creator): + """Action output for character rigs""" + + name = "actionMain" + label = "Action" + family = "action" + icon = "male" + + def process(self): + import pype.blender + + asset = self.data["asset"] + subset = self.data["subset"] + name = pype.blender.plugin.asset_name(asset, subset) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + self.data['task'] = api.Session.get('AVALON_TASK') + lib.imprint(collection, self.data) + + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): + if obj.animation_data is not None and obj.animation_data.action is not None: + + empty_obj = bpy.data.objects.new( name = name, object_data = None ) + empty_obj.animation_data_create() + empty_obj.animation_data.action = obj.animation_data.action + empty_obj.animation_data.action.name = name + collection.objects.link(empty_obj) + + return collection diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py index cfe569f918..14a50ba5ea 100644 --- a/pype/plugins/blender/create/create_animation.py +++ b/pype/plugins/blender/create/create_animation.py @@ -1,3 +1,5 @@ +"""Create an animation asset.""" + import bpy from avalon import api diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py new file mode 100644 index 0000000000..6094f712ae --- /dev/null +++ b/pype/plugins/blender/load/load_action.py @@ -0,0 +1,295 @@ +"""Load an action in Blender.""" + +import logging +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +import avalon.blender.pipeline +import bpy +import pype.blender +from avalon import api + +logger = logging.getLogger("pype").getChild("blender").getChild("load_action") + + +class BlendAnimationLoader(pype.blender.AssetLoader): + """Load action from a .blend file. + + Warning: + Loading the same asset more then once is not properly supported at the + moment. + """ + + families = ["action"] + representations = ["blend"] + + label = "Link Action" + icon = "code-fork" + color = "orange" + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( + asset, subset, namespace + ) + relative = bpy.context.preferences.filepaths.use_relative_paths + + container = bpy.data.collections.new(lib_container) + container.name = container_name + avalon.blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = container.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + animation_container = scene.collection.children[lib_container].make_local() + + objects_list = [] + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in animation_container.objects: + + obj = obj.make_local() + + # obj.data.make_local() + + if obj.animation_data is not None and obj.animation_data.action is not None: + + obj.animation_data.action.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + objects_list.append(obj) + + animation_container.pop(avalon.blender.pipeline.AVALON_PROPERTY) + + # Save the list of objects in the metadata container + container_metadata["objects"] = objects_list + + bpy.ops.object.select_all(action='DESELECT') + + nodes = list(container.objects) + nodes.append(container) + self[:] = nodes + return nodes + + def update(self, container: Dict, representation: Dict): + """Update the loaded asset. + + This will remove all objects of the current collection, load the new + ones and add them to the collection. + If the objects of the collection are used in another collection they + will not be removed, only unlinked. Normally this should not be the + case though. + + Warning: + No nested collections are supported at the moment! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + logger.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert collection, ( + f"The asset is not loaded: {container['objectName']}" + ) + assert not (collection.children), ( + "Nested collections are not supported." + ) + assert libpath, ( + "No existing library file found for {container['objectName']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in pype.blender.plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + collection_libpath = collection_metadata["libpath"] + normalized_collection_libpath = ( + str(Path(bpy.path.abspath(collection_libpath)).resolve()) + ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + logger.debug( + "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_collection_libpath, + normalized_libpath, + ) + if normalized_collection_libpath == normalized_libpath: + logger.info("Library already loaded, not updating...") + return + + strips = [] + + for obj in collection_metadata["objects"]: + + for armature_obj in [ objj for objj in bpy.data.objects if objj.type == 'ARMATURE' ]: + + if armature_obj.animation_data is not None: + + for track in armature_obj.animation_data.nla_tracks: + + for strip in track.strips: + + if strip.action == obj.animation_data.action: + + strips.append(strip) + + bpy.data.actions.remove(obj.animation_data.action) + bpy.data.objects.remove(obj) + + lib_container = collection_metadata["lib_container"] + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + relative = bpy.context.preferences.filepaths.use_relative_paths + with bpy.data.libraries.load( + str(libpath), link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + animation_container = scene.collection.children[lib_container].make_local() + + objects_list = [] + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in animation_container.objects: + + obj = obj.make_local() + + if obj.animation_data is not None and obj.animation_data.action is not None: + + obj.animation_data.action.make_local() + + for strip in strips: + + strip.action = obj.animation_data.action + strip.action_frame_end = obj.animation_data.action.frame_range[1] + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": collection.name}) + + objects_list.append(obj) + + animation_container.pop(avalon.blender.pipeline.AVALON_PROPERTY) + + # Save the list of objects in the metadata container + collection_metadata["objects"] = objects_list + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + bpy.ops.object.select_all(action='DESELECT') + + def remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (avalon-core:container-1.0): Container to remove, + from `host.ls()`. + + Returns: + bool: Whether the container was deleted. + + Warning: + No nested collections are supported at the moment! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + if not collection: + return False + assert not (collection.children), ( + "Nested collections are not supported." + ) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + + for obj in objects: + + for armature_obj in [ objj for objj in bpy.data.objects if objj.type == 'ARMATURE' ]: + + if armature_obj.animation_data is not None: + + for track in armature_obj.animation_data.nla_tracks: + + for strip in track.strips: + + if strip.action == obj.animation_data.action: + + track.strips.remove(strip) + + bpy.data.actions.remove(obj.animation_data.action) + bpy.data.objects.remove(obj) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + bpy.data.collections.remove(collection) + + return True diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index 58a0e94665..c6d18fb1a9 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -10,15 +10,12 @@ import bpy import pype.blender from avalon import api -logger = logging.getLogger("pype").getChild("blender").getChild("load_model") +logger = logging.getLogger("pype").getChild("blender").getChild("load_animation") class BlendAnimationLoader(pype.blender.AssetLoader): """Load animations from a .blend file. - Because they come from a .blend file we can simply link the collection that - contains the model. There is no further need to 'containerise' it. - Warning: Loading the same asset more then once is not properly supported at the moment. @@ -94,6 +91,10 @@ class BlendAnimationLoader(pype.blender.AssetLoader): obj.data.make_local() + if obj.animation_data is not None and obj.animation_data.action is not None: + + obj.animation_data.action.make_local() + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 40d6c3434c..8ba8c5cfc8 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -87,6 +87,10 @@ class BlendModelLoader(pype.blender.AssetLoader): obj.data.make_local() + for material_slot in obj.material_slots: + + material_slot.material.make_local() + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() diff --git a/pype/plugins/blender/publish/collect_action.py b/pype/plugins/blender/publish/collect_action.py new file mode 100644 index 0000000000..0b5e468920 --- /dev/null +++ b/pype/plugins/blender/publish/collect_action.py @@ -0,0 +1,53 @@ +import typing +from typing import Generator + +import bpy + +import avalon.api +import pyblish.api +from avalon.blender.pipeline import AVALON_PROPERTY + + +class CollectAnimation(pyblish.api.ContextPlugin): + """Collect the data of an action.""" + + hosts = ["blender"] + label = "Collect Action" + order = pyblish.api.CollectorOrder + + @staticmethod + def get_action_collections() -> Generator: + """Return all 'animation' collections. + + Check if the family is 'action' and if it doesn't have the + representation set. If the representation is set, it is a loaded action + and we don't want to publish it. + """ + for collection in bpy.data.collections: + avalon_prop = collection.get(AVALON_PROPERTY) or dict() + if (avalon_prop.get('family') == 'action' + and not avalon_prop.get('representation')): + yield collection + + def process(self, context): + """Collect the actions from the current Blender scene.""" + collections = self.get_action_collections() + for collection in collections: + avalon_prop = collection[AVALON_PROPERTY] + asset = avalon_prop['asset'] + family = avalon_prop['family'] + subset = avalon_prop['subset'] + task = avalon_prop['task'] + name = f"{asset}_{subset}" + instance = context.create_instance( + name=name, + family=family, + families=[family], + subset=subset, + asset=asset, + task=task, + ) + members = list(collection.objects) + members.append(collection) + instance[:] = members + self.log.debug(instance.data) diff --git a/pype/plugins/blender/publish/collect_animation.py b/pype/plugins/blender/publish/collect_animation.py index 9bc0b02227..109ae98e6f 100644 --- a/pype/plugins/blender/publish/collect_animation.py +++ b/pype/plugins/blender/publish/collect_animation.py @@ -20,7 +20,7 @@ class CollectAnimation(pyblish.api.ContextPlugin): """Return all 'animation' collections. Check if the family is 'animation' and if it doesn't have the - representation set. If the representation is set, it is a loaded rig + representation set. If the representation is set, it is a loaded animation and we don't want to publish it. """ for collection in bpy.data.collections: diff --git a/pype/plugins/blender/publish/extract_blend.py b/pype/plugins/blender/publish/extract_blend.py index 7e11e9ef8d..032f85897d 100644 --- a/pype/plugins/blender/publish/extract_blend.py +++ b/pype/plugins/blender/publish/extract_blend.py @@ -9,7 +9,7 @@ class ExtractBlend(pype.api.Extractor): label = "Extract Blend" hosts = ["blender"] - families = ["animation", "model", "rig"] + families = ["animation", "model", "rig", "action"] optional = True def process(self, instance): diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 813417bdfc..86ada2f111 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -78,7 +78,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "matchmove", "image" "source", - "assembly" + "assembly", + "action" ] exclude_families = ["clip"] db_representation_context_keys = [ From de972a2fa45c22cc6b5337e342028372261028ac Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 5 Mar 2020 10:27:57 +0000 Subject: [PATCH 58/99] Fixed a naming issue --- pype/plugins/blender/publish/collect_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/blender/publish/collect_action.py b/pype/plugins/blender/publish/collect_action.py index 0b5e468920..9a54045cea 100644 --- a/pype/plugins/blender/publish/collect_action.py +++ b/pype/plugins/blender/publish/collect_action.py @@ -8,7 +8,7 @@ import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY -class CollectAnimation(pyblish.api.ContextPlugin): +class CollectAction(pyblish.api.ContextPlugin): """Collect the data of an action.""" hosts = ["blender"] From 5bf25ffd3ee322e16c508bda88322faafa198e04 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 11 Mar 2020 12:39:25 +0000 Subject: [PATCH 59/99] Bug fixing and code optimization --- pype/plugins/blender/create/create_rig.py | 63 ++++++- pype/plugins/blender/load/load_action.py | 6 +- pype/plugins/blender/load/load_animation.py | 161 ++++++++---------- pype/plugins/blender/load/load_model.py | 146 +++++++---------- pype/plugins/blender/load/load_rig.py | 172 ++++++++------------ 5 files changed, 262 insertions(+), 286 deletions(-) diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index 5d83fafdd3..f630c63966 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -14,6 +14,23 @@ class CreateRig(Creator): family = "rig" icon = "wheelchair" + # @staticmethod + # def _find_layer_collection(self, layer_collection, collection): + + # found = None + + # if (layer_collection.collection == collection): + + # return layer_collection + + # for layer in layer_collection.children: + + # found = self._find_layer_collection(layer, collection) + + # if found: + + # return found + def process(self): import pype.blender @@ -25,8 +42,52 @@ class CreateRig(Creator): self.data['task'] = api.Session.get('AVALON_TASK') lib.imprint(collection, self.data) + # Add the rig object and all the children meshes to + # a set and link them all at the end to avoid duplicates. + # Blender crashes if trying to link an object that is already linked. + # This links automatically the children meshes if they were not + # selected, and doesn't link them twice if they, insted, + # were manually selected by the user. + objects_to_link = set() + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): - collection.objects.link(obj) + + objects_to_link.add( obj ) + + if obj.type == 'ARMATURE': + + for subobj in obj.children: + + objects_to_link.add( subobj ) + + # Create a new collection and link the widgets that + # the rig uses. + # custom_shapes = set() + + # for posebone in obj.pose.bones: + + # if posebone.custom_shape is not None: + + # custom_shapes.add( posebone.custom_shape ) + + # if len( custom_shapes ) > 0: + + # widgets_collection = bpy.data.collections.new(name="Widgets") + + # collection.children.link(widgets_collection) + + # for custom_shape in custom_shapes: + + # widgets_collection.objects.link( custom_shape ) + + # layer_collection = self._find_layer_collection(bpy.context.view_layer.layer_collection, widgets_collection) + + # layer_collection.exclude = True + + for obj in objects_to_link: + + collection.objects.link(obj) return collection diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py index 6094f712ae..747bcd47f5 100644 --- a/pype/plugins/blender/load/load_action.py +++ b/pype/plugins/blender/load/load_action.py @@ -13,7 +13,7 @@ from avalon import api logger = logging.getLogger("pype").getChild("blender").getChild("load_action") -class BlendAnimationLoader(pype.blender.AssetLoader): +class BlendActionLoader(pype.blender.AssetLoader): """Load action from a .blend file. Warning: @@ -47,7 +47,6 @@ class BlendAnimationLoader(pype.blender.AssetLoader): container_name = pype.blender.plugin.asset_name( asset, subset, namespace ) - relative = bpy.context.preferences.filepaths.use_relative_paths container = bpy.data.collections.new(lib_container) container.name = container_name @@ -65,6 +64,7 @@ class BlendAnimationLoader(pype.blender.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container + relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( libpath, link=True, relative=relative ) as (_, data_to): @@ -85,8 +85,6 @@ class BlendAnimationLoader(pype.blender.AssetLoader): obj = obj.make_local() - # obj.data.make_local() - if obj.animation_data is not None and obj.animation_data.action is not None: obj.animation_data.action.make_local() diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index c6d18fb1a9..0610517b67 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -28,43 +28,22 @@ class BlendAnimationLoader(pype.blender.AssetLoader): icon = "code-fork" color = "orange" - def process_asset( - self, context: dict, name: str, namespace: Optional[str] = None, - options: Optional[Dict] = None - ) -> Optional[List]: - """ - Arguments: - name: Use pre-defined name - namespace: Use pre-defined namespace - context: Full parenthood of representation to load - options: Additional settings dictionary - """ + @staticmethod + def _remove(self, objects, lib_container): + + for obj in objects: + + if obj.type == 'ARMATURE': + bpy.data.armatures.remove(obj.data) + elif obj.type == 'MESH': + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + @staticmethod + def _process(self, libpath, lib_container, container_name): - libpath = self.fname - asset = context["asset"]["name"] - subset = context["subset"]["name"] - lib_container = pype.blender.plugin.asset_name(asset, subset) - container_name = pype.blender.plugin.asset_name( - asset, subset, namespace - ) relative = bpy.context.preferences.filepaths.use_relative_paths - - container = bpy.data.collections.new(lib_container) - container.name = container_name - avalon.blender.pipeline.containerise_existing( - container, - name, - namespace, - context, - self.__class__.__name__, - ) - - container_metadata = container.get( - avalon.blender.pipeline.AVALON_PROPERTY) - - container_metadata["libpath"] = libpath - container_metadata["lib_container"] = lib_container - with bpy.data.libraries.load( libpath, link=True, relative=relative ) as (_, data_to): @@ -77,8 +56,9 @@ class BlendAnimationLoader(pype.blender.AssetLoader): animation_container = scene.collection.children[lib_container].make_local() meshes = [obj for obj in animation_container.objects if obj.type == 'MESH'] - armatures = [ - obj for obj in animation_container.objects if obj.type == 'ARMATURE'] + armatures = [obj for obj in animation_container.objects if obj.type == 'ARMATURE'] + + # Should check if there is only an armature? objects_list = [] @@ -106,11 +86,51 @@ class BlendAnimationLoader(pype.blender.AssetLoader): animation_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + bpy.ops.object.select_all(action='DESELECT') + + return objects_list + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( + asset, subset, namespace + ) + + container = bpy.data.collections.new(lib_container) + container.name = container_name + avalon.blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = container.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + objects_list = self._process(self, libpath, lib_container, container_name) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list - bpy.ops.object.select_all(action='DESELECT') - nodes = list(container.objects) nodes.append(container) self[:] = nodes @@ -177,59 +197,16 @@ class BlendAnimationLoader(pype.blender.AssetLoader): logger.info("Library already loaded, not updating...") return - # Get the armature of the rig - armatures = [obj for obj in collection_metadata["objects"] - if obj.type == 'ARMATURE'] - assert(len(armatures) == 1) - - for obj in collection_metadata["objects"]: - - if obj.type == 'ARMATURE': - bpy.data.armatures.remove(obj.data) - elif obj.type == 'MESH': - bpy.data.meshes.remove(obj.data) - + objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - bpy.data.collections.remove(bpy.data.collections[lib_container]) - - relative = bpy.context.preferences.filepaths.use_relative_paths - with bpy.data.libraries.load( - str(libpath), link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] - - scene = bpy.context.scene - - scene.collection.children.link(bpy.data.collections[lib_container]) - - animation_container = scene.collection.children[lib_container].make_local() - - meshes = [obj for obj in animation_container.objects if obj.type == 'MESH'] - armatures = [ - obj for obj in animation_container.objects if obj.type == 'ARMATURE'] - objects_list = [] - + # Get the armature of the rig + armatures = [obj for obj in objects if obj.type == 'ARMATURE'] assert(len(armatures) == 1) - # Link meshes first, then armatures. - # The armature is unparented for all the non-local meshes, - # when it is made local. - for obj in meshes + armatures: + self._remove(self, objects, lib_container) - obj = obj.make_local() - - obj.data.make_local() - - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): - - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() - - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": collection.name}) - objects_list.append(obj) - - animation_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + objects_list = self._process(self, str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -266,14 +243,8 @@ class BlendAnimationLoader(pype.blender.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - for obj in objects: - - if obj.type == 'ARMATURE': - bpy.data.armatures.remove(obj.data) - elif obj.type == 'MESH': - bpy.data.meshes.remove(obj.data) - - bpy.data.collections.remove(bpy.data.collections[lib_container]) + self._remove(self, objects, lib_container) + bpy.data.collections.remove(collection) return True diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 8ba8c5cfc8..10904a1f7b 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -31,43 +31,19 @@ class BlendModelLoader(pype.blender.AssetLoader): icon = "code-fork" color = "orange" - def process_asset( - self, context: dict, name: str, namespace: Optional[str] = None, - options: Optional[Dict] = None - ) -> Optional[List]: - """ - Arguments: - name: Use pre-defined name - namespace: Use pre-defined namespace - context: Full parenthood of representation to load - options: Additional settings dictionary - """ + @staticmethod + def _remove(self, objects, lib_container): + + for obj in objects: + + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + @staticmethod + def _process(self, libpath, lib_container, container_name): - libpath = self.fname - asset = context["asset"]["name"] - subset = context["subset"]["name"] - lib_container = pype.blender.plugin.asset_name(asset, subset) - container_name = pype.blender.plugin.asset_name( - asset, subset, namespace - ) relative = bpy.context.preferences.filepaths.use_relative_paths - - container = bpy.data.collections.new(lib_container) - container.name = container_name - avalon.blender.pipeline.containerise_existing( - container, - name, - namespace, - context, - self.__class__.__name__, - ) - - container_metadata = container.get( - avalon.blender.pipeline.AVALON_PROPERTY) - - container_metadata["libpath"] = libpath - container_metadata["lib_container"] = lib_container - with bpy.data.libraries.load( libpath, link=True, relative=relative ) as (_, data_to): @@ -102,13 +78,53 @@ class BlendModelLoader(pype.blender.AssetLoader): model_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + bpy.ops.object.select_all(action='DESELECT') + + return objects_list + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( + asset, subset, namespace + ) + + collection = bpy.data.collections.new(lib_container) + collection.name = container_name + avalon.blender.pipeline.containerise_existing( + collection, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + objects_list = self._process(self, libpath, lib_container, container_name) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list - bpy.ops.object.select_all(action='DESELECT') - - nodes = list(container.objects) - nodes.append(container) + nodes = list(collection.objects) + nodes.append(collection) self[:] = nodes return nodes @@ -154,8 +170,10 @@ class BlendModelLoader(pype.blender.AssetLoader): collection_metadata = collection.get( avalon.blender.pipeline.AVALON_PROPERTY) - collection_libpath = collection_metadata["libpath"] + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) @@ -171,54 +189,15 @@ class BlendModelLoader(pype.blender.AssetLoader): logger.info("Library already loaded, not updating...") return - for obj in collection_metadata["objects"]: + self._remove(self, objects, lib_container) - bpy.data.meshes.remove(obj.data) - - lib_container = collection_metadata["lib_container"] - - bpy.data.collections.remove(bpy.data.collections[lib_container]) - - relative = bpy.context.preferences.filepaths.use_relative_paths - with bpy.data.libraries.load( - str(libpath), link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] - - scene = bpy.context.scene - - scene.collection.children.link(bpy.data.collections[lib_container]) - - model_container = scene.collection.children[lib_container].make_local() - - objects_list = [] - - # Link meshes first, then armatures. - # The armature is unparented for all the non-local meshes, - # when it is made local. - for obj in model_container.objects: - - obj = obj.make_local() - - obj.data.make_local() - - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): - - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() - - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": collection.name}) - objects_list.append(obj) - - model_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + objects_list = self._process(self, str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list collection_metadata["libpath"] = str(libpath) collection_metadata["representation"] = str(representation["_id"]) - bpy.ops.object.select_all(action='DESELECT') - def remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. @@ -246,11 +225,8 @@ class BlendModelLoader(pype.blender.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - for obj in objects: + self._remove(self, objects, lib_container) - bpy.data.meshes.remove(obj.data) - - bpy.data.collections.remove(bpy.data.collections[lib_container]) bpy.data.collections.remove(collection) return True diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index c19717cd82..dcb70da6d8 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -30,6 +30,68 @@ class BlendRigLoader(pype.blender.AssetLoader): label = "Link Rig" icon = "code-fork" color = "orange" + + @staticmethod + def _remove(self, objects, lib_container): + + for obj in objects: + + if obj.type == 'ARMATURE': + bpy.data.armatures.remove(obj.data) + elif obj.type == 'MESH': + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + @staticmethod + def _process(self, libpath, lib_container, container_name, action): + + relative = bpy.context.preferences.filepaths.use_relative_paths + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + rig_container = scene.collection.children[lib_container].make_local() + + meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] + armatures = [obj for obj in rig_container.objects if obj.type == 'ARMATURE'] + + objects_list = [] + + assert(len(armatures) == 1) + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in meshes + armatures: + + obj = obj.make_local() + + obj.data.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + if obj.type == 'ARMATURE' and action is not None: + + obj.animation_data.action = action + + objects_list.append(obj) + + rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + + bpy.ops.object.select_all(action='DESELECT') + + return objects_list def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, @@ -50,7 +112,6 @@ class BlendRigLoader(pype.blender.AssetLoader): container_name = pype.blender.plugin.asset_name( asset, subset, namespace ) - relative = bpy.context.preferences.filepaths.use_relative_paths container = bpy.data.collections.new(lib_container) container.name = container_name @@ -68,48 +129,11 @@ class BlendRigLoader(pype.blender.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - with bpy.data.libraries.load( - libpath, link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] - - scene = bpy.context.scene - - scene.collection.children.link(bpy.data.collections[lib_container]) - - rig_container = scene.collection.children[lib_container].make_local() - - meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] - armatures = [ - obj for obj in rig_container.objects if obj.type == 'ARMATURE'] - - objects_list = [] - - # Link meshes first, then armatures. - # The armature is unparented for all the non-local meshes, - # when it is made local. - for obj in meshes + armatures: - - obj = obj.make_local() - - obj.data.make_local() - - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): - - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() - - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": container_name}) - - objects_list.append(obj) - - rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + objects_list = self._process(self, libpath, lib_container, container_name, None) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list - bpy.ops.object.select_all(action='DESELECT') - nodes = list(container.objects) nodes.append(container) self[:] = nodes @@ -159,8 +183,10 @@ class BlendRigLoader(pype.blender.AssetLoader): collection_metadata = collection.get( avalon.blender.pipeline.AVALON_PROPERTY) - collection_libpath = collection_metadata["libpath"] + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) @@ -177,64 +203,14 @@ class BlendRigLoader(pype.blender.AssetLoader): return # Get the armature of the rig - armatures = [obj for obj in collection_metadata["objects"] - if obj.type == 'ARMATURE'] + armatures = [obj for obj in objects if obj.type == 'ARMATURE'] assert(len(armatures) == 1) action = armatures[0].animation_data.action - for obj in collection_metadata["objects"]: + self._remove(self, objects, lib_container) - if obj.type == 'ARMATURE': - bpy.data.armatures.remove(obj.data) - elif obj.type == 'MESH': - bpy.data.meshes.remove(obj.data) - - lib_container = collection_metadata["lib_container"] - - bpy.data.collections.remove(bpy.data.collections[lib_container]) - - relative = bpy.context.preferences.filepaths.use_relative_paths - with bpy.data.libraries.load( - str(libpath), link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] - - scene = bpy.context.scene - - scene.collection.children.link(bpy.data.collections[lib_container]) - - rig_container = scene.collection.children[lib_container].make_local() - - meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] - armatures = [ - obj for obj in rig_container.objects if obj.type == 'ARMATURE'] - objects_list = [] - - assert(len(armatures) == 1) - - # Link meshes first, then armatures. - # The armature is unparented for all the non-local meshes, - # when it is made local. - for obj in meshes + armatures: - - obj = obj.make_local() - - obj.data.make_local() - - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): - - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() - - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": collection.name}) - objects_list.append(obj) - - if obj.type == 'ARMATURE' and action is not None: - - obj.animation_data.action = action - - rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + objects_list = self._process(self, str(libpath), lib_container, collection.name, action) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -271,14 +247,8 @@ class BlendRigLoader(pype.blender.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - for obj in objects: - - if obj.type == 'ARMATURE': - bpy.data.armatures.remove(obj.data) - elif obj.type == 'MESH': - bpy.data.meshes.remove(obj.data) - - bpy.data.collections.remove(bpy.data.collections[lib_container]) + self._remove(self, objects, lib_container) + bpy.data.collections.remove(collection) return True From 9a8655be1da5a8939a34cf66f71a036b13141fb2 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Sat, 14 Mar 2020 00:41:53 +0100 Subject: [PATCH 60/99] publishing unreal static mesh from maya --- pype/hooks/unreal/unreal_prelaunch.py | 2 +- .../global/publish/collect_scene_version.py | 3 + pype/plugins/global/publish/integrate_new.py | 3 +- .../maya/create/create_unreal_staticmesh.py | 11 ++ .../maya/publish/collect_unreal_staticmesh.py | 33 ++++ pype/plugins/maya/publish/extract_fbx.py | 5 +- .../validate_unreal_mesh_triangulated.py | 33 ++++ .../validate_unreal_staticmesh_naming.py | 120 ++++++++++++++ .../maya/publish/validate_unreal_up_axis.py | 25 +++ pype/plugins/unreal/create/create_fbx.py | 14 ++ .../plugins/unreal/load/load_staticmeshfbx.py | 53 ++++++ .../unreal/publish/collect_instances.py | 152 ++++++++++++++++++ pype/unreal/__init__.py | 45 ++++++ pype/unreal/plugin.py | 9 ++ 14 files changed, 503 insertions(+), 5 deletions(-) create mode 100644 pype/plugins/maya/create/create_unreal_staticmesh.py create mode 100644 pype/plugins/maya/publish/collect_unreal_staticmesh.py create mode 100644 pype/plugins/maya/publish/validate_unreal_mesh_triangulated.py create mode 100644 pype/plugins/maya/publish/validate_unreal_staticmesh_naming.py create mode 100644 pype/plugins/maya/publish/validate_unreal_up_axis.py create mode 100644 pype/plugins/unreal/create/create_fbx.py create mode 100644 pype/plugins/unreal/load/load_staticmeshfbx.py create mode 100644 pype/plugins/unreal/publish/collect_instances.py create mode 100644 pype/unreal/plugin.py diff --git a/pype/hooks/unreal/unreal_prelaunch.py b/pype/hooks/unreal/unreal_prelaunch.py index efb5d9157b..5b6b8e08e0 100644 --- a/pype/hooks/unreal/unreal_prelaunch.py +++ b/pype/hooks/unreal/unreal_prelaunch.py @@ -36,7 +36,7 @@ class UnrealPrelaunch(PypeHook): # Unreal is sensitive about project names longer then 20 chars if len(project_name) > 20: self.log.warning((f"Project name exceed 20 characters " - f"[ {project_name} ]!")) + f"({project_name})!")) # Unreal doesn't accept non alphabet characters at the start # of the project name. This is because project name is then used diff --git a/pype/plugins/global/publish/collect_scene_version.py b/pype/plugins/global/publish/collect_scene_version.py index 02e913199b..314a64f550 100644 --- a/pype/plugins/global/publish/collect_scene_version.py +++ b/pype/plugins/global/publish/collect_scene_version.py @@ -16,6 +16,9 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): if "standalonepublisher" in context.data.get("host", []): return + if "unreal" in context.data.get("host", []): + return + filename = os.path.basename(context.data.get('currentFile')) if '' in filename: diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 1d061af173..8935127e9e 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -80,7 +80,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "matchmove", "image" "source", - "assembly" + "assembly", + "fbx" ] exclude_families = ["clip"] db_representation_context_keys = [ diff --git a/pype/plugins/maya/create/create_unreal_staticmesh.py b/pype/plugins/maya/create/create_unreal_staticmesh.py new file mode 100644 index 0000000000..5a74cb22d5 --- /dev/null +++ b/pype/plugins/maya/create/create_unreal_staticmesh.py @@ -0,0 +1,11 @@ +import avalon.maya + + +class CreateUnrealStaticMesh(avalon.maya.Creator): + name = "staticMeshMain" + label = "Unreal - Static Mesh" + family = "unrealStaticMesh" + icon = "cube" + + def __init__(self, *args, **kwargs): + super(CreateUnrealStaticMesh, self).__init__(*args, **kwargs) diff --git a/pype/plugins/maya/publish/collect_unreal_staticmesh.py b/pype/plugins/maya/publish/collect_unreal_staticmesh.py new file mode 100644 index 0000000000..5ab9643f4b --- /dev/null +++ b/pype/plugins/maya/publish/collect_unreal_staticmesh.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from maya import cmds +import pyblish.api + + +class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): + """Collect unreal static mesh + + Ensures always only a single frame is extracted (current frame). This + also sets correct FBX options for later extraction. + + Note: + This is a workaround so that the `pype.model` family can use the + same pointcache extractor implementation as animation and pointcaches. + This always enforces the "current" frame to be published. + + """ + + order = pyblish.api.CollectorOrder + 0.2 + label = "Collect Model Data" + families = ["unrealStaticMesh"] + + def process(self, instance): + # add fbx family to trigger fbx extractor + instance.data["families"].append("fbx") + # set fbx overrides on instance + instance.data["smoothingGroups"] = True + instance.data["smoothMesh"] = True + instance.data["triangulate"] = True + + frame = cmds.currentTime(query=True) + instance.data["frameStart"] = frame + instance.data["frameEnd"] = frame diff --git a/pype/plugins/maya/publish/extract_fbx.py b/pype/plugins/maya/publish/extract_fbx.py index 01b58241c2..6a75bfce0e 100644 --- a/pype/plugins/maya/publish/extract_fbx.py +++ b/pype/plugins/maya/publish/extract_fbx.py @@ -212,12 +212,11 @@ class ExtractFBX(pype.api.Extractor): instance.data["representations"] = [] representation = { - 'name': 'mov', - 'ext': 'mov', + 'name': 'fbx', + 'ext': 'fbx', 'files': filename, "stagingDir": stagingDir, } instance.data["representations"].append(representation) - self.log.info("Extract FBX successful to: {0}".format(path)) diff --git a/pype/plugins/maya/publish/validate_unreal_mesh_triangulated.py b/pype/plugins/maya/publish/validate_unreal_mesh_triangulated.py new file mode 100644 index 0000000000..77f7144c4e --- /dev/null +++ b/pype/plugins/maya/publish/validate_unreal_mesh_triangulated.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +from maya import cmds +import pyblish.api +import pype.api + + +class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin): + """Validate if mesh is made of triangles for Unreal Engine""" + + order = pype.api.ValidateMeshOder + hosts = ["maya"] + families = ["unrealStaticMesh"] + category = "geometry" + label = "Mesh is Triangulated" + actions = [pype.maya.action.SelectInvalidAction] + + @classmethod + def get_invalid(cls, instance): + invalid = [] + meshes = cmds.ls(instance, type="mesh", long=True) + for mesh in meshes: + faces = cmds.polyEvaluate(mesh, f=True) + tris = cmds.polyEvaluate(mesh, t=True) + if faces != tris: + invalid.append(mesh) + + return invalid + + def process(self, instance): + invalid = self.get_invalid(instance) + assert len(invalid) == 0, ( + "Found meshes without triangles") diff --git a/pype/plugins/maya/publish/validate_unreal_staticmesh_naming.py b/pype/plugins/maya/publish/validate_unreal_staticmesh_naming.py new file mode 100644 index 0000000000..b62a855da9 --- /dev/null +++ b/pype/plugins/maya/publish/validate_unreal_staticmesh_naming.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- + +from maya import cmds +import pyblish.api +import pype.api +import pype.maya.action +import re + + +class ValidateUnrealStaticmeshName(pyblish.api.InstancePlugin): + """Validate name of Unreal Static Mesh + + Unreals naming convention states that staticMesh sould start with `SM` + prefix - SM_[Name]_## (Eg. SM_sube_01). This plugin also validates other + types of meshes - collision meshes: + + UBX_[RenderMeshName]_##: + Boxes are created with the Box objects type in + Max or with the Cube polygonal primitive in Maya. + You cannot move the vertices around or deform it + in any way to make it something other than a + rectangular prism, or else it will not work. + + UCP_[RenderMeshName]_##: + Capsules are created with the Capsule object type. + The capsule does not need to have many segments + (8 is a good number) at all because it is + converted into a true capsule for collision. Like + boxes, you should not move the individual + vertices around. + + USP_[RenderMeshName]_##: + Spheres are created with the Sphere object type. + The sphere does not need to have many segments + (8 is a good number) at all because it is + converted into a true sphere for collision. Like + boxes, you should not move the individual + vertices around. + + UCX_[RenderMeshName]_##: + Convex objects can be any completely closed + convex 3D shape. For example, a box can also be + a convex object + + This validator also checks if collision mesh [RenderMeshName] matches one + of SM_[RenderMeshName]. + + """ + optional = True + order = pype.api.ValidateContentsOrder + hosts = ["maya"] + families = ["unrealStaticMesh"] + label = "Unreal StaticMesh Name" + actions = [pype.maya.action.SelectInvalidAction] + regex_mesh = r"SM_(?P.*)_(\d{2})" + regex_collision = r"((UBX)|(UCP)|(USP)|(UCX))_(?P.*)_(\d{2})" + + @classmethod + def get_invalid(cls, instance): + + # find out if supplied transform is group or not + def is_group(groupName): + try: + children = cmds.listRelatives(groupName, children=True) + for child in children: + if not cmds.ls(child, transforms=True): + return False + return True + except Exception: + return False + + invalid = [] + content_instance = instance.data.get("setMembers", None) + if not content_instance: + cls.log.error("Instance has no nodes!") + return True + pass + descendants = cmds.listRelatives(content_instance, + allDescendents=True, + fullPath=True) or [] + + descendants = cmds.ls(descendants, noIntermediate=True, long=True) + trns = cmds.ls(descendants, long=False, type=('transform')) + + # filter out groups + filter = [node for node in trns if not is_group(node)] + + # compile regex for testing names + sm_r = re.compile(cls.regex_mesh) + cl_r = re.compile(cls.regex_collision) + + sm_names = [] + col_names = [] + for obj in filter: + sm_m = sm_r.match(obj) + if sm_m is None: + # test if it matches collision mesh + cl_r = sm_r.match(obj) + if cl_r is None: + cls.log.error("invalid mesh name on: {}".format(obj)) + invalid.append(obj) + else: + col_names.append((cl_r.group("renderName"), obj)) + else: + sm_names.append(sm_m.group("renderName")) + + for c_mesh in col_names: + if c_mesh[0] not in sm_names: + cls.log.error(("collision name {} doesn't match any " + "static mesh names.").format(obj)) + invalid.append(c_mesh[1]) + + return invalid + + def process(self, instance): + + invalid = self.get_invalid(instance) + + if invalid: + raise RuntimeError("Model naming is invalid. See log.") diff --git a/pype/plugins/maya/publish/validate_unreal_up_axis.py b/pype/plugins/maya/publish/validate_unreal_up_axis.py new file mode 100644 index 0000000000..6641edb4a5 --- /dev/null +++ b/pype/plugins/maya/publish/validate_unreal_up_axis.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +from maya import cmds +import pyblish.api +import pype.api + + +class ValidateUnrealUpAxis(pyblish.api.ContextPlugin): + """Validate if Z is set as up axis in Maya""" + + optional = True + order = pype.api.ValidateContentsOrder + hosts = ["maya"] + families = ["unrealStaticMesh"] + label = "Unreal Up-Axis check" + actions = [pype.api.RepairAction] + + def process(self, context): + assert cmds.upAxis(q=True, axis=True) == "z", ( + "Invalid axis set as up axis" + ) + + @classmethod + def repair(cls, instance): + cmds.upAxis(axis="z", rotateView=True) diff --git a/pype/plugins/unreal/create/create_fbx.py b/pype/plugins/unreal/create/create_fbx.py new file mode 100644 index 0000000000..0d5b0bf316 --- /dev/null +++ b/pype/plugins/unreal/create/create_fbx.py @@ -0,0 +1,14 @@ +from pype.unreal.plugin import Creator + + +class CreateFbx(Creator): + """Static FBX geometry""" + + name = "modelMain" + label = "Model" + family = "model" + icon = "cube" + asset_types = ["StaticMesh"] + + def __init__(self, *args, **kwargs): + super(CreateFbx, self).__init__(*args, **kwargs) diff --git a/pype/plugins/unreal/load/load_staticmeshfbx.py b/pype/plugins/unreal/load/load_staticmeshfbx.py new file mode 100644 index 0000000000..056c81d54d --- /dev/null +++ b/pype/plugins/unreal/load/load_staticmeshfbx.py @@ -0,0 +1,53 @@ +from avalon import api +from avalon import unreal as avalon_unreal +import unreal +import time + + +class StaticMeshFBXLoader(api.Loader): + """Load Unreal StaticMesh from FBX""" + + families = ["unrealStaticMesh"] + label = "Import FBX Static Mesh" + representations = ["fbx"] + icon = "cube" + color = "orange" + + def load(self, context, name, namespace, data): + + tools = unreal.AssetToolsHelpers().get_asset_tools() + temp_dir, temp_name = tools.create_unique_asset_name( + "/Game/{}".format(name), "_TMP" + ) + + # asset_path = "/Game/{}".format(namespace) + unreal.EditorAssetLibrary.make_directory(temp_dir) + + task = unreal.AssetImportTask() + + task.filename = self.fname + task.destination_path = temp_dir + task.destination_name = name + task.replace_existing = False + task.automated = True + task.save = True + + # set import options here + task.options = unreal.FbxImportUI() + task.options.import_animations = False + + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + + imported_assets = unreal.EditorAssetLibrary.list_assets( + temp_dir, recursive=True, include_folder=True + ) + new_dir = avalon_unreal.containerise( + name, namespace, imported_assets, context, self.__class__.__name__) + + asset_content = unreal.EditorAssetLibrary.list_assets( + new_dir, recursive=True, include_folder=True + ) + + unreal.EditorAssetLibrary.delete_directory(temp_dir) + + return asset_content diff --git a/pype/plugins/unreal/publish/collect_instances.py b/pype/plugins/unreal/publish/collect_instances.py new file mode 100644 index 0000000000..fa604f79d3 --- /dev/null +++ b/pype/plugins/unreal/publish/collect_instances.py @@ -0,0 +1,152 @@ +import unreal + +import pyblish.api + + +class CollectInstances(pyblish.api.ContextPlugin): + """Gather instances by objectSet and pre-defined attribute + + This collector takes into account assets that are associated with + an objectSet and marked with a unique identifier; + + Identifier: + id (str): "pyblish.avalon.instance" + + Limitations: + - Does not take into account nodes connected to those + within an objectSet. Extractors are assumed to export + with history preserved, but this limits what they will + be able to achieve and the amount of data available + to validators. An additional collector could also + append this input data into the instance, as we do + for `pype.rig` with collect_history. + + """ + + label = "Collect Instances" + order = pyblish.api.CollectorOrder + hosts = ["unreal"] + + def process(self, context): + + objectset = cmds.ls("*.id", long=True, type="objectSet", + recursive=True, objectsOnly=True) + + context.data['objectsets'] = objectset + for objset in objectset: + + if not cmds.attributeQuery("id", node=objset, exists=True): + continue + + id_attr = "{}.id".format(objset) + if cmds.getAttr(id_attr) != "pyblish.avalon.instance": + continue + + # The developer is responsible for specifying + # the family of each instance. + has_family = cmds.attributeQuery("family", + node=objset, + exists=True) + assert has_family, "\"%s\" was missing a family" % objset + + members = cmds.sets(objset, query=True) + if members is None: + self.log.warning("Skipped empty instance: \"%s\" " % objset) + continue + + self.log.info("Creating instance for {}".format(objset)) + + data = dict() + + # Apply each user defined attribute as data + for attr in cmds.listAttr(objset, userDefined=True) or list(): + try: + value = cmds.getAttr("%s.%s" % (objset, attr)) + except Exception: + # Some attributes cannot be read directly, + # such as mesh and color attributes. These + # are considered non-essential to this + # particular publishing pipeline. + value = None + data[attr] = value + + # temporarily translation of `active` to `publish` till issue has + # been resolved, https://github.com/pyblish/pyblish-base/issues/307 + if "active" in data: + data["publish"] = data["active"] + + # Collect members + members = cmds.ls(members, long=True) or [] + + # `maya.cmds.listRelatives(noIntermediate=True)` only works when + # `shapes=True` argument is passed, since we also want to include + # transforms we filter afterwards. + children = cmds.listRelatives(members, + allDescendents=True, + fullPath=True) or [] + children = cmds.ls(children, noIntermediate=True, long=True) + + parents = [] + if data.get("includeParentHierarchy", True): + # If `includeParentHierarchy` then include the parents + # so they will also be picked up in the instance by validators + parents = self.get_all_parents(members) + members_hierarchy = list(set(members + children + parents)) + + if 'families' not in data: + data['families'] = [data.get('family')] + + # Create the instance + instance = context.create_instance(objset) + instance[:] = members_hierarchy + + # Store the exact members of the object set + instance.data["setMembers"] = members + + + # Define nice label + name = cmds.ls(objset, long=False)[0] # use short name + label = "{0} ({1})".format(name, + data["asset"]) + + # Append start frame and end frame to label if present + if "frameStart" and "frameEnd" in data: + label += " [{0}-{1}]".format(int(data["frameStart"]), + int(data["frameEnd"])) + + instance.data["label"] = label + + instance.data.update(data) + + # Produce diagnostic message for any graphical + # user interface interested in visualising it. + self.log.info("Found: \"%s\" " % instance.data["name"]) + self.log.debug("DATA: \"%s\" " % instance.data) + + + def sort_by_family(instance): + """Sort by family""" + return instance.data.get("families", instance.data.get("family")) + + # Sort/grouped by family (preserving local index) + context[:] = sorted(context, key=sort_by_family) + + return context + + def get_all_parents(self, nodes): + """Get all parents by using string operations (optimization) + + Args: + nodes (list): the nodes which are found in the objectSet + + Returns: + list + """ + + parents = [] + for node in nodes: + splitted = node.split("|") + items = ["|".join(splitted[0:i]) for i in range(2, len(splitted))] + parents.extend(items) + + return list(set(parents)) diff --git a/pype/unreal/__init__.py b/pype/unreal/__init__.py index e69de29bb2..bb8a765a43 100644 --- a/pype/unreal/__init__.py +++ b/pype/unreal/__init__.py @@ -0,0 +1,45 @@ +import os +import logging + +from avalon import api as avalon +from pyblish import api as pyblish + +logger = logging.getLogger("pype.unreal") + +PARENT_DIR = os.path.dirname(__file__) +PACKAGE_DIR = os.path.dirname(PARENT_DIR) +PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") + +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "unreal", "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "unreal", "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "unreal", "create") + + +def install(): + """Install Unreal configuration for Avalon.""" + print("-=" * 40) + logo = '''. +. + ____________ + / \\ __ \\ + \\ \\ \\/_\\ \\ + \\ \\ _____/ ______ + \\ \\ \\___// \\ \\ + \\ \\____\\ \\ \\_____\\ + \\/_____/ \\/______/ PYPE Club . +. +''' + print(logo) + print("installing Pype for Unreal ...") + print("-=" * 40) + logger.info("installing Pype for Unreal") + pyblish.register_plugin_path(str(PUBLISH_PATH)) + avalon.register_plugin_path(avalon.Loader, str(LOAD_PATH)) + avalon.register_plugin_path(avalon.Creator, str(CREATE_PATH)) + + +def uninstall(): + """Uninstall Unreal configuration for Avalon.""" + pyblish.deregister_plugin_path(str(PUBLISH_PATH)) + avalon.deregister_plugin_path(avalon.Loader, str(LOAD_PATH)) + avalon.deregister_plugin_path(avalon.Creator, str(CREATE_PATH)) diff --git a/pype/unreal/plugin.py b/pype/unreal/plugin.py new file mode 100644 index 0000000000..d403417ad1 --- /dev/null +++ b/pype/unreal/plugin.py @@ -0,0 +1,9 @@ +from avalon import api + + +class Creator(api.Creator): + pass + + +class Loader(api.Loader): + pass From ada2cc0f4e9b6a351c9cb9ae6b1d56ed1d37e631 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 16 Mar 2020 21:43:47 +0100 Subject: [PATCH 61/99] base of unreal staticMesh pipeline --- .../global/publish/collect_scene_version.py | 2 +- pype/plugins/unreal/create/create_fbx.py | 14 -- .../unreal/create/create_staticmeshfbx.py | 33 ++++ .../plugins/unreal/load/load_staticmeshfbx.py | 52 +++++- .../unreal/publish/collect_instances.py | 149 ++++-------------- 5 files changed, 112 insertions(+), 138 deletions(-) delete mode 100644 pype/plugins/unreal/create/create_fbx.py create mode 100644 pype/plugins/unreal/create/create_staticmeshfbx.py diff --git a/pype/plugins/global/publish/collect_scene_version.py b/pype/plugins/global/publish/collect_scene_version.py index 314a64f550..8c2bacf6e1 100644 --- a/pype/plugins/global/publish/collect_scene_version.py +++ b/pype/plugins/global/publish/collect_scene_version.py @@ -16,7 +16,7 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): if "standalonepublisher" in context.data.get("host", []): return - if "unreal" in context.data.get("host", []): + if "unreal" in pyblish.api.registered_hosts(): return filename = os.path.basename(context.data.get('currentFile')) diff --git a/pype/plugins/unreal/create/create_fbx.py b/pype/plugins/unreal/create/create_fbx.py deleted file mode 100644 index 0d5b0bf316..0000000000 --- a/pype/plugins/unreal/create/create_fbx.py +++ /dev/null @@ -1,14 +0,0 @@ -from pype.unreal.plugin import Creator - - -class CreateFbx(Creator): - """Static FBX geometry""" - - name = "modelMain" - label = "Model" - family = "model" - icon = "cube" - asset_types = ["StaticMesh"] - - def __init__(self, *args, **kwargs): - super(CreateFbx, self).__init__(*args, **kwargs) diff --git a/pype/plugins/unreal/create/create_staticmeshfbx.py b/pype/plugins/unreal/create/create_staticmeshfbx.py new file mode 100644 index 0000000000..8002299f0a --- /dev/null +++ b/pype/plugins/unreal/create/create_staticmeshfbx.py @@ -0,0 +1,33 @@ +import unreal +from pype.unreal.plugin import Creator +from avalon.unreal import ( + instantiate, +) + + +class CreateStaticMeshFBX(Creator): + """Static FBX geometry""" + + name = "unrealStaticMeshMain" + label = "Unreal - Static Mesh" + family = "unrealStaticMesh" + icon = "cube" + asset_types = ["StaticMesh"] + + root = "/Game" + suffix = "_INS" + + def __init__(self, *args, **kwargs): + super(CreateStaticMeshFBX, self).__init__(*args, **kwargs) + + def process(self): + + name = self.data["subset"] + + selection = [] + if (self.options or {}).get("useSelection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] + + unreal.log("selection: {}".format(selection)) + instantiate(self.root, name, self.data, selection, self.suffix) diff --git a/pype/plugins/unreal/load/load_staticmeshfbx.py b/pype/plugins/unreal/load/load_staticmeshfbx.py index 056c81d54d..61e765f7c2 100644 --- a/pype/plugins/unreal/load/load_staticmeshfbx.py +++ b/pype/plugins/unreal/load/load_staticmeshfbx.py @@ -1,7 +1,6 @@ from avalon import api from avalon import unreal as avalon_unreal import unreal -import time class StaticMeshFBXLoader(api.Loader): @@ -14,13 +13,33 @@ class StaticMeshFBXLoader(api.Loader): color = "orange" def load(self, context, name, namespace, data): + """ + Load and containerise representation into Content Browser. + + This is two step process. First, import FBX to temporary path and + then call `containerise()` on it - this moves all content to new + directory and then it will create AssetContainer there and imprint it + with metadata. This will mark this path as container. + + Args: + context (dict): application context + name (str): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + data (dict): Those would be data to be imprinted. This is not used + now, data are imprinted by `containerise()`. + + Returns: + list(str): list of container content + """ tools = unreal.AssetToolsHelpers().get_asset_tools() temp_dir, temp_name = tools.create_unique_asset_name( "/Game/{}".format(name), "_TMP" ) - # asset_path = "/Game/{}".format(namespace) unreal.EditorAssetLibrary.make_directory(temp_dir) task = unreal.AssetImportTask() @@ -51,3 +70,32 @@ class StaticMeshFBXLoader(api.Loader): unreal.EditorAssetLibrary.delete_directory(temp_dir) return asset_content + + def update(self, container, representation): + node = container["objectName"] + source_path = api.get_representation_path(representation) + destination_path = container["namespace"] + + task = unreal.AssetImportTask() + + task.filename = source_path + task.destination_path = destination_path + # strip suffix + task.destination_name = node[:-4] + task.replace_existing = True + task.automated = True + task.save = True + + task.options = unreal.FbxImportUI() + task.options.import_animations = False + + # do import fbx and replace existing data + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + container_path = "{}/{}".format(container["namespace"], + container["objectName"]) + # update metadata + avalon_unreal.imprint( + container_path, {"_id": str(representation["_id"])}) + + def remove(self, container): + unreal.EditorAssetLibrary.delete_directory(container["namespace"]) diff --git a/pype/plugins/unreal/publish/collect_instances.py b/pype/plugins/unreal/publish/collect_instances.py index fa604f79d3..766a73028c 100644 --- a/pype/plugins/unreal/publish/collect_instances.py +++ b/pype/plugins/unreal/publish/collect_instances.py @@ -4,23 +4,14 @@ import pyblish.api class CollectInstances(pyblish.api.ContextPlugin): - """Gather instances by objectSet and pre-defined attribute + """Gather instances by AvalonPublishInstance class - This collector takes into account assets that are associated with - an objectSet and marked with a unique identifier; + This collector finds all paths containing `AvalonPublishInstance` class + asset Identifier: id (str): "pyblish.avalon.instance" - Limitations: - - Does not take into account nodes connected to those - within an objectSet. Extractors are assumed to export - with history preserved, but this limits what they will - be able to achieve and the amount of data available - to validators. An additional collector could also - append this input data into the instance, as we do - for `pype.rig` with collect_history. - """ label = "Collect Instances" @@ -29,124 +20,40 @@ class CollectInstances(pyblish.api.ContextPlugin): def process(self, context): - objectset = cmds.ls("*.id", long=True, type="objectSet", - recursive=True, objectsOnly=True) + ar = unreal.AssetRegistryHelpers.get_asset_registry() + instance_containers = ar.get_assets_by_class( + "AvalonPublishInstance", True) - context.data['objectsets'] = objectset - for objset in objectset: + for container_data in instance_containers: + asset = container_data.get_asset() + data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) + data["objectName"] = container_data.asset_name + # convert to strings + data = {str(key): str(value) for (key, value) in data.items()} + assert data.get("family"), ( + "instance has no family" + ) - if not cmds.attributeQuery("id", node=objset, exists=True): - continue + # content of container + members = unreal.EditorAssetLibrary.list_assets( + asset.get_path_name(), recursive=True, include_folder=True + ) + self.log.debug(members) + self.log.debug(asset.get_path_name()) + # remove instance container + members.remove(asset.get_path_name()) + self.log.info("Creating instance for {}".format(asset.get_name())) - id_attr = "{}.id".format(objset) - if cmds.getAttr(id_attr) != "pyblish.avalon.instance": - continue - - # The developer is responsible for specifying - # the family of each instance. - has_family = cmds.attributeQuery("family", - node=objset, - exists=True) - assert has_family, "\"%s\" was missing a family" % objset - - members = cmds.sets(objset, query=True) - if members is None: - self.log.warning("Skipped empty instance: \"%s\" " % objset) - continue - - self.log.info("Creating instance for {}".format(objset)) - - data = dict() - - # Apply each user defined attribute as data - for attr in cmds.listAttr(objset, userDefined=True) or list(): - try: - value = cmds.getAttr("%s.%s" % (objset, attr)) - except Exception: - # Some attributes cannot be read directly, - # such as mesh and color attributes. These - # are considered non-essential to this - # particular publishing pipeline. - value = None - data[attr] = value - - # temporarily translation of `active` to `publish` till issue has - # been resolved, https://github.com/pyblish/pyblish-base/issues/307 - if "active" in data: - data["publish"] = data["active"] - - # Collect members - members = cmds.ls(members, long=True) or [] - - # `maya.cmds.listRelatives(noIntermediate=True)` only works when - # `shapes=True` argument is passed, since we also want to include - # transforms we filter afterwards. - children = cmds.listRelatives(members, - allDescendents=True, - fullPath=True) or [] - children = cmds.ls(children, noIntermediate=True, long=True) - - parents = [] - if data.get("includeParentHierarchy", True): - # If `includeParentHierarchy` then include the parents - # so they will also be picked up in the instance by validators - parents = self.get_all_parents(members) - members_hierarchy = list(set(members + children + parents)) - - if 'families' not in data: - data['families'] = [data.get('family')] - - # Create the instance - instance = context.create_instance(objset) - instance[:] = members_hierarchy + instance = context.create_instance(asset.get_name()) + instance[:] = members # Store the exact members of the object set instance.data["setMembers"] = members + instance.data["families"] = [data.get("family")] - - # Define nice label - name = cmds.ls(objset, long=False)[0] # use short name - label = "{0} ({1})".format(name, + label = "{0} ({1})".format(asset.get_name()[:-4], data["asset"]) - # Append start frame and end frame to label if present - if "frameStart" and "frameEnd" in data: - label += " [{0}-{1}]".format(int(data["frameStart"]), - int(data["frameEnd"])) - instance.data["label"] = label instance.data.update(data) - - # Produce diagnostic message for any graphical - # user interface interested in visualising it. - self.log.info("Found: \"%s\" " % instance.data["name"]) - self.log.debug("DATA: \"%s\" " % instance.data) - - - def sort_by_family(instance): - """Sort by family""" - return instance.data.get("families", instance.data.get("family")) - - # Sort/grouped by family (preserving local index) - context[:] = sorted(context, key=sort_by_family) - - return context - - def get_all_parents(self, nodes): - """Get all parents by using string operations (optimization) - - Args: - nodes (list): the nodes which are found in the objectSet - - Returns: - list - """ - - parents = [] - for node in nodes: - splitted = node.split("|") - items = ["|".join(splitted[0:i]) for i in range(2, len(splitted))] - parents.extend(items) - - return list(set(parents)) From a1a27784ae85f5c7da925059c4ff3f0b4ef66429 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 17 Mar 2020 00:01:35 +0100 Subject: [PATCH 62/99] updated docs --- pype/unreal/lib.py | 12 +++++++++++- pype/unreal/plugin.py | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py index be6314b09b..faf3a6e99f 100644 --- a/pype/unreal/lib.py +++ b/pype/unreal/lib.py @@ -13,7 +13,17 @@ def get_engine_versions(): Location can be overridden by `UNREAL_ENGINE_LOCATION` environment variable. - Returns dictionary with version as a key and dir as value. + Returns: + + dict: dictionary with version as a key and dir as value. + + Example: + + >>> get_engine_version() + { + "4.23": "C:/Epic Games/UE_4.23", + "4.24": "C:/Epic Games/UE_4.24" + } """ try: engine_locations = {} diff --git a/pype/unreal/plugin.py b/pype/unreal/plugin.py index d403417ad1..0c00eb77d6 100644 --- a/pype/unreal/plugin.py +++ b/pype/unreal/plugin.py @@ -2,8 +2,10 @@ from avalon import api class Creator(api.Creator): + """This serves as skeleton for future Pype specific functionality""" pass class Loader(api.Loader): + """This serves as skeleton for future Pype specific functionality""" pass From 3bf653b006e336734cfecb421964cc2226e61717 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Mar 2020 11:25:13 +0100 Subject: [PATCH 63/99] fixed bug with cpp project setup --- pype/unreal/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py index faf3a6e99f..8f87fdbf4e 100644 --- a/pype/unreal/lib.py +++ b/pype/unreal/lib.py @@ -257,7 +257,7 @@ def create_unreal_project(project_name: str, "pip", "install", "pyside"]) if dev_mode or preset["dev_mode"]: - _prepare_cpp_project(pr_dir, engine_path) + _prepare_cpp_project(project_file, engine_path) def _prepare_cpp_project(project_file: str, engine_path: str) -> None: From fcc26cd9f7d36d823e42656fa6f2a9952c8efdf3 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 18 Mar 2020 21:47:05 +0100 Subject: [PATCH 64/99] add all frame data to context and fix order --- .../global/publish/collect_avalon_entities.py | 11 ++++++++++- .../global/publish/collect_rendered_files.py | 2 +- pype/plugins/maya/publish/collect_scene.py | 14 ++++++++------ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/pype/plugins/global/publish/collect_avalon_entities.py b/pype/plugins/global/publish/collect_avalon_entities.py index 103f5abd1a..53f11aa693 100644 --- a/pype/plugins/global/publish/collect_avalon_entities.py +++ b/pype/plugins/global/publish/collect_avalon_entities.py @@ -15,7 +15,7 @@ import pyblish.api class CollectAvalonEntities(pyblish.api.ContextPlugin): """Collect Anatomy into Context""" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder - 0.02 label = "Collect Avalon Entities" def process(self, context): @@ -47,7 +47,16 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin): context.data["assetEntity"] = asset_entity data = asset_entity['data'] + + context.data["frameStart"] = data.get("frameStart") + context.data["frameEnd"] = data.get("frameEnd") + handles = int(data.get("handles") or 0) context.data["handles"] = handles context.data["handleStart"] = int(data.get("handleStart", handles)) context.data["handleEnd"] = int(data.get("handleEnd", handles)) + + frame_start_h = data.get("frameStart") - context.data["handleStart"] + frame_end_h = data.get("frameEnd") + context.data["handleEnd"] + context.data["frameStartHandle"] = frame_start_h + context.data["frameEndHandle"] = frame_end_h diff --git a/pype/plugins/global/publish/collect_rendered_files.py b/pype/plugins/global/publish/collect_rendered_files.py index 552fd49f6d..8ecf7ba156 100644 --- a/pype/plugins/global/publish/collect_rendered_files.py +++ b/pype/plugins/global/publish/collect_rendered_files.py @@ -13,7 +13,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): `PYPE_PUBLISH_DATA`. Those files _MUST_ share same context. """ - order = pyblish.api.CollectorOrder - 0.0001 + order = pyblish.api.CollectorOrder - 0.1 targets = ["filesequence"] label = "Collect rendered frames" diff --git a/pype/plugins/maya/publish/collect_scene.py b/pype/plugins/maya/publish/collect_scene.py index 089019f2d3..e6976356e8 100644 --- a/pype/plugins/maya/publish/collect_scene.py +++ b/pype/plugins/maya/publish/collect_scene.py @@ -9,13 +9,14 @@ from pype.maya import lib class CollectMayaScene(pyblish.api.ContextPlugin): """Inject the current working file into context""" - order = pyblish.api.CollectorOrder - 0.1 + order = pyblish.api.CollectorOrder - 0.01 label = "Maya Workfile" hosts = ['maya'] def process(self, context): """Inject the current working file""" - current_file = context.data['currentFile'] + current_file = cmds.file(query=True, sceneName=True) + context.data['currentFile'] = current_file folder, file = os.path.split(current_file) filename, ext = os.path.splitext(file) @@ -24,9 +25,6 @@ class CollectMayaScene(pyblish.api.ContextPlugin): data = {} - for key, value in lib.collect_animation_data().items(): - data[key] = value - # create instance instance = context.create_instance(name=filename) subset = 'workfile' + task.capitalize() @@ -38,7 +36,11 @@ class CollectMayaScene(pyblish.api.ContextPlugin): "publish": True, "family": 'workfile', "families": ['workfile'], - "setMembers": [current_file] + "setMembers": [current_file], + "frameStart": context.data['frameStart'], + "frameEnd": context.data['frameEnd'], + "handleStart": context.data['handleStart'], + "handleEnd": context.data['handleEnd'] }) data['representations'] = [{ From 4778dbd3e6aa48d11c653f429e87018bdd55d1e4 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 18 Mar 2020 21:47:37 +0100 Subject: [PATCH 65/99] remove handles from custom pointcache range --- pype/plugins/maya/publish/extract_pointcache.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pype/plugins/maya/publish/extract_pointcache.py b/pype/plugins/maya/publish/extract_pointcache.py index cec4886712..e40ab6e7da 100644 --- a/pype/plugins/maya/publish/extract_pointcache.py +++ b/pype/plugins/maya/publish/extract_pointcache.py @@ -25,12 +25,8 @@ class ExtractAlembic(pype.api.Extractor): nodes = instance[:] # Collect the start and end including handles - start = instance.data.get("frameStart", 1) - end = instance.data.get("frameEnd", 1) - handles = instance.data.get("handles", 0) - if handles: - start -= handles - end += handles + start = float(instance.data.get("frameStartHandle", 1)) + end = float(instance.data.get("frameEndHandle", 1)) attrs = instance.data.get("attr", "").split(";") attrs = [value for value in attrs if value.strip()] From b329fde9835df94a56def669c2bb60ceb913f473 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 18 Mar 2020 21:48:14 +0100 Subject: [PATCH 66/99] use custom frame range if other than asset --- .../plugins/maya/publish/collect_instances.py | 44 ++++++++++++++++--- pype/plugins/maya/publish/collect_render.py | 41 ++++++++++++----- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/pype/plugins/maya/publish/collect_instances.py b/pype/plugins/maya/publish/collect_instances.py index 5af717ba4d..9ea3ebe7fa 100644 --- a/pype/plugins/maya/publish/collect_instances.py +++ b/pype/plugins/maya/publish/collect_instances.py @@ -1,6 +1,7 @@ from maya import cmds import pyblish.api +import json class CollectInstances(pyblish.api.ContextPlugin): @@ -32,6 +33,13 @@ class CollectInstances(pyblish.api.ContextPlugin): objectset = cmds.ls("*.id", long=True, type="objectSet", recursive=True, objectsOnly=True) + ctx_frame_start = context.data['frameStart'] + ctx_frame_end = context.data['frameEnd'] + ctx_handle_start = context.data['handleStart'] + ctx_handle_end = context.data['handleEnd'] + ctx_frame_start_handle = context.data['frameStartHandle'] + ctx_frame_end_handle = context.data['frameEndHandle'] + context.data['objectsets'] = objectset for objset in objectset: @@ -108,14 +116,36 @@ class CollectInstances(pyblish.api.ContextPlugin): label = "{0} ({1})".format(name, data["asset"]) - if "handles" in data: - data["handleStart"] = data["handles"] - data["handleEnd"] = data["handles"] - # Append start frame and end frame to label if present if "frameStart" and "frameEnd" in data: - data["frameStartHandle"] = data["frameStart"] - data["handleStart"] - data["frameEndHandle"] = data["frameEnd"] + data["handleEnd"] + + # if frame range on maya set is the same as full shot range + # adjust the values to match the asset data + if (ctx_frame_start_handle == data["frameStart"] + and ctx_frame_end_handle == data["frameEnd"]): + data["frameStartHandle"] = ctx_frame_start_handle + data["frameEndHandle"] = ctx_frame_end_handle + data["frameStart"] = ctx_frame_start + data["frameEnd"] = ctx_frame_end + data["handleStart"] = ctx_handle_start + data["handleEnd"] = ctx_handle_end + + # if there are user values on start and end frame not matching + # the asset, use them + + else: + if "handles" in data: + data["handleStart"] = data["handles"] + data["handleEnd"] = data["handles"] + else: + data["handleStart"] = 0 + data["handleEnd"] = 0 + + data["frameStartHandle"] = data["frameStart"] - data["handleStart"] + data["frameEndHandle"] = data["frameEnd"] + data["handleEnd"] + + if "handles" in data: + data.pop('handles') label += " [{0}-{1}]".format(int(data["frameStartHandle"]), int(data["frameEndHandle"])) @@ -127,7 +157,7 @@ class CollectInstances(pyblish.api.ContextPlugin): # Produce diagnostic message for any graphical # user interface interested in visualising it. self.log.info("Found: \"%s\" " % instance.data["name"]) - self.log.debug("DATA: \"%s\" " % instance.data) + self.log.debug("DATA: {} ".format(json.dumps(instance.data, indent=4))) def sort_by_family(instance): """Sort by family""" diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index be3878e6bd..88c1be477d 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -41,6 +41,7 @@ import re import os import types import six +import json from abc import ABCMeta, abstractmethod from maya import cmds @@ -202,6 +203,28 @@ class CollectMayaRender(pyblish.api.ContextPlugin): full_paths.append(full_path) aov_dict["beauty"] = full_paths + frame_start_render = int(self.get_render_attribute("startFrame", + layer=layer_name)) + frame_end_render = int(self.get_render_attribute("endFrame", + layer=layer_name)) + + if (int(context.data['frameStartHandle']) == frame_start_render and + int(context.data['frameEndHandle']) == frame_end_render): + + handle_start = context.data['handleStart'] + handle_end = context.data['handleEnd'] + frame_start = context.data['frameStart'] + frame_end = context.data['frameEnd'] + frame_start_handle = context.data['frameStartHandle'] + frame_end_handle = context.data['frameEndHandle'] + else: + handle_start = 0 + handle_end = 0 + frame_start = frame_start_render + frame_end = frame_end_render + frame_start_handle = frame_start_render + frame_end_handle = frame_end_render + full_exp_files.append(aov_dict) self.log.info(full_exp_files) self.log.info("collecting layer: {}".format(layer_name)) @@ -211,20 +234,18 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "attachTo": attachTo, "setMembers": layer_name, "publish": True, - "frameStart": int(context.data["assetEntity"]['data']['frameStart']), - "frameEnd": int(context.data["assetEntity"]['data']['frameEnd']), - "frameStartHandle": int(self.get_render_attribute("startFrame", - layer=layer_name)), - "frameEndHandle": int(self.get_render_attribute("endFrame", - layer=layer_name)), + + "handleStart": handle_start, + "handleEnd": handle_end, + "frameStart": frame_start, + "frameEnd": frame_end, + "frameStartHandle": frame_start_handle, + "frameEndHandle": frame_end_handle, "byFrameStep": int( self.get_render_attribute("byFrameStep", layer=layer_name)), "renderer": self.get_render_attribute("currentRenderer", layer=layer_name), - "handleStart": int(context.data["assetEntity"]['data']['handleStart']), - "handleEnd": int(context.data["assetEntity"]['data']['handleEnd']), - # instance subset "family": "renderlayer", "families": ["renderlayer"], @@ -267,7 +288,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): instance = context.create_instance(expected_layer_name) instance.data["label"] = label instance.data.update(data) - pass + self.log.debug("data: {}".format(json.dumps(data, indent=4))) def parse_options(self, render_globals): """Get all overrides with a value, skip those without From 5649b112f1772e6acf8544829b380e48aca29268 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 18 Mar 2020 21:48:34 +0100 Subject: [PATCH 67/99] remove unnecessary current scene collector --- .../plugins/maya/publish/collect_current_file.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 pype/plugins/maya/publish/collect_current_file.py diff --git a/pype/plugins/maya/publish/collect_current_file.py b/pype/plugins/maya/publish/collect_current_file.py deleted file mode 100644 index 0b38ebcf3d..0000000000 --- a/pype/plugins/maya/publish/collect_current_file.py +++ /dev/null @@ -1,16 +0,0 @@ -from maya import cmds - -import pyblish.api - - -class CollectMayaCurrentFile(pyblish.api.ContextPlugin): - """Inject the current working file into context""" - - order = pyblish.api.CollectorOrder - 0.5 - label = "Maya Current File" - hosts = ['maya'] - - def process(self, context): - """Inject the current working file""" - current_file = cmds.file(query=True, sceneName=True) - context.data['currentFile'] = current_file From 05441eb2fe3df8b44e465118d6ef319ba774d535 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 19 Mar 2020 18:07:36 +0100 Subject: [PATCH 68/99] fix imports --- pype/plugins/blender/create/create_action.py | 2 ++ .../blender/create/create_animation.py | 2 ++ pype/plugins/blender/create/create_model.py | 3 +- pype/plugins/blender/create/create_rig.py | 11 ++++--- pype/plugins/blender/load/load_action.py | 29 +++++++++-------- pype/plugins/blender/load/load_animation.py | 26 ++++++++-------- pype/plugins/blender/load/load_model.py | 25 +++++++-------- pype/plugins/blender/load/load_rig.py | 31 +++++++++---------- 8 files changed, 65 insertions(+), 64 deletions(-) diff --git a/pype/plugins/blender/create/create_action.py b/pype/plugins/blender/create/create_action.py index 88ecebdfff..64dfe9ff90 100644 --- a/pype/plugins/blender/create/create_action.py +++ b/pype/plugins/blender/create/create_action.py @@ -4,6 +4,8 @@ import bpy from avalon import api from avalon.blender import Creator, lib +import pype.blender.plugin + class CreateAction(Creator): diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py index 14a50ba5ea..0758db280f 100644 --- a/pype/plugins/blender/create/create_animation.py +++ b/pype/plugins/blender/create/create_animation.py @@ -4,6 +4,8 @@ import bpy from avalon import api from avalon.blender import Creator, lib +import pype.blender.plugin + class CreateAnimation(Creator): diff --git a/pype/plugins/blender/create/create_model.py b/pype/plugins/blender/create/create_model.py index a3b2ffc55b..7a53f215f2 100644 --- a/pype/plugins/blender/create/create_model.py +++ b/pype/plugins/blender/create/create_model.py @@ -4,7 +4,7 @@ import bpy from avalon import api from avalon.blender import Creator, lib - +import pype.blender.plugin class CreateModel(Creator): """Polygonal static geometry""" @@ -15,7 +15,6 @@ class CreateModel(Creator): icon = "cube" def process(self): - import pype.blender asset = self.data["asset"] subset = self.data["subset"] diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index f630c63966..b5860787ea 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -4,6 +4,7 @@ import bpy from avalon import api from avalon.blender import Creator, lib +import pype.blender.plugin class CreateRig(Creator): @@ -42,16 +43,16 @@ class CreateRig(Creator): self.data['task'] = api.Session.get('AVALON_TASK') lib.imprint(collection, self.data) - # Add the rig object and all the children meshes to - # a set and link them all at the end to avoid duplicates. + # Add the rig object and all the children meshes to + # a set and link them all at the end to avoid duplicates. # Blender crashes if trying to link an object that is already linked. - # This links automatically the children meshes if they were not + # This links automatically the children meshes if they were not # selected, and doesn't link them twice if they, insted, # were manually selected by the user. objects_to_link = set() if (self.options or {}).get("useSelection"): - + for obj in lib.get_selection(): objects_to_link.add( obj ) @@ -75,7 +76,7 @@ class CreateRig(Creator): # if len( custom_shapes ) > 0: # widgets_collection = bpy.data.collections.new(name="Widgets") - + # collection.children.link(widgets_collection) # for custom_shape in custom_shapes: diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py index 747bcd47f5..afde8b90a1 100644 --- a/pype/plugins/blender/load/load_action.py +++ b/pype/plugins/blender/load/load_action.py @@ -5,10 +5,9 @@ from pathlib import Path from pprint import pformat from typing import Dict, List, Optional -import avalon.blender.pipeline +from avalon import api, blender import bpy -import pype.blender -from avalon import api +import pype.blender.plugin logger = logging.getLogger("pype").getChild("blender").getChild("load_action") @@ -50,7 +49,7 @@ class BlendActionLoader(pype.blender.AssetLoader): container = bpy.data.collections.new(lib_container) container.name = container_name - avalon.blender.pipeline.containerise_existing( + blender.pipeline.containerise_existing( container, name, namespace, @@ -59,7 +58,7 @@ class BlendActionLoader(pype.blender.AssetLoader): ) container_metadata = container.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container @@ -89,16 +88,16 @@ class BlendActionLoader(pype.blender.AssetLoader): obj.animation_data.action.make_local() - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) objects_list.append(obj) - animation_container.pop(avalon.blender.pipeline.AVALON_PROPERTY) + animation_container.pop(blender.pipeline.AVALON_PROPERTY) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -153,7 +152,7 @@ class BlendActionLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] normalized_collection_libpath = ( @@ -224,16 +223,16 @@ class BlendActionLoader(pype.blender.AssetLoader): strip.action = obj.animation_data.action strip.action_frame_end = obj.animation_data.action.frame_range[1] - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": collection.name}) objects_list.append(obj) - animation_container.pop(avalon.blender.pipeline.AVALON_PROPERTY) + animation_container.pop(blender.pipeline.AVALON_PROPERTY) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -266,7 +265,7 @@ class BlendActionLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index 0610517b67..ec3e24443f 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -5,15 +5,15 @@ from pathlib import Path from pprint import pformat from typing import Dict, List, Optional -import avalon.blender.pipeline +from avalon import api, blender import bpy -import pype.blender -from avalon import api +import pype.blender.plugin + logger = logging.getLogger("pype").getChild("blender").getChild("load_animation") -class BlendAnimationLoader(pype.blender.AssetLoader): +class BlendAnimationLoader(pype.blender.plugin.AssetLoader): """Load animations from a .blend file. Warning: @@ -75,16 +75,16 @@ class BlendAnimationLoader(pype.blender.AssetLoader): obj.animation_data.action.make_local() - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) objects_list.append(obj) - animation_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + animation_container.pop( blender.pipeline.AVALON_PROPERTY ) bpy.ops.object.select_all(action='DESELECT') @@ -112,7 +112,7 @@ class BlendAnimationLoader(pype.blender.AssetLoader): container = bpy.data.collections.new(lib_container) container.name = container_name - avalon.blender.pipeline.containerise_existing( + blender.pipeline.containerise_existing( container, name, namespace, @@ -121,7 +121,7 @@ class BlendAnimationLoader(pype.blender.AssetLoader): ) container_metadata = container.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container @@ -179,7 +179,7 @@ class BlendAnimationLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] normalized_collection_libpath = ( @@ -239,12 +239,12 @@ class BlendAnimationLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] self._remove(self, objects, lib_container) - + bpy.data.collections.remove(collection) return True diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 10904a1f7b..b8b6b9b956 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -5,15 +5,14 @@ from pathlib import Path from pprint import pformat from typing import Dict, List, Optional -import avalon.blender.pipeline +from avalon import api, blender import bpy -import pype.blender -from avalon import api +import pype.blender.plugin logger = logging.getLogger("pype").getChild("blender").getChild("load_model") -class BlendModelLoader(pype.blender.AssetLoader): +class BlendModelLoader(pype.blender.plugin.AssetLoader): """Load models from a .blend file. Because they come from a .blend file we can simply link the collection that @@ -67,16 +66,16 @@ class BlendModelLoader(pype.blender.AssetLoader): material_slot.material.make_local() - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) objects_list.append(obj) - model_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + model_container.pop( blender.pipeline.AVALON_PROPERTY ) bpy.ops.object.select_all(action='DESELECT') @@ -104,7 +103,7 @@ class BlendModelLoader(pype.blender.AssetLoader): collection = bpy.data.collections.new(lib_container) collection.name = container_name - avalon.blender.pipeline.containerise_existing( + blender.pipeline.containerise_existing( collection, name, namespace, @@ -113,7 +112,7 @@ class BlendModelLoader(pype.blender.AssetLoader): ) container_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container @@ -169,7 +168,7 @@ class BlendModelLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] @@ -221,7 +220,7 @@ class BlendModelLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] @@ -232,7 +231,7 @@ class BlendModelLoader(pype.blender.AssetLoader): return True -class CacheModelLoader(pype.blender.AssetLoader): +class CacheModelLoader(pype.blender.plugin.AssetLoader): """Load cache models. Stores the imported asset in a collection named after the asset. diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index dcb70da6d8..44d47b41a1 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -5,15 +5,14 @@ from pathlib import Path from pprint import pformat from typing import Dict, List, Optional -import avalon.blender.pipeline +from avalon import api, blender import bpy -import pype.blender -from avalon import api +import pype.blender.plugin logger = logging.getLogger("pype").getChild("blender").getChild("load_model") -class BlendRigLoader(pype.blender.AssetLoader): +class BlendRigLoader(pype.blender.plugin.AssetLoader): """Load rigs from a .blend file. Because they come from a .blend file we can simply link the collection that @@ -30,7 +29,7 @@ class BlendRigLoader(pype.blender.AssetLoader): label = "Link Rig" icon = "code-fork" color = "orange" - + @staticmethod def _remove(self, objects, lib_container): @@ -60,7 +59,7 @@ class BlendRigLoader(pype.blender.AssetLoader): meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] armatures = [obj for obj in rig_container.objects if obj.type == 'ARMATURE'] - + objects_list = [] assert(len(armatures) == 1) @@ -74,11 +73,11 @@ class BlendRigLoader(pype.blender.AssetLoader): obj.data.make_local() - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) if obj.type == 'ARMATURE' and action is not None: @@ -86,8 +85,8 @@ class BlendRigLoader(pype.blender.AssetLoader): obj.animation_data.action = action objects_list.append(obj) - - rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + + rig_container.pop( blender.pipeline.AVALON_PROPERTY ) bpy.ops.object.select_all(action='DESELECT') @@ -115,7 +114,7 @@ class BlendRigLoader(pype.blender.AssetLoader): container = bpy.data.collections.new(lib_container) container.name = container_name - avalon.blender.pipeline.containerise_existing( + blender.pipeline.containerise_existing( container, name, namespace, @@ -124,7 +123,7 @@ class BlendRigLoader(pype.blender.AssetLoader): ) container_metadata = container.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container @@ -182,7 +181,7 @@ class BlendRigLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] @@ -243,12 +242,12 @@ class BlendRigLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] self._remove(self, objects, lib_container) - + bpy.data.collections.remove(collection) return True From 5ff73c064ecae24e0470529503bda8ab76875ddd Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 19 Mar 2020 19:40:40 +0100 Subject: [PATCH 69/99] remove extra imports --- pype/plugins/blender/create/create_action.py | 8 ++++---- pype/plugins/blender/create/create_animation.py | 2 -- pype/plugins/blender/create/create_model.py | 1 + pype/plugins/blender/create/create_rig.py | 3 +-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pype/plugins/blender/create/create_action.py b/pype/plugins/blender/create/create_action.py index 64dfe9ff90..68e2a50b61 100644 --- a/pype/plugins/blender/create/create_action.py +++ b/pype/plugins/blender/create/create_action.py @@ -7,7 +7,6 @@ from avalon.blender import Creator, lib import pype.blender.plugin - class CreateAction(Creator): """Action output for character rigs""" @@ -17,7 +16,6 @@ class CreateAction(Creator): icon = "male" def process(self): - import pype.blender asset = self.data["asset"] subset = self.data["subset"] @@ -29,9 +27,11 @@ class CreateAction(Creator): if (self.options or {}).get("useSelection"): for obj in lib.get_selection(): - if obj.animation_data is not None and obj.animation_data.action is not None: + if (obj.animation_data is not None + and obj.animation_data.action is not None): - empty_obj = bpy.data.objects.new( name = name, object_data = None ) + empty_obj = bpy.data.objects.new(name=name, + object_data=None) empty_obj.animation_data_create() empty_obj.animation_data.action = obj.animation_data.action empty_obj.animation_data.action.name = name diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py index 0758db280f..b40a456c8f 100644 --- a/pype/plugins/blender/create/create_animation.py +++ b/pype/plugins/blender/create/create_animation.py @@ -7,7 +7,6 @@ from avalon.blender import Creator, lib import pype.blender.plugin - class CreateAnimation(Creator): """Animation output for character rigs""" @@ -17,7 +16,6 @@ class CreateAnimation(Creator): icon = "male" def process(self): - import pype.blender asset = self.data["asset"] subset = self.data["subset"] diff --git a/pype/plugins/blender/create/create_model.py b/pype/plugins/blender/create/create_model.py index 7a53f215f2..303a7a63a1 100644 --- a/pype/plugins/blender/create/create_model.py +++ b/pype/plugins/blender/create/create_model.py @@ -6,6 +6,7 @@ from avalon import api from avalon.blender import Creator, lib import pype.blender.plugin + class CreateModel(Creator): """Polygonal static geometry""" diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index b5860787ea..d28e854232 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -33,7 +33,6 @@ class CreateRig(Creator): # return found def process(self): - import pype.blender asset = self.data["asset"] subset = self.data["subset"] @@ -61,7 +60,7 @@ class CreateRig(Creator): for subobj in obj.children: - objects_to_link.add( subobj ) + objects_to_link.add(subobj) # Create a new collection and link the widgets that # the rig uses. From 0f8b35c831420dc55ff07699ab54f1064b9a3004 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Mar 2020 11:31:59 +0000 Subject: [PATCH 70/99] Fixed Loader's parent class --- pype/plugins/blender/load/load_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py index afde8b90a1..e185bff7a8 100644 --- a/pype/plugins/blender/load/load_action.py +++ b/pype/plugins/blender/load/load_action.py @@ -12,7 +12,7 @@ import pype.blender.plugin logger = logging.getLogger("pype").getChild("blender").getChild("load_action") -class BlendActionLoader(pype.blender.AssetLoader): +class BlendActionLoader(pype.blender.plugin.AssetLoader): """Load action from a .blend file. Warning: From 7e8bb47ed727a5798427e393dac2361ba555b065 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Mar 2020 12:29:34 +0000 Subject: [PATCH 71/99] Fixed creation of the animation and fbx publishing --- .../blender/create/create_animation.py | 22 +++++++++++- .../blender/publish/extract_fbx_animation.py | 36 +++++++++++-------- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py index b40a456c8f..6b7616bbfd 100644 --- a/pype/plugins/blender/create/create_animation.py +++ b/pype/plugins/blender/create/create_animation.py @@ -25,8 +25,28 @@ class CreateAnimation(Creator): self.data['task'] = api.Session.get('AVALON_TASK') lib.imprint(collection, self.data) + # Add the rig object and all the children meshes to + # a set and link them all at the end to avoid duplicates. + # Blender crashes if trying to link an object that is already linked. + # This links automatically the children meshes if they were not + # selected, and doesn't link them twice if they, insted, + # were manually selected by the user. + objects_to_link = set() + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): - collection.objects.link(obj) + + objects_to_link.add( obj ) + + if obj.type == 'ARMATURE': + + for subobj in obj.children: + + objects_to_link.add(subobj) + + for obj in objects_to_link: + + collection.objects.link(obj) return collection diff --git a/pype/plugins/blender/publish/extract_fbx_animation.py b/pype/plugins/blender/publish/extract_fbx_animation.py index bc088f8bb7..4b1fe98c2f 100644 --- a/pype/plugins/blender/publish/extract_fbx_animation.py +++ b/pype/plugins/blender/publish/extract_fbx_animation.py @@ -47,8 +47,7 @@ class ExtractAnimationFBX(pype.api.Extractor): # We set the scale of the scene for the export bpy.context.scene.unit_settings.scale_length = 0.01 - # We export all the objects in the collection - objects_to_export = collections[0].objects + armatures = [obj for obj in collections[0].objects if obj.type == 'ARMATURE'] object_action_pairs = [] original_actions = [] @@ -56,20 +55,25 @@ class ExtractAnimationFBX(pype.api.Extractor): starting_frames = [] ending_frames = [] - # For each object, we make a copy of the current action - for obj in objects_to_export: + # For each armature, we make a copy of the current action + for obj in armatures: - curr_action = obj.animation_data.action - copy_action = curr_action.copy() + curr_action = None + copy_action = None + + if obj.animation_data and obj.animation_data.action: + + curr_action = obj.animation_data.action + copy_action = curr_action.copy() + + curr_frame_range = curr_action.frame_range + + starting_frames.append( curr_frame_range[0] ) + ending_frames.append( curr_frame_range[1] ) object_action_pairs.append((obj, copy_action)) original_actions.append(curr_action) - curr_frame_range = curr_action.frame_range - - starting_frames.append( curr_frame_range[0] ) - ending_frames.append( curr_frame_range[1] ) - # We compute the starting and ending frames max_frame = min( starting_frames ) min_frame = max( ending_frames ) @@ -98,10 +102,14 @@ class ExtractAnimationFBX(pype.api.Extractor): # We delete the baked action and set the original one back for i in range(0, len(object_action_pairs)): - object_action_pairs[i][0].animation_data.action = original_actions[i] + if original_actions[i]: - object_action_pairs[i][1].user_clear() - bpy.data.actions.remove(object_action_pairs[i][1]) + object_action_pairs[i][0].animation_data.action = original_actions[i] + + if object_action_pairs[i][1]: + + object_action_pairs[i][1].user_clear() + bpy.data.actions.remove(object_action_pairs[i][1]) if "representations" not in instance.data: instance.data["representations"] = [] From a3d025f7845ad3ef30f04fe5b7fcb7f6b0c8ab20 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Mar 2020 16:13:55 +0000 Subject: [PATCH 72/99] PEP8 compliance --- pype/blender/plugin.py | 16 +++--- pype/plugins/blender/create/create_action.py | 4 +- .../blender/create/create_animation.py | 2 +- pype/plugins/blender/create/create_rig.py | 43 +------------- pype/plugins/blender/load/load_action.py | 40 ++++++++----- pype/plugins/blender/load/load_animation.py | 24 +++++--- pype/plugins/blender/load/load_model.py | 8 ++- pype/plugins/blender/load/load_rig.py | 11 ++-- .../plugins/blender/publish/collect_action.py | 6 +- .../blender/publish/collect_animation.py | 10 ++-- .../blender/publish/collect_current_file.py | 3 +- pype/plugins/blender/publish/collect_model.py | 6 +- pype/plugins/blender/publish/collect_rig.py | 6 +- pype/plugins/blender/publish/extract_blend.py | 3 +- pype/plugins/blender/publish/extract_fbx.py | 34 +++++++---- .../blender/publish/extract_fbx_animation.py | 57 ++++++++++++------- 16 files changed, 137 insertions(+), 136 deletions(-) diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index b441714c0d..5e98d8314b 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -10,14 +10,16 @@ from avalon import api VALID_EXTENSIONS = [".blend"] -def asset_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: +def asset_name( + asset: str, subset: str, namespace: Optional[str] = None +) -> str: """Return a consistent name for an asset.""" name = f"{asset}_{subset}" if namespace: name = f"{namespace}:{name}" return name -def create_blender_context( obj: Optional[bpy.types.Object] = None ): +def create_blender_context(obj: Optional[bpy.types.Object] = None): """Create a new Blender context. If an object is passed as parameter, it is set as selected and active. """ @@ -27,16 +29,16 @@ def create_blender_context( obj: Optional[bpy.types.Object] = None ): for region in area.regions: if region.type == 'WINDOW': override_context = { - 'window': win, - 'screen': win.screen, - 'area': area, - 'region': region, + 'window': win, + 'screen': win.screen, + 'area': area, + 'region': region, 'scene': bpy.context.scene, 'active_object': obj, 'selected_objects': [obj] } return override_context - raise Exception( "Could not create a custom Blender context." ) + raise Exception("Could not create a custom Blender context.") class AssetLoader(api.Loader): """A basic AssetLoader for Blender diff --git a/pype/plugins/blender/create/create_action.py b/pype/plugins/blender/create/create_action.py index 68e2a50b61..6c24065f81 100644 --- a/pype/plugins/blender/create/create_action.py +++ b/pype/plugins/blender/create/create_action.py @@ -27,8 +27,8 @@ class CreateAction(Creator): if (self.options or {}).get("useSelection"): for obj in lib.get_selection(): - if (obj.animation_data is not None - and obj.animation_data.action is not None): + if (obj.animation_data is not None and + obj.animation_data.action is not None): empty_obj = bpy.data.objects.new(name=name, object_data=None) diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py index 6b7616bbfd..3a5985d7a2 100644 --- a/pype/plugins/blender/create/create_animation.py +++ b/pype/plugins/blender/create/create_animation.py @@ -37,7 +37,7 @@ class CreateAnimation(Creator): for obj in lib.get_selection(): - objects_to_link.add( obj ) + objects_to_link.add(obj) if obj.type == 'ARMATURE': diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index d28e854232..dc97d8b4ce 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -15,23 +15,6 @@ class CreateRig(Creator): family = "rig" icon = "wheelchair" - # @staticmethod - # def _find_layer_collection(self, layer_collection, collection): - - # found = None - - # if (layer_collection.collection == collection): - - # return layer_collection - - # for layer in layer_collection.children: - - # found = self._find_layer_collection(layer, collection) - - # if found: - - # return found - def process(self): asset = self.data["asset"] @@ -54,7 +37,7 @@ class CreateRig(Creator): for obj in lib.get_selection(): - objects_to_link.add( obj ) + objects_to_link.add(obj) if obj.type == 'ARMATURE': @@ -62,30 +45,6 @@ class CreateRig(Creator): objects_to_link.add(subobj) - # Create a new collection and link the widgets that - # the rig uses. - # custom_shapes = set() - - # for posebone in obj.pose.bones: - - # if posebone.custom_shape is not None: - - # custom_shapes.add( posebone.custom_shape ) - - # if len( custom_shapes ) > 0: - - # widgets_collection = bpy.data.collections.new(name="Widgets") - - # collection.children.link(widgets_collection) - - # for custom_shape in custom_shapes: - - # widgets_collection.objects.link( custom_shape ) - - # layer_collection = self._find_layer_collection(bpy.context.view_layer.layer_collection, widgets_collection) - - # layer_collection.exclude = True - for obj in objects_to_link: collection.objects.link(obj) diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py index e185bff7a8..303d1ead4d 100644 --- a/pype/plugins/blender/load/load_action.py +++ b/pype/plugins/blender/load/load_action.py @@ -69,11 +69,11 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): ) as (_, data_to): data_to.collections = [lib_container] - scene = bpy.context.scene + collection = bpy.context.scene.collection - scene.collection.children.link(bpy.data.collections[lib_container]) + collection.children.link(bpy.data.collections[lib_container]) - animation_container = scene.collection.children[lib_container].make_local() + animation_container = collection.children[lib_container].make_local() objects_list = [] @@ -84,9 +84,11 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): obj = obj.make_local() - if obj.animation_data is not None and obj.animation_data.action is not None: + anim_data = obj.animation_data - obj.animation_data.action.make_local() + if anim_data is not None and anim_data.action is not None: + + anim_data.action.make_local() if not obj.get(blender.pipeline.AVALON_PROPERTY): @@ -173,8 +175,12 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): strips = [] for obj in collection_metadata["objects"]: + + # Get all the strips that use the action + arm_objs = [ + arm for arm in bpy.data.objects if arm.type == 'ARMATURE'] - for armature_obj in [ objj for objj in bpy.data.objects if objj.type == 'ARMATURE' ]: + for armature_obj in arm_objs: if armature_obj.animation_data is not None: @@ -203,25 +209,27 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): scene.collection.children.link(bpy.data.collections[lib_container]) - animation_container = scene.collection.children[lib_container].make_local() + anim_container = scene.collection.children[lib_container].make_local() objects_list = [] # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. - for obj in animation_container.objects: + for obj in anim_container.objects: obj = obj.make_local() - if obj.animation_data is not None and obj.animation_data.action is not None: + anim_data = obj.animation_data - obj.animation_data.action.make_local() + if anim_data is not None and anim_data.action is not None: + + anim_data.action.make_local() for strip in strips: - strip.action = obj.animation_data.action - strip.action_frame_end = obj.animation_data.action.frame_range[1] + strip.action = anim_data.action + strip.action_frame_end = anim_data.action.frame_range[1] if not obj.get(blender.pipeline.AVALON_PROPERTY): @@ -232,7 +240,7 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): objects_list.append(obj) - animation_container.pop(blender.pipeline.AVALON_PROPERTY) + anim_container.pop(blender.pipeline.AVALON_PROPERTY) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -271,7 +279,11 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): for obj in objects: - for armature_obj in [ objj for objj in bpy.data.objects if objj.type == 'ARMATURE' ]: + # Get all the strips that use the action + arm_objs = [ + arm for arm in bpy.data.objects if arm.type == 'ARMATURE'] + + for armature_obj in arm_objs: if armature_obj.animation_data is not None: diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index ec3e24443f..395684a3ba 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -10,7 +10,8 @@ import bpy import pype.blender.plugin -logger = logging.getLogger("pype").getChild("blender").getChild("load_animation") +logger = logging.getLogger("pype").getChild( + "blender").getChild("load_animation") class BlendAnimationLoader(pype.blender.plugin.AssetLoader): @@ -53,10 +54,11 @@ class BlendAnimationLoader(pype.blender.plugin.AssetLoader): scene.collection.children.link(bpy.data.collections[lib_container]) - animation_container = scene.collection.children[lib_container].make_local() + anim_container = scene.collection.children[lib_container].make_local() - meshes = [obj for obj in animation_container.objects if obj.type == 'MESH'] - armatures = [obj for obj in animation_container.objects if obj.type == 'ARMATURE'] + meshes = [obj for obj in anim_container.objects if obj.type == 'MESH'] + armatures = [ + obj for obj in anim_container.objects if obj.type == 'ARMATURE'] # Should check if there is only an armature? @@ -71,9 +73,11 @@ class BlendAnimationLoader(pype.blender.plugin.AssetLoader): obj.data.make_local() - if obj.animation_data is not None and obj.animation_data.action is not None: + anim_data = obj.animation_data - obj.animation_data.action.make_local() + if anim_data is not None and anim_data.action is not None: + + anim_data.action.make_local() if not obj.get(blender.pipeline.AVALON_PROPERTY): @@ -84,7 +88,7 @@ class BlendAnimationLoader(pype.blender.plugin.AssetLoader): objects_list.append(obj) - animation_container.pop( blender.pipeline.AVALON_PROPERTY ) + anim_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') @@ -126,7 +130,8 @@ class BlendAnimationLoader(pype.blender.plugin.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - objects_list = self._process(self, libpath, lib_container, container_name) + objects_list = self._process( + self, libpath, lib_container, container_name) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -206,7 +211,8 @@ class BlendAnimationLoader(pype.blender.plugin.AssetLoader): self._remove(self, objects, lib_container) - objects_list = self._process(self, str(libpath), lib_container, collection.name) + objects_list = self._process( + self, str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index b8b6b9b956..ff7c6c49c2 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -75,7 +75,7 @@ class BlendModelLoader(pype.blender.plugin.AssetLoader): objects_list.append(obj) - model_container.pop( blender.pipeline.AVALON_PROPERTY ) + model_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') @@ -117,7 +117,8 @@ class BlendModelLoader(pype.blender.plugin.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - objects_list = self._process(self, libpath, lib_container, container_name) + objects_list = self._process( + self, libpath, lib_container, container_name) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -190,7 +191,8 @@ class BlendModelLoader(pype.blender.plugin.AssetLoader): self._remove(self, objects, lib_container) - objects_list = self._process(self, str(libpath), lib_container, collection.name) + objects_list = self._process( + self, str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 44d47b41a1..d14a868722 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -58,7 +58,8 @@ class BlendRigLoader(pype.blender.plugin.AssetLoader): rig_container = scene.collection.children[lib_container].make_local() meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] - armatures = [obj for obj in rig_container.objects if obj.type == 'ARMATURE'] + armatures = [ + obj for obj in rig_container.objects if obj.type == 'ARMATURE'] objects_list = [] @@ -86,7 +87,7 @@ class BlendRigLoader(pype.blender.plugin.AssetLoader): objects_list.append(obj) - rig_container.pop( blender.pipeline.AVALON_PROPERTY ) + rig_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') @@ -128,7 +129,8 @@ class BlendRigLoader(pype.blender.plugin.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - objects_list = self._process(self, libpath, lib_container, container_name, None) + objects_list = self._process( + self, libpath, lib_container, container_name, None) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -209,7 +211,8 @@ class BlendRigLoader(pype.blender.plugin.AssetLoader): self._remove(self, objects, lib_container) - objects_list = self._process(self, str(libpath), lib_container, collection.name, action) + objects_list = self._process( + self, str(libpath), lib_container, collection.name, action) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list diff --git a/pype/plugins/blender/publish/collect_action.py b/pype/plugins/blender/publish/collect_action.py index 9a54045cea..a8ceed9c82 100644 --- a/pype/plugins/blender/publish/collect_action.py +++ b/pype/plugins/blender/publish/collect_action.py @@ -1,9 +1,7 @@ -import typing from typing import Generator import bpy -import avalon.api import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY @@ -25,8 +23,8 @@ class CollectAction(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'action' - and not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'action' and + not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_animation.py b/pype/plugins/blender/publish/collect_animation.py index 109ae98e6f..50d49692b8 100644 --- a/pype/plugins/blender/publish/collect_animation.py +++ b/pype/plugins/blender/publish/collect_animation.py @@ -1,9 +1,7 @@ -import typing from typing import Generator import bpy -import avalon.api import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY @@ -20,13 +18,13 @@ class CollectAnimation(pyblish.api.ContextPlugin): """Return all 'animation' collections. Check if the family is 'animation' and if it doesn't have the - representation set. If the representation is set, it is a loaded animation - and we don't want to publish it. + representation set. If the representation is set, it is a loaded + animation and we don't want to publish it. """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'animation' - and not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'animation' and + not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_current_file.py b/pype/plugins/blender/publish/collect_current_file.py index 926d290b31..72976c490b 100644 --- a/pype/plugins/blender/publish/collect_current_file.py +++ b/pype/plugins/blender/publish/collect_current_file.py @@ -15,4 +15,5 @@ class CollectBlenderCurrentFile(pyblish.api.ContextPlugin): current_file = bpy.data.filepath context.data['currentFile'] = current_file - assert current_file != '', "Current file is empty. Save the file before continuing." + assert current_file != '', "Current file is empty. " \ + "Save the file before continuing." diff --git a/pype/plugins/blender/publish/collect_model.py b/pype/plugins/blender/publish/collect_model.py index ee10eaf7f2..df5c1e709a 100644 --- a/pype/plugins/blender/publish/collect_model.py +++ b/pype/plugins/blender/publish/collect_model.py @@ -1,9 +1,7 @@ -import typing from typing import Generator import bpy -import avalon.api import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY @@ -25,8 +23,8 @@ class CollectModel(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'model' - and not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'model' and + not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_rig.py b/pype/plugins/blender/publish/collect_rig.py index a4b30541f6..01958da37a 100644 --- a/pype/plugins/blender/publish/collect_rig.py +++ b/pype/plugins/blender/publish/collect_rig.py @@ -1,9 +1,7 @@ -import typing from typing import Generator import bpy -import avalon.api import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY @@ -25,8 +23,8 @@ class CollectRig(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'rig' - and not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'rig' and + not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/extract_blend.py b/pype/plugins/blender/publish/extract_blend.py index 032f85897d..5f3fdac293 100644 --- a/pype/plugins/blender/publish/extract_blend.py +++ b/pype/plugins/blender/publish/extract_blend.py @@ -43,4 +43,5 @@ class ExtractBlend(pype.api.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", instance.name, representation) + self.log.info("Extracted instance '%s' to: %s", + instance.name, representation) diff --git a/pype/plugins/blender/publish/extract_fbx.py b/pype/plugins/blender/publish/extract_fbx.py index 95466c1d2b..231bfdde24 100644 --- a/pype/plugins/blender/publish/extract_fbx.py +++ b/pype/plugins/blender/publish/extract_fbx.py @@ -1,10 +1,10 @@ import os -import avalon.blender.workio import pype.api import bpy + class ExtractFBX(pype.api.Extractor): """Extract as FBX.""" @@ -20,29 +20,39 @@ class ExtractFBX(pype.api.Extractor): filename = f"{instance.name}.fbx" filepath = os.path.join(stagingdir, filename) + context = bpy.context + scene = context.scene + view_layer = context.view_layer + # Perform extraction self.log.info("Performing extraction..") - collections = [obj for obj in instance if type(obj) is bpy.types.Collection] + collections = [ + obj for obj in instance if type(obj) is bpy.types.Collection] - assert len(collections) == 1, "There should be one and only one collection collected for this asset" + assert len(collections) == 1, "There should be one and only one " \ + "collection collected for this asset" - old_active_layer_collection = bpy.context.view_layer.active_layer_collection + old_active_layer_collection = view_layer.active_layer_collection + + layers = view_layer.layer_collection.children # Get the layer collection from the collection we need to export. - # This is needed because in Blender you can only set the active + # This is needed because in Blender you can only set the active # collection with the layer collection, and there is no way to get - # the layer collection from the collection (but there is the vice versa). - layer_collections = [layer for layer in bpy.context.view_layer.layer_collection.children if layer.collection == collections[0]] + # the layer collection from the collection + # (but there is the vice versa). + layer_collections = [ + layer for layer in layers if layer.collection == collections[0]] assert len(layer_collections) == 1 - bpy.context.view_layer.active_layer_collection = layer_collections[0] + view_layer.active_layer_collection = layer_collections[0] - old_scale = bpy.context.scene.unit_settings.scale_length + old_scale = scene.unit_settings.scale_length # We set the scale of the scene for the export - bpy.context.scene.unit_settings.scale_length = 0.01 + scene.unit_settings.scale_length = 0.01 # We export the fbx bpy.ops.export_scene.fbx( @@ -52,9 +62,9 @@ class ExtractFBX(pype.api.Extractor): add_leaf_bones=False ) - bpy.context.view_layer.active_layer_collection = old_active_layer_collection + view_layer.active_layer_collection = old_active_layer_collection - bpy.context.scene.unit_settings.scale_length = old_scale + scene.unit_settings.scale_length = old_scale if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/pype/plugins/blender/publish/extract_fbx_animation.py b/pype/plugins/blender/publish/extract_fbx_animation.py index 4b1fe98c2f..d51c641e9c 100644 --- a/pype/plugins/blender/publish/extract_fbx_animation.py +++ b/pype/plugins/blender/publish/extract_fbx_animation.py @@ -1,5 +1,4 @@ import os -import avalon.blender.workio import pype.api @@ -23,31 +22,42 @@ class ExtractAnimationFBX(pype.api.Extractor): filename = f"{instance.name}.fbx" filepath = os.path.join(stagingdir, filename) + context = bpy.context + scene = context.scene + view_layer = context.view_layer + # Perform extraction self.log.info("Performing extraction..") - collections = [obj for obj in instance if type(obj) is bpy.types.Collection] + collections = [ + obj for obj in instance if type(obj) is bpy.types.Collection] - assert len(collections) == 1, "There should be one and only one collection collected for this asset" + assert len(collections) == 1, "There should be one and only one " \ + "collection collected for this asset" - old_active_layer_collection = bpy.context.view_layer.active_layer_collection + old_active_layer_collection = view_layer.active_layer_collection + + layers = view_layer.layer_collection.children # Get the layer collection from the collection we need to export. - # This is needed because in Blender you can only set the active + # This is needed because in Blender you can only set the active # collection with the layer collection, and there is no way to get - # the layer collection from the collection (but there is the vice versa). - layer_collections = [layer for layer in bpy.context.view_layer.layer_collection.children if layer.collection == collections[0]] + # the layer collection from the collection + # (but there is the vice versa). + layer_collections = [ + layer for layer in layers if layer.collection == collections[0]] assert len(layer_collections) == 1 - bpy.context.view_layer.active_layer_collection = layer_collections[0] + view_layer.active_layer_collection = layer_collections[0] - old_scale = bpy.context.scene.unit_settings.scale_length + old_scale = scene.unit_settings.scale_length # We set the scale of the scene for the export - bpy.context.scene.unit_settings.scale_length = 0.01 + scene.unit_settings.scale_length = 0.01 - armatures = [obj for obj in collections[0].objects if obj.type == 'ARMATURE'] + armatures = [ + obj for obj in collections[0].objects if obj.type == 'ARMATURE'] object_action_pairs = [] original_actions = [] @@ -68,15 +78,15 @@ class ExtractAnimationFBX(pype.api.Extractor): curr_frame_range = curr_action.frame_range - starting_frames.append( curr_frame_range[0] ) - ending_frames.append( curr_frame_range[1] ) + starting_frames.append(curr_frame_range[0]) + ending_frames.append(curr_frame_range[1]) object_action_pairs.append((obj, copy_action)) original_actions.append(curr_action) # We compute the starting and ending frames - max_frame = min( starting_frames ) - min_frame = max( ending_frames ) + max_frame = min(starting_frames) + min_frame = max(ending_frames) # We bake the copy of the current action for each object bpy_extras.anim_utils.bake_action_objects( @@ -95,21 +105,24 @@ class ExtractAnimationFBX(pype.api.Extractor): add_leaf_bones=False ) - bpy.context.view_layer.active_layer_collection = old_active_layer_collection + view_layer.active_layer_collection = old_active_layer_collection - bpy.context.scene.unit_settings.scale_length = old_scale + scene.unit_settings.scale_length = old_scale # We delete the baked action and set the original one back for i in range(0, len(object_action_pairs)): - if original_actions[i]: + pair = object_action_pairs[i] + action = original_actions[i] - object_action_pairs[i][0].animation_data.action = original_actions[i] + if action: - if object_action_pairs[i][1]: + pair[0].animation_data.action = action - object_action_pairs[i][1].user_clear() - bpy.data.actions.remove(object_action_pairs[i][1]) + if pair[1]: + + pair[1].user_clear() + bpy.data.actions.remove(pair[1]) if "representations" not in instance.data: instance.data["representations"] = [] From 40cf778f106c32deec2d850d0c72c906537ebfcf Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Mar 2020 16:46:45 +0000 Subject: [PATCH 73/99] More PEP8 compliance --- pype/blender/plugin.py | 5 ++++- pype/plugins/blender/create/create_action.py | 4 ++-- pype/plugins/blender/load/load_action.py | 2 +- pype/plugins/blender/publish/collect_action.py | 4 ++-- pype/plugins/blender/publish/collect_animation.py | 6 +++--- pype/plugins/blender/publish/collect_model.py | 4 ++-- pype/plugins/blender/publish/collect_rig.py | 4 ++-- 7 files changed, 16 insertions(+), 13 deletions(-) diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index 5e98d8314b..8f72d04a1d 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -19,6 +19,7 @@ def asset_name( name = f"{namespace}:{name}" return name + def create_blender_context(obj: Optional[bpy.types.Object] = None): """Create a new Blender context. If an object is passed as parameter, it is set as selected and active. @@ -40,6 +41,7 @@ def create_blender_context(obj: Optional[bpy.types.Object] = None): return override_context raise Exception("Could not create a custom Blender context.") + class AssetLoader(api.Loader): """A basic AssetLoader for Blender @@ -89,7 +91,8 @@ class AssetLoader(api.Loader): assert obj.library, f"'{obj.name}' is not linked." libraries.add(obj.library) - assert len(libraries) == 1, "'{container.name}' contains objects from more then 1 library." + assert len( + libraries) == 1, "'{container.name}' contains objects from more then 1 library." return list(libraries)[0] diff --git a/pype/plugins/blender/create/create_action.py b/pype/plugins/blender/create/create_action.py index 6c24065f81..68e2a50b61 100644 --- a/pype/plugins/blender/create/create_action.py +++ b/pype/plugins/blender/create/create_action.py @@ -27,8 +27,8 @@ class CreateAction(Creator): if (self.options or {}).get("useSelection"): for obj in lib.get_selection(): - if (obj.animation_data is not None and - obj.animation_data.action is not None): + if (obj.animation_data is not None + and obj.animation_data.action is not None): empty_obj = bpy.data.objects.new(name=name, object_data=None) diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py index 303d1ead4d..a1b1ad3cea 100644 --- a/pype/plugins/blender/load/load_action.py +++ b/pype/plugins/blender/load/load_action.py @@ -175,7 +175,7 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): strips = [] for obj in collection_metadata["objects"]: - + # Get all the strips that use the action arm_objs = [ arm for arm in bpy.data.objects if arm.type == 'ARMATURE'] diff --git a/pype/plugins/blender/publish/collect_action.py b/pype/plugins/blender/publish/collect_action.py index a8ceed9c82..c359198490 100644 --- a/pype/plugins/blender/publish/collect_action.py +++ b/pype/plugins/blender/publish/collect_action.py @@ -23,8 +23,8 @@ class CollectAction(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'action' and - not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'action' + and not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_animation.py b/pype/plugins/blender/publish/collect_animation.py index 50d49692b8..681f945f25 100644 --- a/pype/plugins/blender/publish/collect_animation.py +++ b/pype/plugins/blender/publish/collect_animation.py @@ -18,13 +18,13 @@ class CollectAnimation(pyblish.api.ContextPlugin): """Return all 'animation' collections. Check if the family is 'animation' and if it doesn't have the - representation set. If the representation is set, it is a loaded + representation set. If the representation is set, it is a loaded animation and we don't want to publish it. """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'animation' and - not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'animation' + and not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_model.py b/pype/plugins/blender/publish/collect_model.py index df5c1e709a..5cbd097a4e 100644 --- a/pype/plugins/blender/publish/collect_model.py +++ b/pype/plugins/blender/publish/collect_model.py @@ -23,8 +23,8 @@ class CollectModel(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'model' and - not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'model' + and not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_rig.py b/pype/plugins/blender/publish/collect_rig.py index 01958da37a..730f209e89 100644 --- a/pype/plugins/blender/publish/collect_rig.py +++ b/pype/plugins/blender/publish/collect_rig.py @@ -23,8 +23,8 @@ class CollectRig(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'rig' and - not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'rig' + and not avalon_prop.get('representation')): yield collection def process(self, context): From 043d8225a368510acb3427a92ae2672e23501367 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 21 Mar 2020 01:07:02 +0100 Subject: [PATCH 74/99] unify collectors --- .../plugins/blender/publish/collect_action.py | 51 ------------------- .../blender/publish/collect_animation.py | 51 ------------------- ...{collect_model.py => collect_instances.py} | 17 ++++--- pype/plugins/blender/publish/collect_rig.py | 51 ------------------- 4 files changed, 10 insertions(+), 160 deletions(-) delete mode 100644 pype/plugins/blender/publish/collect_action.py delete mode 100644 pype/plugins/blender/publish/collect_animation.py rename pype/plugins/blender/publish/{collect_model.py => collect_instances.py} (78%) delete mode 100644 pype/plugins/blender/publish/collect_rig.py diff --git a/pype/plugins/blender/publish/collect_action.py b/pype/plugins/blender/publish/collect_action.py deleted file mode 100644 index c359198490..0000000000 --- a/pype/plugins/blender/publish/collect_action.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import Generator - -import bpy - -import pyblish.api -from avalon.blender.pipeline import AVALON_PROPERTY - - -class CollectAction(pyblish.api.ContextPlugin): - """Collect the data of an action.""" - - hosts = ["blender"] - label = "Collect Action" - order = pyblish.api.CollectorOrder - - @staticmethod - def get_action_collections() -> Generator: - """Return all 'animation' collections. - - Check if the family is 'action' and if it doesn't have the - representation set. If the representation is set, it is a loaded action - and we don't want to publish it. - """ - for collection in bpy.data.collections: - avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'action' - and not avalon_prop.get('representation')): - yield collection - - def process(self, context): - """Collect the actions from the current Blender scene.""" - collections = self.get_action_collections() - for collection in collections: - avalon_prop = collection[AVALON_PROPERTY] - asset = avalon_prop['asset'] - family = avalon_prop['family'] - subset = avalon_prop['subset'] - task = avalon_prop['task'] - name = f"{asset}_{subset}" - instance = context.create_instance( - name=name, - family=family, - families=[family], - subset=subset, - asset=asset, - task=task, - ) - members = list(collection.objects) - members.append(collection) - instance[:] = members - self.log.debug(instance.data) diff --git a/pype/plugins/blender/publish/collect_animation.py b/pype/plugins/blender/publish/collect_animation.py deleted file mode 100644 index 681f945f25..0000000000 --- a/pype/plugins/blender/publish/collect_animation.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import Generator - -import bpy - -import pyblish.api -from avalon.blender.pipeline import AVALON_PROPERTY - - -class CollectAnimation(pyblish.api.ContextPlugin): - """Collect the data of an animation.""" - - hosts = ["blender"] - label = "Collect Animation" - order = pyblish.api.CollectorOrder - - @staticmethod - def get_animation_collections() -> Generator: - """Return all 'animation' collections. - - Check if the family is 'animation' and if it doesn't have the - representation set. If the representation is set, it is a loaded - animation and we don't want to publish it. - """ - for collection in bpy.data.collections: - avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'animation' - and not avalon_prop.get('representation')): - yield collection - - def process(self, context): - """Collect the animations from the current Blender scene.""" - collections = self.get_animation_collections() - for collection in collections: - avalon_prop = collection[AVALON_PROPERTY] - asset = avalon_prop['asset'] - family = avalon_prop['family'] - subset = avalon_prop['subset'] - task = avalon_prop['task'] - name = f"{asset}_{subset}" - instance = context.create_instance( - name=name, - family=family, - families=[family], - subset=subset, - asset=asset, - task=task, - ) - members = list(collection.objects) - members.append(collection) - instance[:] = members - self.log.debug(instance.data) diff --git a/pype/plugins/blender/publish/collect_model.py b/pype/plugins/blender/publish/collect_instances.py similarity index 78% rename from pype/plugins/blender/publish/collect_model.py rename to pype/plugins/blender/publish/collect_instances.py index 5cbd097a4e..1d3693216d 100644 --- a/pype/plugins/blender/publish/collect_model.py +++ b/pype/plugins/blender/publish/collect_instances.py @@ -1,20 +1,21 @@ from typing import Generator import bpy +import json import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY -class CollectModel(pyblish.api.ContextPlugin): +class CollectInstances(pyblish.api.ContextPlugin): """Collect the data of a model.""" hosts = ["blender"] - label = "Collect Model" + label = "Collect Instances" order = pyblish.api.CollectorOrder @staticmethod - def get_model_collections() -> Generator: + def get_collections() -> Generator: """Return all 'model' collections. Check if the family is 'model' and if it doesn't have the @@ -23,13 +24,13 @@ class CollectModel(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'model' - and not avalon_prop.get('representation')): + if avalon_prop.get('id') == 'pyblish.avalon.instance': yield collection def process(self, context): """Collect the models from the current Blender scene.""" - collections = self.get_model_collections() + collections = self.get_collections() + for collection in collections: avalon_prop = collection[AVALON_PROPERTY] asset = avalon_prop['asset'] @@ -48,4 +49,6 @@ class CollectModel(pyblish.api.ContextPlugin): members = list(collection.objects) members.append(collection) instance[:] = members - self.log.debug(instance.data) + self.log.debug(json.dumps(instance.data, indent=4)) + for obj in instance: + self.log.debug(obj) diff --git a/pype/plugins/blender/publish/collect_rig.py b/pype/plugins/blender/publish/collect_rig.py deleted file mode 100644 index 730f209e89..0000000000 --- a/pype/plugins/blender/publish/collect_rig.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import Generator - -import bpy - -import pyblish.api -from avalon.blender.pipeline import AVALON_PROPERTY - - -class CollectRig(pyblish.api.ContextPlugin): - """Collect the data of a rig.""" - - hosts = ["blender"] - label = "Collect Rig" - order = pyblish.api.CollectorOrder - - @staticmethod - def get_rig_collections() -> Generator: - """Return all 'rig' collections. - - Check if the family is 'rig' and if it doesn't have the - representation set. If the representation is set, it is a loaded rig - and we don't want to publish it. - """ - for collection in bpy.data.collections: - avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'rig' - and not avalon_prop.get('representation')): - yield collection - - def process(self, context): - """Collect the rigs from the current Blender scene.""" - collections = self.get_rig_collections() - for collection in collections: - avalon_prop = collection[AVALON_PROPERTY] - asset = avalon_prop['asset'] - family = avalon_prop['family'] - subset = avalon_prop['subset'] - task = avalon_prop['task'] - name = f"{asset}_{subset}" - instance = context.create_instance( - name=name, - family=family, - families=[family], - subset=subset, - asset=asset, - task=task, - ) - members = list(collection.objects) - members.append(collection) - instance[:] = members - self.log.debug(instance.data) From 00bec457481636d48be8ec516786b83594061a82 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 21 Mar 2020 01:07:34 +0100 Subject: [PATCH 75/99] naive fix to crashing uv validator --- .../blender/publish/validate_mesh_has_uv.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pype/plugins/blender/publish/validate_mesh_has_uv.py b/pype/plugins/blender/publish/validate_mesh_has_uv.py index b71a40ad8f..d0cd33645b 100644 --- a/pype/plugins/blender/publish/validate_mesh_has_uv.py +++ b/pype/plugins/blender/publish/validate_mesh_has_uv.py @@ -35,12 +35,15 @@ class ValidateMeshHasUvs(pyblish.api.InstancePlugin): invalid = [] # TODO (jasper): only check objects in the collection that will be published? for obj in [ - obj for obj in bpy.data.objects if obj.type == 'MESH' - ]: - # Make sure we are in object mode. - bpy.ops.object.mode_set(mode='OBJECT') - if not cls.has_uvs(obj): - invalid.append(obj) + obj for obj in instance]: + try: + if obj.type == 'MESH': + # Make sure we are in object mode. + bpy.ops.object.mode_set(mode='OBJECT') + if not cls.has_uvs(obj): + invalid.append(obj) + except: + continue return invalid def process(self, instance): From 28c626b69469c1b46e3939022ab7e997c2c7c09c Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 21 Mar 2020 01:07:59 +0100 Subject: [PATCH 76/99] attempt at alembic extractor --- pype/blender/plugin.py | 11 ++- pype/plugins/blender/publish/extract_abc.py | 91 +++++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 pype/plugins/blender/publish/extract_abc.py diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index 8f72d04a1d..f27bf0daab 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -20,10 +20,15 @@ def asset_name( return name -def create_blender_context(obj: Optional[bpy.types.Object] = None): +def create_blender_context(active: Optional[bpy.types.Object] = None, + selected: Optional[bpy.types.Object] = None,): """Create a new Blender context. If an object is passed as parameter, it is set as selected and active. """ + + if not isinstance(selected, list): + selected = [selected] + for win in bpy.context.window_manager.windows: for area in win.screen.areas: if area.type == 'VIEW_3D': @@ -35,8 +40,8 @@ def create_blender_context(obj: Optional[bpy.types.Object] = None): 'area': area, 'region': region, 'scene': bpy.context.scene, - 'active_object': obj, - 'selected_objects': [obj] + 'active_object': selected[0], + 'selected_objects': selected } return override_context raise Exception("Could not create a custom Blender context.") diff --git a/pype/plugins/blender/publish/extract_abc.py b/pype/plugins/blender/publish/extract_abc.py new file mode 100644 index 0000000000..b953d41ba2 --- /dev/null +++ b/pype/plugins/blender/publish/extract_abc.py @@ -0,0 +1,91 @@ +import os + +import pype.api +import pype.blender.plugin + +import bpy + + +class ExtractABC(pype.api.Extractor): + """Extract as ABC.""" + + label = "Extract ABC" + hosts = ["blender"] + families = ["model"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.fbx" + filepath = os.path.join(stagingdir, filename) + + context = bpy.context + scene = context.scene + view_layer = context.view_layer + + # Perform extraction + self.log.info("Performing extraction..") + + collections = [ + obj for obj in instance if type(obj) is bpy.types.Collection] + + assert len(collections) == 1, "There should be one and only one " \ + "collection collected for this asset" + + old_active_layer_collection = view_layer.active_layer_collection + + layers = view_layer.layer_collection.children + + # Get the layer collection from the collection we need to export. + # This is needed because in Blender you can only set the active + # collection with the layer collection, and there is no way to get + # the layer collection from the collection + # (but there is the vice versa). + layer_collections = [ + layer for layer in layers if layer.collection == collections[0]] + + assert len(layer_collections) == 1 + + view_layer.active_layer_collection = layer_collections[0] + + old_scale = scene.unit_settings.scale_length + + selected = list() + + for obj in instance: + selected.append(obj) + + new_context = pype.blender.plugin.create_blender_context(active=None, selected=selected) + + # We set the scale of the scene for the export + scene.unit_settings.scale_length = 0.01 + + self.log.info(new_context) + + # We export the abc + bpy.ops.wm.alembic_export( + new_context, + filepath=filepath, + start=1, + end=1 + ) + + view_layer.active_layer_collection = old_active_layer_collection + + scene.unit_settings.scale_length = old_scale + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'abc', + 'ext': 'abc', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s", + instance.name, representation) From 59dd912a7626e3e98238c11a4c38ebaf53162f23 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 21 Mar 2020 01:26:40 +0100 Subject: [PATCH 77/99] tweak context override --- pype/blender/plugin.py | 2 +- pype/plugins/blender/publish/extract_abc.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index f27bf0daab..77fce90d65 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -40,7 +40,7 @@ def create_blender_context(active: Optional[bpy.types.Object] = None, 'area': area, 'region': region, 'scene': bpy.context.scene, - 'active_object': selected[0], + 'active_object': active, 'selected_objects': selected } return override_context diff --git a/pype/plugins/blender/publish/extract_abc.py b/pype/plugins/blender/publish/extract_abc.py index b953d41ba2..d2c0c769ae 100644 --- a/pype/plugins/blender/publish/extract_abc.py +++ b/pype/plugins/blender/publish/extract_abc.py @@ -55,9 +55,13 @@ class ExtractABC(pype.api.Extractor): selected = list() for obj in instance: - selected.append(obj) + try: + obj.select_set(True) + selected.append(obj) + except: + continue - new_context = pype.blender.plugin.create_blender_context(active=None, selected=selected) + new_context = pype.blender.plugin.create_blender_context(active=selected[0], selected=selected) # We set the scale of the scene for the export scene.unit_settings.scale_length = 0.01 From e5fd3d3b5df0f2717d5a566264940ddabcaf8ad9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Sat, 21 Mar 2020 19:13:53 +0100 Subject: [PATCH 78/99] frame range validator remake --- .../maya/publish/validate_frame_range.py | 158 +++++++++++++++--- 1 file changed, 135 insertions(+), 23 deletions(-) diff --git a/pype/plugins/maya/publish/validate_frame_range.py b/pype/plugins/maya/publish/validate_frame_range.py index d4aad812d5..26235b37ae 100644 --- a/pype/plugins/maya/publish/validate_frame_range.py +++ b/pype/plugins/maya/publish/validate_frame_range.py @@ -1,18 +1,18 @@ import pyblish.api import pype.api +from maya import cmds + class ValidateFrameRange(pyblish.api.InstancePlugin): """Valides the frame ranges. - Checks the `startFrame`, `endFrame` and `handles` data. - This does NOT ensure there's actual data present. + This is optional validator checking if the frame range matches the one of + asset. - This validates: - - `startFrame` is lower than or equal to the `endFrame`. - - must have both the `startFrame` and `endFrame` data. - - The `handles` value is not lower than zero. + Repair action will change everything to match asset. + This can be turned off by artist to allow custom ranges. """ label = "Validate Frame Range" @@ -20,26 +20,138 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): families = ["animation", "pointcache", "camera", - "renderlayer", - "colorbleed.vrayproxy"] + "render", + "review", + "yeticache"] + optional = True + actions = [pype.api.RepairAction] def process(self, instance): + context = instance.context - start = instance.data.get("frameStart", None) - end = instance.data.get("frameEnd", None) - handles = instance.data.get("handles", None) + frame_start_handle = int(context.data.get("frameStartHandle")) + frame_end_handle = int(context.data.get("frameEndHandle")) + handles = int(context.data.get("handles")) + handle_start = int(context.data.get("handleStart")) + handle_end = int(context.data.get("handleEnd")) + frame_start = int(context.data.get("frameStart")) + frame_end = int(context.data.get("frameEnd")) - # Check if any of the values are present - if any(value is None for value in [start, end]): - raise ValueError("No time values for this instance. " - "(Missing `startFrame` or `endFrame`)") + inst_start = int(instance.data.get("frameStartHandle")) + inst_end = int(instance.data.get("frameEndHandle")) - self.log.info("Comparing start (%s) and end (%s)" % (start, end)) - if start > end: - raise RuntimeError("The start frame is a higher value " - "than the end frame: " - "{0}>{1}".format(start, end)) + # basic sanity checks + assert frame_start_handle <= frame_end_handle, ( + "start frame is lower then end frame") - if handles is not None: - if handles < 0.0: - raise RuntimeError("Handles are set to a negative value") + assert handles >= 0, ("handles cannot have negative values") + + # compare with data on instance + errors = [] + + if(inst_start != frame_start_handle): + errors.append("Instance start frame [ {} ] doesn't " + "match the one set on instance [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + inst_start, + frame_start_handle, + handle_start, frame_start, frame_end, handle_end + )) + + if(inst_end != frame_end_handle): + errors.append("Instance end frame [ {} ] doesn't " + "match the one set on instance [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + inst_end, + frame_end_handle, + handle_start, frame_start, frame_end, handle_end + )) + + minTime = int(cmds.playbackOptions(minTime=True, query=True)) + maxTime = int(cmds.playbackOptions(maxTime=True, query=True)) + animStartTime = int(cmds.playbackOptions(animationStartTime=True, + query=True)) + animEndTime = int(cmds.playbackOptions(animationEndTime=True, + query=True)) + + if int(minTime) != inst_start: + errors.append("Start of Maya timeline is set to frame [ {} ] " + " and doesn't match the one set on asset [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + minTime, + frame_start_handle, + handle_start, frame_start, frame_end, handle_end + )) + + if int(maxTime) != inst_end: + errors.append("End of Maya timeline is set to frame [ {} ] " + " and doesn't match the one set on asset [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + maxTime, + frame_end_handle, + handle_start, frame_start, frame_end, handle_end + )) + + if int(animStartTime) != inst_start: + errors.append("Animation start in Maya is set to frame [ {} ] " + " and doesn't match the one set on asset [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + animStartTime, + frame_start_handle, + handle_start, frame_start, frame_end, handle_end + )) + + if int(animEndTime) != inst_end: + errors.append("Animation start in Maya is set to frame [ {} ] " + " and doesn't match the one set on asset [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + animEndTime, + frame_end_handle, + handle_start, frame_start, frame_end, handle_end + )) + + render_start = int(cmds.getAttr("defaultRenderGlobals.startFrame")) + render_end = int(cmds.getAttr("defaultRenderGlobals.endFrame")) + + if int(render_start) != inst_start: + errors.append("Render settings start frame is set to [ {} ] " + " and doesn't match the one set on asset [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + int(render_start), + frame_start_handle, + handle_start, frame_start, frame_end, handle_end + )) + + if int(render_end) != inst_end: + errors.append("Render settings end frame is set to [ {} ] " + " and doesn't match the one set on asset [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + int(render_end), + frame_end_handle, + handle_start, frame_start, frame_end, handle_end + )) + + for e in errors: + self.log.error(e) + + assert len(errors) == 0, ("Frame range settings are incorrect") + + @classmethod + def repair(cls, instance): + """ + Repair by calling avalon reset frame range function. This will set + timeline frame range, render settings range and frame information + on instance container to match asset data. + """ + import avalon.maya.interactive + avalon.maya.interactive.reset_frame_range() + cls.log.debug("-" * 80) + cls.log.debug("{}.frameStart".format(instance.data["name"])) + cmds.setAttr( + "{}.frameStart".format(instance.data["name"]), + instance.context.data.get("frameStartHandle")) + + cmds.setAttr( + "{}.frameEnd".format(instance.data["name"]), + instance.context.data.get("frameEndHandle")) + cls.log.debug("-" * 80) From 8d32d77668ebb7b535f5b660d72bfec367788d30 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Sat, 21 Mar 2020 21:00:39 +0100 Subject: [PATCH 79/99] remove debug prints --- pype/plugins/maya/publish/validate_frame_range.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pype/plugins/maya/publish/validate_frame_range.py b/pype/plugins/maya/publish/validate_frame_range.py index 26235b37ae..0be77644a0 100644 --- a/pype/plugins/maya/publish/validate_frame_range.py +++ b/pype/plugins/maya/publish/validate_frame_range.py @@ -145,8 +145,6 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): """ import avalon.maya.interactive avalon.maya.interactive.reset_frame_range() - cls.log.debug("-" * 80) - cls.log.debug("{}.frameStart".format(instance.data["name"])) cmds.setAttr( "{}.frameStart".format(instance.data["name"]), instance.context.data.get("frameStartHandle")) @@ -154,4 +152,3 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): cmds.setAttr( "{}.frameEnd".format(instance.data["name"]), instance.context.data.get("frameEndHandle")) - cls.log.debug("-" * 80) From 0e991c4fb572ebfb3a54c8d9e8b882f8377fdba1 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Sat, 21 Mar 2020 23:21:41 +0100 Subject: [PATCH 80/99] shutting up hound --- .../plugins/maya/publish/collect_instances.py | 9 ++++---- pype/plugins/maya/publish/collect_render.py | 21 ++++++++----------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/pype/plugins/maya/publish/collect_instances.py b/pype/plugins/maya/publish/collect_instances.py index 9ea3ebe7fa..1d59a68bf6 100644 --- a/pype/plugins/maya/publish/collect_instances.py +++ b/pype/plugins/maya/publish/collect_instances.py @@ -122,7 +122,7 @@ class CollectInstances(pyblish.api.ContextPlugin): # if frame range on maya set is the same as full shot range # adjust the values to match the asset data if (ctx_frame_start_handle == data["frameStart"] - and ctx_frame_end_handle == data["frameEnd"]): + and ctx_frame_end_handle == data["frameEnd"]): # noqa: W503, E501 data["frameStartHandle"] = ctx_frame_start_handle data["frameEndHandle"] = ctx_frame_end_handle data["frameStart"] = ctx_frame_start @@ -141,8 +141,8 @@ class CollectInstances(pyblish.api.ContextPlugin): data["handleStart"] = 0 data["handleEnd"] = 0 - data["frameStartHandle"] = data["frameStart"] - data["handleStart"] - data["frameEndHandle"] = data["frameEnd"] + data["handleEnd"] + data["frameStartHandle"] = data["frameStart"] - data["handleStart"] # noqa: E501 + data["frameEndHandle"] = data["frameEnd"] + data["handleEnd"] # noqa: E501 if "handles" in data: data.pop('handles') @@ -157,7 +157,8 @@ class CollectInstances(pyblish.api.ContextPlugin): # Produce diagnostic message for any graphical # user interface interested in visualising it. self.log.info("Found: \"%s\" " % instance.data["name"]) - self.log.debug("DATA: {} ".format(json.dumps(instance.data, indent=4))) + self.log.debug( + "DATA: {} ".format(json.dumps(instance.data, indent=4))) def sort_by_family(instance): """Sort by family""" diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 88c1be477d..365b0b5a13 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -203,13 +203,13 @@ class CollectMayaRender(pyblish.api.ContextPlugin): full_paths.append(full_path) aov_dict["beauty"] = full_paths - frame_start_render = int(self.get_render_attribute("startFrame", - layer=layer_name)) - frame_end_render = int(self.get_render_attribute("endFrame", - layer=layer_name)) + frame_start_render = int(self.get_render_attribute( + "startFrame", layer=layer_name)) + frame_end_render = int(self.get_render_attribute( + "endFrame", layer=layer_name)) - if (int(context.data['frameStartHandle']) == frame_start_render and - int(context.data['frameEndHandle']) == frame_end_render): + if (int(context.data['frameStartHandle']) == frame_start_render + and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501 handle_start = context.data['handleStart'] handle_end = context.data['handleEnd'] @@ -506,7 +506,7 @@ class AExpectedFiles: expected_files.append( '{}.{}.{}'.format(file_prefix, str(frame).rjust( - layer_data["padding"], "0"), + layer_data["padding"], "0"), layer_data["defaultExt"])) return expected_files @@ -642,7 +642,7 @@ class ExpectedFilesArnold(AExpectedFiles): enabled_aovs = [] try: if not (cmds.getAttr('defaultArnoldRenderOptions.aovMode') - and not cmds.getAttr('defaultArnoldDriver.mergeAOVs')): + and not cmds.getAttr('defaultArnoldDriver.mergeAOVs')): # noqa: W503, E501 # AOVs are merged in mutli-channel file return enabled_aovs except ValueError: @@ -763,10 +763,7 @@ class ExpectedFilesVray(AExpectedFiles): if enabled: # todo: find how vray set format for AOVs enabled_aovs.append( - ( - self._get_vray_aov_name(aov), - default_ext) - ) + (self._get_vray_aov_name(aov), default_ext)) return enabled_aovs def _get_vray_aov_name(self, node): From 80aa8a52b755b36a11769d2a4e81ee7fb3b189e8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 23 Mar 2020 11:58:11 +0100 Subject: [PATCH 81/99] validate only important things --- .../maya/publish/validate_frame_range.py | 90 ++++++------------- 1 file changed, 27 insertions(+), 63 deletions(-) diff --git a/pype/plugins/maya/publish/validate_frame_range.py b/pype/plugins/maya/publish/validate_frame_range.py index 0be77644a0..c0a43fe4c7 100644 --- a/pype/plugins/maya/publish/validate_frame_range.py +++ b/pype/plugins/maya/publish/validate_frame_range.py @@ -7,8 +7,9 @@ from maya import cmds class ValidateFrameRange(pyblish.api.InstancePlugin): """Valides the frame ranges. - This is optional validator checking if the frame range matches the one of - asset. + This is optional validator checking if the frame range on instance + matches the one of asset. It also validate render frame range of render + layers Repair action will change everything to match asset. @@ -20,7 +21,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): families = ["animation", "pointcache", "camera", - "render", + "renderlayer", "review", "yeticache"] optional = True @@ -67,69 +68,32 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): handle_start, frame_start, frame_end, handle_end )) - minTime = int(cmds.playbackOptions(minTime=True, query=True)) - maxTime = int(cmds.playbackOptions(maxTime=True, query=True)) - animStartTime = int(cmds.playbackOptions(animationStartTime=True, - query=True)) - animEndTime = int(cmds.playbackOptions(animationEndTime=True, - query=True)) + if "renderlayer" in self.families: - if int(minTime) != inst_start: - errors.append("Start of Maya timeline is set to frame [ {} ] " - " and doesn't match the one set on asset [ {} ]: " - "{}/{}/{}/{} (handle/start/end/handle)".format( - minTime, - frame_start_handle, - handle_start, frame_start, frame_end, handle_end - )) + render_start = int(cmds.getAttr("defaultRenderGlobals.startFrame")) + render_end = int(cmds.getAttr("defaultRenderGlobals.endFrame")) - if int(maxTime) != inst_end: - errors.append("End of Maya timeline is set to frame [ {} ] " - " and doesn't match the one set on asset [ {} ]: " - "{}/{}/{}/{} (handle/start/end/handle)".format( - maxTime, - frame_end_handle, - handle_start, frame_start, frame_end, handle_end - )) + if int(render_start) != inst_start: + errors.append("Render settings start frame is set to [ {} ] " + "and doesn't match the one set on " + "asset [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + int(render_start), + frame_start_handle, + handle_start, frame_start, frame_end, + handle_end + )) - if int(animStartTime) != inst_start: - errors.append("Animation start in Maya is set to frame [ {} ] " - " and doesn't match the one set on asset [ {} ]: " - "{}/{}/{}/{} (handle/start/end/handle)".format( - animStartTime, - frame_start_handle, - handle_start, frame_start, frame_end, handle_end - )) - - if int(animEndTime) != inst_end: - errors.append("Animation start in Maya is set to frame [ {} ] " - " and doesn't match the one set on asset [ {} ]: " - "{}/{}/{}/{} (handle/start/end/handle)".format( - animEndTime, - frame_end_handle, - handle_start, frame_start, frame_end, handle_end - )) - - render_start = int(cmds.getAttr("defaultRenderGlobals.startFrame")) - render_end = int(cmds.getAttr("defaultRenderGlobals.endFrame")) - - if int(render_start) != inst_start: - errors.append("Render settings start frame is set to [ {} ] " - " and doesn't match the one set on asset [ {} ]: " - "{}/{}/{}/{} (handle/start/end/handle)".format( - int(render_start), - frame_start_handle, - handle_start, frame_start, frame_end, handle_end - )) - - if int(render_end) != inst_end: - errors.append("Render settings end frame is set to [ {} ] " - " and doesn't match the one set on asset [ {} ]: " - "{}/{}/{}/{} (handle/start/end/handle)".format( - int(render_end), - frame_end_handle, - handle_start, frame_start, frame_end, handle_end - )) + if int(render_end) != inst_end: + errors.append("Render settings end frame is set to [ {} ] " + "and doesn't match the one set on " + "asset [ {} ]: " + "{}/{}/{}/{} (handle/start/end/handle)".format( + int(render_end), + frame_end_handle, + handle_start, frame_start, frame_end, + handle_end + )) for e in errors: self.log.error(e) From 566d4952486a24cd8e3e867490a9647fac57a582 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 23 Mar 2020 18:06:26 +0100 Subject: [PATCH 82/99] simplified frame range validator --- .../maya/publish/validate_frame_range.py | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/pype/plugins/maya/publish/validate_frame_range.py b/pype/plugins/maya/publish/validate_frame_range.py index c0a43fe4c7..0d51a83cf5 100644 --- a/pype/plugins/maya/publish/validate_frame_range.py +++ b/pype/plugins/maya/publish/validate_frame_range.py @@ -68,33 +68,6 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): handle_start, frame_start, frame_end, handle_end )) - if "renderlayer" in self.families: - - render_start = int(cmds.getAttr("defaultRenderGlobals.startFrame")) - render_end = int(cmds.getAttr("defaultRenderGlobals.endFrame")) - - if int(render_start) != inst_start: - errors.append("Render settings start frame is set to [ {} ] " - "and doesn't match the one set on " - "asset [ {} ]: " - "{}/{}/{}/{} (handle/start/end/handle)".format( - int(render_start), - frame_start_handle, - handle_start, frame_start, frame_end, - handle_end - )) - - if int(render_end) != inst_end: - errors.append("Render settings end frame is set to [ {} ] " - "and doesn't match the one set on " - "asset [ {} ]: " - "{}/{}/{}/{} (handle/start/end/handle)".format( - int(render_end), - frame_end_handle, - handle_start, frame_start, frame_end, - handle_end - )) - for e in errors: self.log.error(e) @@ -103,12 +76,8 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): """ - Repair by calling avalon reset frame range function. This will set - timeline frame range, render settings range and frame information - on instance container to match asset data. + Repair instance container to match asset data. """ - import avalon.maya.interactive - avalon.maya.interactive.reset_frame_range() cmds.setAttr( "{}.frameStart".format(instance.data["name"]), instance.context.data.get("frameStartHandle")) From 4511b915035cdb509562b84d088f7f8834c04c8a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 23 Mar 2020 18:32:20 +0100 Subject: [PATCH 83/99] set avalon project on publish job by environment variable --- .../global/publish/submit_publish_job.py | 3 ++- pype/scripts/publish_filesequence.py | 18 ------------------ 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 556132cd77..9cfeb0762e 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -222,9 +222,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # Transfer the environment from the original job to this dependent # job so they use the same environment - environment = job["Props"].get("Env", {}) environment["PYPE_METADATA_FILE"] = metadata_path + environment["AVALON_PROJECT"] = api.Session.get("AVALON_PROJECT") + i = 0 for index, key in enumerate(environment): if key.upper() in self.enviro_filter: diff --git a/pype/scripts/publish_filesequence.py b/pype/scripts/publish_filesequence.py index fe795564a5..a41d97668e 100644 --- a/pype/scripts/publish_filesequence.py +++ b/pype/scripts/publish_filesequence.py @@ -25,18 +25,6 @@ log.setLevel(logging.DEBUG) error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}" -def _load_json(path): - assert os.path.isfile(path), ("path to json file doesn't exist") - data = None - with open(path, "r") as json_file: - try: - data = json.load(json_file) - except Exception as exc: - log.error( - "Error loading json: " - "{} - Exception: {}".format(path, exc) - ) - return data def __main__(): parser = argparse.ArgumentParser() @@ -90,12 +78,6 @@ def __main__(): paths = kwargs.paths or [os.environ.get("PYPE_METADATA_FILE")] or [os.getcwd()] # noqa - for path in paths: - data = _load_json(path) - log.info("Setting session using data from file") - os.environ["AVALON_PROJECT"] = data["session"]["AVALON_PROJECT"] - break - args = [ os.path.join(pype_root, pype_command), "publish", From f1241415f991f41f06e440b7ce9a677bfa079530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Mon, 23 Mar 2020 18:38:03 +0100 Subject: [PATCH 84/99] fixing flake8 configuration there was duplicated ignore option --- .flake8 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.flake8 b/.flake8 index f28d8cbfc3..b04062ceab 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,6 @@ [flake8] # ignore = D203 -ignore = BLK100 -ignore = W504 +ignore = BLK100, W504 max-line-length = 79 exclude = .git, From d1dd5023571b10934c0b81dc4d8cd31e6983998f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 24 Mar 2020 16:35:37 +0100 Subject: [PATCH 85/99] fix(nuke): validator clam fps float number --- pype/plugins/nuke/publish/validate_script.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pype/plugins/nuke/publish/validate_script.py b/pype/plugins/nuke/publish/validate_script.py index f7dd84d714..36df228ed5 100644 --- a/pype/plugins/nuke/publish/validate_script.py +++ b/pype/plugins/nuke/publish/validate_script.py @@ -74,17 +74,14 @@ class ValidateScript(pyblish.api.InstancePlugin): if "handleEnd" in asset_attributes: handle_end = asset_attributes["handleEnd"] - # Set frame range with handles - # asset_attributes["frameStart"] -= handle_start - # asset_attributes["frameEnd"] += handle_end - if len(str(asset_attributes["fps"])) > 4: - asset_attributes["fps"] = float("{0:.8f}".format(asset_attributes["fps"])) + asset_attributes["fps"] = float("{0:.4f}".format( + asset_attributes["fps"])) # Get values from nukescript script_attributes = { "handleStart": ctx_data["handleStart"], "handleEnd": ctx_data["handleEnd"], - "fps": ctx_data["fps"], + "fps": float("{0:.4f}".format(ctx_data["fps"])), "frameStart": ctx_data["frameStart"], "frameEnd": ctx_data["frameEnd"], "resolutionWidth": ctx_data["resolutionWidth"], From 139add3a45d353c211ef39264d03b589227d9d62 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Mar 2020 17:23:36 +0100 Subject: [PATCH 86/99] it is possible to use `source_timecode` in burnins --- pype/scripts/otio_burnin.py | 48 ++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/pype/scripts/otio_burnin.py b/pype/scripts/otio_burnin.py index 8b52216968..7c94006466 100644 --- a/pype/scripts/otio_burnin.py +++ b/pype/scripts/otio_burnin.py @@ -36,7 +36,8 @@ TIMECODE = ( MISSING_KEY_VALUE = "N/A" CURRENT_FRAME_KEY = "{current_frame}" CURRENT_FRAME_SPLITTER = "_-_CURRENT_FRAME_-_" -TIME_CODE_KEY = "{timecode}" +TIMECODE_KEY = "{timecode}" +SOURCE_TIMECODE_KEY = "{source_timecode}" def _streams(source): @@ -188,10 +189,13 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): if not options.get("fps"): options["fps"] = self.frame_rate - options["timecode"] = ffmpeg_burnins._frames_to_timecode( - frame_start_tc, - self.frame_rate - ) + if isinstance(frame_start_tc, str): + options["timecode"] = frame_start_tc + else: + options["timecode"] = ffmpeg_burnins._frames_to_timecode( + frame_start_tc, + self.frame_rate + ) self._add_burnin(text, align, options, TIMECODE) @@ -412,7 +416,14 @@ def burnins_from_data( data[CURRENT_FRAME_KEY[1:-1]] = CURRENT_FRAME_SPLITTER if frame_start_tc is not None: - data[TIME_CODE_KEY[1:-1]] = TIME_CODE_KEY + data[TIMECODE_KEY[1:-1]] = TIMECODE_KEY + + source_timecode = stream.get("timecode") + if source_timecode is None: + source_timecode = stream.get("tags", {}).get("timecode") + + if source_timecode is not None: + data[SOURCE_TIMECODE_KEY[1:-1]] = SOURCE_TIMECODE_KEY for align_text, value in presets.get('burnins', {}).items(): if not value: @@ -425,8 +436,6 @@ def burnins_from_data( " (Make sure you have new burnin presets)." ).format(str(type(value)), str(value))) - has_timecode = TIME_CODE_KEY in value - align = None align_text = align_text.strip().lower() if align_text == "top_left": @@ -442,6 +451,7 @@ def burnins_from_data( elif align_text == "bottom_right": align = ModifiedBurnins.BOTTOM_RIGHT + has_timecode = TIMECODE_KEY in value # Replace with missing key value if frame_start_tc is not set if frame_start_tc is None and has_timecode: has_timecode = False @@ -449,7 +459,13 @@ def burnins_from_data( "`frame_start` and `frame_start_tc`" " are not set in entered data." ) - value = value.replace(TIME_CODE_KEY, MISSING_KEY_VALUE) + value = value.replace(TIMECODE_KEY, MISSING_KEY_VALUE) + + has_source_timecode = SOURCE_TIMECODE_KEY in value + if source_timecode is None and has_source_timecode: + has_source_timecode = False + log.warning("Source does not have set timecode value.") + value = value.replace(SOURCE_TIMECODE_KEY, MISSING_KEY_VALUE) key_pattern = re.compile(r"(\{.*?[^{0]*\})") @@ -465,10 +481,20 @@ def burnins_from_data( value = value.replace(key, MISSING_KEY_VALUE) # Handle timecode differently + if has_source_timecode: + args = [align, frame_start, frame_end, source_timecode] + if not value.startswith(SOURCE_TIMECODE_KEY): + value_items = value.split(SOURCE_TIMECODE_KEY) + text = value_items[0].format(**data) + args.append(text) + + burnin.add_timecode(*args) + continue + if has_timecode: args = [align, frame_start, frame_end, frame_start_tc] - if not value.startswith(TIME_CODE_KEY): - value_items = value.split(TIME_CODE_KEY) + if not value.startswith(TIMECODE_KEY): + value_items = value.split(TIMECODE_KEY) text = value_items[0].format(**data) args.append(text) From 695351cefcf25e3bc564f4f35fb87c6efeebfcb9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 24 Mar 2020 18:27:11 +0100 Subject: [PATCH 87/99] feat(nks): improving way file's metadata are collected --- .../nukestudio/publish/collect_clips.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/pype/plugins/nukestudio/publish/collect_clips.py b/pype/plugins/nukestudio/publish/collect_clips.py index 6a1dad9a6d..746df67485 100644 --- a/pype/plugins/nukestudio/publish/collect_clips.py +++ b/pype/plugins/nukestudio/publish/collect_clips.py @@ -47,6 +47,16 @@ class CollectClips(api.ContextPlugin): track = item.parent() source = item.source().mediaSource() source_path = source.firstpath() + file_head = source.filenameHead() + file_info = next((f for f in source.fileinfos()), None) + source_first_frame = file_info.startFrame() + is_sequence = False + + if not source.singleFile(): + self.log.info("Single file") + is_sequence = True + source_path = file_info.filename() + effects = [f for f in item.linkedItems() if f.isEnabled() if isinstance(f, hiero.core.EffectTrackItem)] @@ -78,12 +88,6 @@ class CollectClips(api.ContextPlugin): ) ) - try: - head, padding, ext = os.path.basename(source_path).split(".") - source_first_frame = int(padding) - except Exception: - source_first_frame = 0 - data.update({ "name": "{0}_{1}".format(track.name(), item.name()), "item": item, @@ -91,6 +95,8 @@ class CollectClips(api.ContextPlugin): "timecodeStart": str(source.timecodeStart()), "timelineTimecodeStart": str(sequence.timecodeStart()), "sourcePath": source_path, + "sourceFileHead": file_head, + "isSequence": is_sequence, "track": track.name(), "trackIndex": track_index, "sourceFirst": source_first_frame, @@ -101,8 +107,9 @@ class CollectClips(api.ContextPlugin): int(item.sourceIn())) + 1, "clipIn": int(item.timelineIn()), "clipOut": int(item.timelineOut()), - "clipDuration": (int(item.timelineOut()) - - int(item.timelineIn())) + 1, + "clipDuration": ( + int(item.timelineOut()) - int( + item.timelineIn())) + 1, "asset": asset, "family": "clip", "families": [], From c6c4c26a54386b0b05ec147942ecd5ced97fc4f6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 24 Mar 2020 18:27:45 +0100 Subject: [PATCH 88/99] fix(nks): plate single file vs sequence treatment --- .../nukestudio/publish/collect_plates.py | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/pype/plugins/nukestudio/publish/collect_plates.py b/pype/plugins/nukestudio/publish/collect_plates.py index 4ed281f0ee..8a79354bbf 100644 --- a/pype/plugins/nukestudio/publish/collect_plates.py +++ b/pype/plugins/nukestudio/publish/collect_plates.py @@ -147,22 +147,15 @@ class CollectPlatesData(api.InstancePlugin): "version": version }) + source_first_frame = instance.data.get("sourceFirst") + source_file_head = instance.data.get("sourceFileHead") - try: - basename, ext = os.path.splitext(source_file) - head, padding = os.path.splitext(basename) - ext = ext[1:] - padding = padding[1:] - self.log.debug("_ padding: `{}`".format(padding)) - # head, padding, ext = source_file.split('.') - source_first_frame = int(padding) - padding = len(padding) - file = "{head}.%0{padding}d.{ext}".format( - head=head, - padding=padding, - ext=ext - ) - + if instance.data.get("isSequence", False): + self.log.info("Is sequence of files") + file = os.path.basename(source_file) + ext = os.path.splitext(file)[-1][1:] + self.log.debug("source_file_head: `{}`".format(source_file_head)) + head = source_file_head[:-1] start_frame = int(source_first_frame + instance.data["sourceInH"]) duration = int( instance.data["sourceOutH"] - instance.data["sourceInH"]) @@ -170,10 +163,10 @@ class CollectPlatesData(api.InstancePlugin): self.log.debug("start_frame: `{}`".format(start_frame)) self.log.debug("end_frame: `{}`".format(end_frame)) files = [file % i for i in range(start_frame, (end_frame + 1), 1)] - except Exception as e: - self.log.warning("Exception in file: {}".format(e)) - head, ext = os.path.splitext(source_file) - ext = ext[1:] + else: + self.log.info("Is single file") + ext = os.path.splitext(source_file)[-1][1:] + head = source_file_head files = source_file start_frame = instance.data["sourceInH"] end_frame = instance.data["sourceOutH"] From 461441ebf42ab734f741acd764739e51f2d805c8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 24 Mar 2020 19:47:36 +0100 Subject: [PATCH 89/99] fix(nk): build first workfile get jpeg sequences --- pype/nuke/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index 6cd66407d6..cbec8b4300 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -1135,7 +1135,7 @@ class BuildWorkfile(WorkfileSettings): regex_filter=None, version=None, representations=["exr", "dpx", "lutJson", "mov", - "preview", "png"]): + "preview", "png", "jpeg", "jpg"]): """ A short description. From f80e4e123546de2cf40d2c1f0d5c488cc9055f25 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 25 Mar 2020 11:13:54 +0100 Subject: [PATCH 90/99] clean(nuke): pep8 improvments --- pype/nuke/lib.py | 29 +++++++++++++++-------------- setup/nuke/nuke_path/menu.py | 10 +++++----- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index ad2d576da3..ccb4f343db 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -28,7 +28,7 @@ self = sys.modules[__name__] self._project = None -def onScriptLoad(): +def on_script_load(): ''' Callback for ffmpeg support ''' if nuke.env['LINUX']: @@ -39,7 +39,7 @@ def onScriptLoad(): nuke.tcl('load movWriter') -def checkInventoryVersions(): +def check_inventory_versions(): """ Actiual version idetifier of Loaded containers @@ -180,8 +180,8 @@ def format_anatomy(data): padding = int(anatomy.templates['render']['padding']) except KeyError as e: msg = ("`padding` key is not in `render` " - "Anatomy template. Please, add it there and restart " - "the pipeline (padding: \"4\"): `{}`").format(e) + "Anatomy template. Please, add it there and restart " + "the pipeline (padding: \"4\"): `{}`").format(e) log.error(msg) nuke.message(msg) @@ -701,7 +701,8 @@ class WorkfileSettings(object): def set_reads_colorspace(self, reads): """ Setting colorspace to Read nodes - Looping trought all read nodes and tries to set colorspace based on regex rules in presets + Looping trought all read nodes and tries to set colorspace based + on regex rules in presets """ changes = dict() for n in nuke.allNodes(): @@ -873,10 +874,10 @@ class WorkfileSettings(object): if any(x for x in data.values() if x is None): msg = ("Missing set shot attributes in DB." - "\nContact your supervisor!." - "\n\nWidth: `{width}`" - "\nHeight: `{height}`" - "\nPixel Asspect: `{pixel_aspect}`").format(**data) + "\nContact your supervisor!." + "\n\nWidth: `{width}`" + "\nHeight: `{height}`" + "\nPixel Asspect: `{pixel_aspect}`").format(**data) log.error(msg) nuke.message(msg) @@ -895,8 +896,9 @@ class WorkfileSettings(object): ) except Exception as e: bbox = None - msg = ("{}:{} \nFormat:Crop need to be set with dots, example: " - "0.0.1920.1080, /nSetting to default").format(__name__, e) + msg = ("{}:{} \nFormat:Crop need to be set with dots, " + "example: 0.0.1920.1080, " + "/nSetting to default").format(__name__, e) log.error(msg) nuke.message(msg) @@ -1037,7 +1039,8 @@ class BuildWorkfile(WorkfileSettings): """ Building first version of workfile. - Settings are taken from presets and db. It will add all subsets in last version for defined representaions + Settings are taken from presets and db. It will add all subsets + in last version for defined representaions Arguments: variable (type): description @@ -1265,8 +1268,6 @@ class BuildWorkfile(WorkfileSettings): representation (dict): avalon db entity """ - context = representation["context"] - loader_name = "LoadLuts" loader_plugin = None diff --git a/setup/nuke/nuke_path/menu.py b/setup/nuke/nuke_path/menu.py index 15702fa364..be4f39b542 100644 --- a/setup/nuke/nuke_path/menu.py +++ b/setup/nuke/nuke_path/menu.py @@ -4,8 +4,8 @@ import KnobScripter from pype.nuke.lib import ( writes_version_sync, - onScriptLoad, - checkInventoryVersions + on_script_load, + check_inventory_versions ) import nuke @@ -15,9 +15,9 @@ log = Logger().get_logger(__name__, "nuke") # nuke.addOnScriptSave(writes_version_sync) -nuke.addOnScriptSave(onScriptLoad) -nuke.addOnScriptLoad(checkInventoryVersions) -nuke.addOnScriptSave(checkInventoryVersions) +nuke.addOnScriptSave(on_script_load) +nuke.addOnScriptLoad(check_inventory_versions) +nuke.addOnScriptSave(check_inventory_versions) # nuke.addOnScriptSave(writes_version_sync) log.info('Automatic syncing of write file knob to script version') From 3d0b7e49c7cadc849b3d165443e16923ae33309f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 25 Mar 2020 13:52:40 +0100 Subject: [PATCH 91/99] PEP fixes --- .flake8 | 2 +- pype/ftrack/lib/ftrack_app_handler.py | 7 +++---- pype/plugins/unreal/load/load_staticmeshfbx.py | 4 ++-- pype/unreal/lib.py | 10 +++++----- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.flake8 b/.flake8 index b04062ceab..f9c81de232 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,6 @@ [flake8] # ignore = D203 -ignore = BLK100, W504 +ignore = BLK100, W504, W503 max-line-length = 79 exclude = .git, diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py index 58c550b3dd..b5576ae046 100644 --- a/pype/ftrack/lib/ftrack_app_handler.py +++ b/pype/ftrack/lib/ftrack_app_handler.py @@ -277,7 +277,7 @@ class AppAction(BaseHandler): 'success': False, 'message': "Hook didn't finish successfully {0}" .format(self.label) - } + } if sys.platform == "win32": @@ -290,7 +290,7 @@ class AppAction(BaseHandler): # Run SW if was found executable if execfile is not None: - popen = avalonlib.launch( + avalonlib.launch( executable=execfile, args=[], environment=env ) else: @@ -298,8 +298,7 @@ class AppAction(BaseHandler): 'success': False, 'message': "We didn't found launcher for {0}" .format(self.label) - } - pass + } if sys.platform.startswith('linux'): execfile = os.path.join(path.strip('"'), self.executable) diff --git a/pype/plugins/unreal/load/load_staticmeshfbx.py b/pype/plugins/unreal/load/load_staticmeshfbx.py index 61e765f7c2..4c27f9aa92 100644 --- a/pype/plugins/unreal/load/load_staticmeshfbx.py +++ b/pype/plugins/unreal/load/load_staticmeshfbx.py @@ -37,7 +37,7 @@ class StaticMeshFBXLoader(api.Loader): tools = unreal.AssetToolsHelpers().get_asset_tools() temp_dir, temp_name = tools.create_unique_asset_name( - "/Game/{}".format(name), "_TMP" + "/Game/{}".format(name), "_TMP" ) unreal.EditorAssetLibrary.make_directory(temp_dir) @@ -95,7 +95,7 @@ class StaticMeshFBXLoader(api.Loader): container["objectName"]) # update metadata avalon_unreal.imprint( - container_path, {"_id": str(representation["_id"])}) + container_path, {"_id": str(representation["_id"])}) def remove(self, container): unreal.EditorAssetLibrary.delete_directory(container["namespace"]) diff --git a/pype/unreal/lib.py b/pype/unreal/lib.py index 8f87fdbf4e..0b049c8b1d 100644 --- a/pype/unreal/lib.py +++ b/pype/unreal/lib.py @@ -214,11 +214,11 @@ def create_unreal_project(project_name: str, # sources at start data["Modules"] = [{ - "Name": project_name, - "Type": "Runtime", - "LoadingPhase": "Default", - "AdditionalDependencies": ["Engine"], - }] + "Name": project_name, + "Type": "Runtime", + "LoadingPhase": "Default", + "AdditionalDependencies": ["Engine"], + }] if preset["install_unreal_python_engine"]: # now we need to fix python path in: From fc86c46af00598b67ea633a37e0ac12c6fa59882 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 25 Mar 2020 14:03:58 +0100 Subject: [PATCH 92/99] missing_options_from_look_loader --- pype/plugins/maya/load/load_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/maya/load/load_look.py b/pype/plugins/maya/load/load_look.py index 04ac9b23e4..c31b7c5fe0 100644 --- a/pype/plugins/maya/load/load_look.py +++ b/pype/plugins/maya/load/load_look.py @@ -16,7 +16,7 @@ class LookLoader(pype.maya.plugin.ReferenceLoader): icon = "code-fork" color = "orange" - def process_reference(self, context, name, namespace, data): + def process_reference(self, context, name, namespace, options): """ Load and try to assign Lookdev to nodes based on relationship data Args: From 599a227359e1bc57271215441af8e22f957bdd90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Mar 2020 13:18:26 +0100 Subject: [PATCH 93/99] fix queue import --- pype/scripts/slates/lib.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/scripts/slates/lib.py b/pype/scripts/slates/lib.py index a73f87e82f..154c689349 100644 --- a/pype/scripts/slates/lib.py +++ b/pype/scripts/slates/lib.py @@ -1,5 +1,9 @@ import logging -from queue import Queue + +try: + from queue import Queue +except Exception: + from Queue import Queue from .slate_base.main_frame import MainFrame from .slate_base.layer import Layer From 9ca3541c827353f4da8656853f6469f2ea7ab320 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Mar 2020 15:30:46 +0100 Subject: [PATCH 94/99] slates are more like package, fixed few bugs and example is in specific file with more data of example --- pype/scripts/slates/__init__.py | 2 + pype/scripts/slates/__main__.py | 10 + pype/scripts/slates/slate_base/api.py | 15 ++ pype/scripts/slates/slate_base/example.py | 254 ++++++++++++++++++++ pype/scripts/slates/slate_base/items.py | 1 + pype/scripts/slates/{ => slate_base}/lib.py | 69 ++---- 6 files changed, 300 insertions(+), 51 deletions(-) create mode 100644 pype/scripts/slates/__init__.py create mode 100644 pype/scripts/slates/__main__.py create mode 100644 pype/scripts/slates/slate_base/api.py create mode 100644 pype/scripts/slates/slate_base/example.py rename pype/scripts/slates/{ => slate_base}/lib.py (63%) diff --git a/pype/scripts/slates/__init__.py b/pype/scripts/slates/__init__.py new file mode 100644 index 0000000000..52937708ea --- /dev/null +++ b/pype/scripts/slates/__init__.py @@ -0,0 +1,2 @@ +from . import slate_base +from .slate_base import api diff --git a/pype/scripts/slates/__main__.py b/pype/scripts/slates/__main__.py new file mode 100644 index 0000000000..29282d3226 --- /dev/null +++ b/pype/scripts/slates/__main__.py @@ -0,0 +1,10 @@ +from slate_base import api + + +def main(in_args=None): + # TODO proper argument handling + api.example() + + +if __name__ == "__main__": + main() diff --git a/pype/scripts/slates/slate_base/api.py b/pype/scripts/slates/slate_base/api.py new file mode 100644 index 0000000000..cd64c68134 --- /dev/null +++ b/pype/scripts/slates/slate_base/api.py @@ -0,0 +1,15 @@ +from .font_factory import FontFactory +from .base import BaseObj, load_default_style +from .main_frame import MainFrame +from .layer import Layer +from .items import ( + BaseItem, + ItemImage, + ItemRectangle, + ItemPlaceHolder, + ItemText, + ItemTable, + TableField +) +from .lib import create_slates +from .example import example diff --git a/pype/scripts/slates/slate_base/example.py b/pype/scripts/slates/slate_base/example.py new file mode 100644 index 0000000000..560f9ec02d --- /dev/null +++ b/pype/scripts/slates/slate_base/example.py @@ -0,0 +1,254 @@ +# import sys +# sys.append(r"PATH/TO/PILLOW/PACKAGE") + +from . import api + + +def example(): + """Example data to demontrate function. + + It is required to fill "destination_path", "thumbnail_path" + and "color_bar_path" in `example_fill_data` to be able to execute. + """ + + example_fill_data = { + "destination_path": "PATH/TO/OUTPUT/FILE", + "project": { + "name": "Testing project" + }, + "intent": "WIP", + "version_name": "seq01_sh0100_compositing_v01", + "date": "2019-08-09", + "shot_type": "2d comp", + "submission_note": ( + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit." + " Aenean commodo ligula eget dolor. Aenean massa." + " Cum sociis natoque penatibus et magnis dis parturient montes," + " nascetur ridiculus mus. Donec quam felis, ultricies nec," + " pellentesque eu, pretium quis, sem. Nulla consequat massa quis" + " enim. Donec pede justo, fringilla vel," + " aliquet nec, vulputate eget, arcu." + ), + "thumbnail_path": "PATH/TO/THUMBNAIL/FILE", + "color_bar_path": "PATH/TO/COLOR/BAR/FILE", + "vendor": "Our Studio", + "shot_name": "sh0100", + "frame_start": 1001, + "frame_end": 1004, + "duration": 3 + } + + example_presets = {"example_HD": { + "width": 1920, + "height": 1080, + "destination_path": "{destination_path}", + "style": { + "*": { + "font-family": "arial", + "font-color": "#ffffff", + "font-bold": False, + "font-italic": False, + "bg-color": "#0077ff", + "alignment-horizontal": "left", + "alignment-vertical": "top" + }, + "layer": { + "padding": 0, + "margin": 0 + }, + "rectangle": { + "padding": 0, + "margin": 0, + "bg-color": "#E9324B", + "fill": True + }, + "main_frame": { + "padding": 0, + "margin": 0, + "bg-color": "#252525" + }, + "table": { + "padding": 0, + "margin": 0, + "bg-color": "transparent" + }, + "table-item": { + "padding": 5, + "padding-bottom": 10, + "margin": 0, + "bg-color": "#212121", + "bg-alter-color": "#272727", + "font-color": "#dcdcdc", + "font-bold": False, + "font-italic": False, + "alignment-horizontal": "left", + "alignment-vertical": "top", + "word-wrap": False, + "ellide": True, + "max-lines": 1 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "font-bold": True, + "ellide": False, + "word-wrap": True, + "max-lines": None + }, + "table-item-col[1]": { + "font-size": 40, + "padding-left": 10 + }, + "#colorbar": { + "bg-color": "#9932CC" + } + }, + "items": [{ + "type": "layer", + "direction": 1, + "name": "MainLayer", + "style": { + "#MainLayer": { + "width": 1094, + "height": 1000, + "margin": 25, + "padding": 0 + }, + "#LeftSide": { + "margin-right": 25 + } + }, + "items": [{ + "type": "layer", + "name": "LeftSide", + "items": [{ + "type": "layer", + "direction": 1, + "style": { + "table-item": { + "bg-color": "transparent", + "padding-bottom": 20 + }, + "table-item-col[0]": { + "font-size": 20, + "font-color": "#898989", + "alignment-horizontal": "right" + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "font-bold": True, + "font-size": 40 + } + }, + "items": [{ + "type": "table", + "values": [ + ["Show:", "{project[name]}"] + ], + "style": { + "table-item-field[0:0]": { + "width": 150 + }, + "table-item-field[0:1]": { + "width": 580 + } + } + }, { + "type": "table", + "values": [ + ["Submitting For:", "{intent}"] + ], + "style": { + "table-item-field[0:0]": { + "width": 160 + }, + "table-item-field[0:1]": { + "width": 218, + "alignment-horizontal": "right" + } + } + }] + }, { + "type": "rectangle", + "style": { + "bg-color": "#bc1015", + "width": 1108, + "height": 5, + "fill": True + } + }, { + "type": "table", + "use_alternate_color": True, + "values": [ + ["Version name:", "{version_name}"], + ["Date:", "{date}"], + ["Shot Types:", "{shot_type}"], + ["Submission Note:", "{submission_note}"] + ], + "style": { + "table-item": { + "padding-bottom": 20 + }, + "table-item-field[0:1]": { + "font-bold": True + }, + "table-item-field[3:0]": { + "word-wrap": True, + "ellide": True, + "max-lines": 4 + }, + "table-item-col[0]": { + "alignment-horizontal": "right", + "width": 150 + }, + "table-item-col[1]": { + "alignment-horizontal": "left", + "width": 958 + } + } + }] + }, { + "type": "layer", + "name": "RightSide", + "items": [{ + "type": "placeholder", + "name": "thumbnail", + "path": "{thumbnail_path}", + "style": { + "width": 730, + "height": 412 + } + }, { + "type": "placeholder", + "name": "colorbar", + "path": "{color_bar_path}", + "return_data": True, + "style": { + "width": 730, + "height": 55 + } + }, { + "type": "table", + "use_alternate_color": True, + "values": [ + ["Vendor:", "{vendor}"], + ["Shot Name:", "{shot_name}"], + ["Frames:", "{frame_start} - {frame_end} ({duration})"] + ], + "style": { + "table-item-col[0]": { + "alignment-horizontal": "left", + "width": 200 + }, + "table-item-col[1]": { + "alignment-horizontal": "right", + "width": 530, + "font-size": 30 + } + } + }] + }] + }] + }} + + api.create_slates(example_fill_data, "example_HD", example_presets) diff --git a/pype/scripts/slates/slate_base/items.py b/pype/scripts/slates/slate_base/items.py index ea31443f80..1183d73305 100644 --- a/pype/scripts/slates/slate_base/items.py +++ b/pype/scripts/slates/slate_base/items.py @@ -1,3 +1,4 @@ +import os import re from PIL import Image diff --git a/pype/scripts/slates/lib.py b/pype/scripts/slates/slate_base/lib.py similarity index 63% rename from pype/scripts/slates/lib.py rename to pype/scripts/slates/slate_base/lib.py index 154c689349..3c7a465e98 100644 --- a/pype/scripts/slates/lib.py +++ b/pype/scripts/slates/slate_base/lib.py @@ -1,17 +1,19 @@ import logging - try: from queue import Queue except Exception: from Queue import Queue -from .slate_base.main_frame import MainFrame -from .slate_base.layer import Layer -from .slate_base.items import ( +from .main_frame import MainFrame +from .layer import Layer +from .items import ( ItemTable, ItemImage, ItemRectangle, ItemPlaceHolder ) -from pypeapp import config +try: + from pypeapp.config import get_presets +except Exception: + get_presets = dict log = logging.getLogger(__name__) @@ -19,17 +21,20 @@ log = logging.getLogger(__name__) RequiredSlateKeys = ["width", "height", "destination_path"] -def create_slates(fill_data, slate_name): - presets = config.get_presets() - slate_presets = ( - presets - .get("tools", {}) - .get("slates") - ) or {} +def create_slates(fill_data, slate_name, slate_presets=None): + if slate_presets is None: + presets = get_presets() + slate_presets = ( + presets + .get("tools", {}) + .get("slates") + ) or {} slate_data = slate_presets.get(slate_name) if not slate_data: - log.error("Slate data of <{}> does not exists.") + log.error( + "Name \"{}\" was not found in slate presets.".format(slate_name) + ) return False missing_keys = [] @@ -111,41 +116,3 @@ def create_slates(fill_data, slate_name): main.draw() log.debug("Slate creation finished") - - -def example(): - # import sys - # sys.append(r"PATH/TO/PILLOW/PACKAGE") - # sys.append(r"PATH/TO/PYPE-SETUP") - - fill_data = { - "destination_path": "PATH/TO/OUTPUT/FILE", - "project": { - "name": "Testing project" - }, - "intent": "WIP", - "version_name": "seq01_sh0100_compositing_v01", - "date": "2019-08-09", - "shot_type": "2d comp", - "submission_note": ( - "Lorem ipsum dolor sit amet, consectetuer adipiscing elit." - " Aenean commodo ligula eget dolor. Aenean massa." - " Cum sociis natoque penatibus et magnis dis parturient montes," - " nascetur ridiculus mus. Donec quam felis, ultricies nec," - " pellentesque eu, pretium quis, sem. Nulla consequat massa quis" - " enim. Donec pede justo, fringilla vel," - " aliquet nec, vulputate eget, arcu." - ), - "thumbnail_path": "PATH/TO/THUMBNAIL/FILE", - "color_bar_path": "PATH/TO/COLOR/BAR/FILE", - "vendor": "Our Studio", - "shot_name": "sh0100", - "frame_start": 1001, - "frame_end": 1004, - "duration": 3 - } - create_slates(fill_data, "example_HD") - - -if __name__ == "__main__": - example() From f152b9b6a7c691d962344519be36cf1d12b84b6f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 26 Mar 2020 16:19:08 +0100 Subject: [PATCH 95/99] do not allow token in Redshift --- .../maya/publish/validate_rendersettings.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/pype/plugins/maya/publish/validate_rendersettings.py b/pype/plugins/maya/publish/validate_rendersettings.py index c98f0f8cdc..67239d4790 100644 --- a/pype/plugins/maya/publish/validate_rendersettings.py +++ b/pype/plugins/maya/publish/validate_rendersettings.py @@ -13,13 +13,17 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): """Validates the global render settings * File Name Prefix must start with: `maya/` - all other token are customizable but sane values are: + all other token are customizable but sane values for Arnold are: `maya///_` - token is supported also, usefull for multiple renderable + token is supported also, useful for multiple renderable cameras per render layer. + For Redshift omit token. Redshift will append it + automatically if AOVs are enabled and if you user Multipart EXR + it doesn't make much sense. + * Frame Padding must be: * default: 4 @@ -127,8 +131,13 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): # no vray checks implemented yet pass elif renderer == "redshift": - # no redshift check implemented yet - pass + if re.search(cls.R_AOV_TOKEN, prefix): + invalid = True + cls.log.error("Do not use AOV token [ {} ] - " + "Redshift automatically append AOV name and " + "it doesn't make much sense with " + "Multipart EXR".format(prefix)) + elif renderer == "renderman": file_prefix = cmds.getAttr("rmanGlobals.imageFileFormat") dir_prefix = cmds.getAttr("rmanGlobals.imageOutputDir") @@ -143,8 +152,8 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): dir_prefix)) else: - multichannel = cmds.getAttr("defaultArnoldDriver.mergeAOVs") - if multichannel: + multipart = cmds.getAttr("defaultArnoldDriver.mergeAOVs") + if multipart: if re.search(cls.R_AOV_TOKEN, prefix): invalid = True cls.log.error("Wrong image prefix [ {} ] - " From a5d313492f7f157513c124894289f297a7ec237c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Mar 2020 16:27:52 +0100 Subject: [PATCH 96/99] hopefully fix creating items --- pype/scripts/slates/slate_base/items.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/scripts/slates/slate_base/items.py b/pype/scripts/slates/slate_base/items.py index 1183d73305..6d19fc6a0c 100644 --- a/pype/scripts/slates/slate_base/items.py +++ b/pype/scripts/slates/slate_base/items.py @@ -36,8 +36,8 @@ class ItemImage(BaseItem): obj_type = "image" def __init__(self, image_path, *args, **kwargs): - super(ItemImage, self).__init__(*args, **kwargs) self.image_path = image_path + super(ItemImage, self).__init__(*args, **kwargs) def fill_data_format(self): if re.match(self.fill_data_regex, self.image_path): @@ -143,8 +143,8 @@ class ItemText(BaseItem): obj_type = "text" def __init__(self, value, *args, **kwargs): - super(ItemText, self).__init__(*args, **kwargs) self.value = value + super(ItemText, self).__init__(*args, **kwargs) def draw(self, image, drawer): bg_color = self.style["bg-color"] From 41e6eb05caf8bf053bc2d8183a484be278436f20 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 26 Mar 2020 17:06:00 +0100 Subject: [PATCH 97/99] fix(nuke): build first workfile was not accepting jpeg sequences --- pype/nuke/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py index ad2d576da3..3ff9c6d397 100644 --- a/pype/nuke/lib.py +++ b/pype/nuke/lib.py @@ -1135,7 +1135,7 @@ class BuildWorkfile(WorkfileSettings): regex_filter=None, version=None, representations=["exr", "dpx", "lutJson", "mov", - "preview", "png"]): + "preview", "png", "jpeg", "jpg"]): """ A short description. From 3e216dc3b39026515aa8ff2d86b25224ce68bb8f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 27 Mar 2020 12:04:00 +0100 Subject: [PATCH 98/99] slates can receive data though command line like burnins and it is possible to save slates metadata to json --- pype/scripts/slates/__main__.py | 13 +++++-- pype/scripts/slates/slate_base/lib.py | 56 +++++++++++++++++++++------ 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/pype/scripts/slates/__main__.py b/pype/scripts/slates/__main__.py index 29282d3226..43efcfbb06 100644 --- a/pype/scripts/slates/__main__.py +++ b/pype/scripts/slates/__main__.py @@ -1,10 +1,17 @@ +import json from slate_base import api def main(in_args=None): - # TODO proper argument handling - api.example() + data_arg = in_args[-1] + in_data = json.loads(data_arg) + api.create_slates( + in_data["fill_data"], + in_data.get("slate_name"), + in_data.get("slate_data"), + in_data.get("data_output_json") + ) if __name__ == "__main__": - main() + main(sys.argv) diff --git a/pype/scripts/slates/slate_base/lib.py b/pype/scripts/slates/slate_base/lib.py index 3c7a465e98..d9f8ad6d42 100644 --- a/pype/scripts/slates/slate_base/lib.py +++ b/pype/scripts/slates/slate_base/lib.py @@ -1,3 +1,5 @@ +import os +import json import logging try: from queue import Queue @@ -21,21 +23,36 @@ log = logging.getLogger(__name__) RequiredSlateKeys = ["width", "height", "destination_path"] -def create_slates(fill_data, slate_name, slate_presets=None): - if slate_presets is None: - presets = get_presets() +# TODO proper documentation +def create_slates( + fill_data, slate_name=None, slate_data=None, data_output_json=None +): + """Implmentation for command line executing. + + Data for slates are by defaule taken from presets. That requires to enter, + `slate_name`. If `slate_data` are entered then they are used. + + `data_output` should be path to json file where data will be collected. + """ + if slate_data is None and slate_name is None: + raise TypeError( + "`create_slates` expects to enter data for slates or name" + " of slate preset." + ) + + elif slate_data is None: slate_presets = ( - presets + get_presets() .get("tools", {}) .get("slates") ) or {} - slate_data = slate_presets.get(slate_name) - - if not slate_data: - log.error( - "Name \"{}\" was not found in slate presets.".format(slate_name) - ) - return False + slate_data = slate_presets.get(slate_name) + if slate_data is None: + raise ValueError( + "Preset name \"{}\" was not found in slate presets.".format( + slate_name + ) + ) missing_keys = [] for key in RequiredSlateKeys: @@ -116,3 +133,20 @@ def create_slates(fill_data, slate_name, slate_presets=None): main.draw() log.debug("Slate creation finished") + + if not data_output_json: + return + + if not data_output_json.endswith(".json"): + raise ValueError("Output path must be .json file.") + + data_output_json_dir = os.path.dirname(data_output_json) + if not os.path.exists(data_output_json_dir): + log.info("Creating folder \"{}\"".format(data_output_json_dir)) + os.makedirs(data_output_json_dir) + + output_data = main.collect_data() + with open(data_output_json, "w") as json_file: + json_file.write(json.dumps(output_data, indent=4)) + + log.info("Metadata collected in \"{}\".".format(data_output_json)) From 9a749a03a950afbcccb8467f6308f0d7456de4ad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 27 Mar 2020 12:06:04 +0100 Subject: [PATCH 99/99] added important import --- pype/scripts/slates/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/scripts/slates/__main__.py b/pype/scripts/slates/__main__.py index 43efcfbb06..bd49389d84 100644 --- a/pype/scripts/slates/__main__.py +++ b/pype/scripts/slates/__main__.py @@ -1,3 +1,4 @@ +import sys import json from slate_base import api