# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# ---------------------------- ADAPTIVE DUPLIFACES --------------------------- #
# ------------------------------- version 0.84 ------------------------------- #
#                                                                              #
# Creates duplicates of selected mesh to active morphing the shape according   #
# to target faces.                                                             #
#                                                                              #
#                    (c)  Alessandro Zomparelli                                #
#                             (2017)                                           #
#                                                                              #
# http://www.co-de-it.com/                                                     #
#                                                                              #
# ############################################################################ #


import bpy
from bpy.types import (
        Operator,
        Panel,
        PropertyGroup,
        )
from bpy.props import (
        BoolProperty,
        EnumProperty,
        FloatProperty,
        IntProperty,
        StringProperty,
        )
from mathutils import Vector
import numpy as np
from math import sqrt
import random


def lerp(a, b, t):
    return a + (b - a) * t


def lerp2(v1, v2, v3, v4, v):
    v12 = v1 + (v2 - v1) * v.x
    v43 = v4 + (v3 - v4) * v.x
    return v12 + (v43 - v12) * v.y


def lerp3(v1, v2, v3, v4, v):
    loc = lerp2(v1.co, v2.co, v3.co, v4.co, v)
    nor = lerp2(v1.normal, v2.normal, v3.normal, v4.normal, v)
    nor.normalize()
    return loc + nor * v.z


def tassellate(ob0, ob1, offset, zscale, gen_modifiers, com_modifiers, mode,
               scale_mode, rotation_mode, rand_seed, fill_mode,
               bool_vertex_group, bool_selection, bool_shapekeys):
    random.seed(rand_seed)
    old_me0 = ob0.data      # Store generator mesh

    if gen_modifiers:       # Apply generator modifiers
        me0 = ob0.to_mesh(bpy.context.scene, apply_modifiers=True,
                          settings='PREVIEW')
    else:
        me0 = ob0.data
    ob0.data = me0
    base_polygons = []

    # Check if zero faces are selected
    if bool_selection:
        for p in ob0.data.polygons:
            if p.select:
                base_polygons.append(p)
    else:
        base_polygons = ob0.data.polygons
    if len(base_polygons) == 0:
        return 0

    # Apply component modifiers
    if com_modifiers:
        me1 = ob1.to_mesh(bpy.context.scene, apply_modifiers=True,
                          settings='PREVIEW')
    else:
        me1 = ob1.data

    verts0 = me0.vertices   # Collect generator vertices

    # Component statistics
    n_verts = len(me1.vertices)
    # n_edges = len(me1.edges)
    # n_faces = len(me1.polygons)

    # Component transformations
    # loc = ob1.location
    # dim = ob1.dimensions
    # scale = ob1.scale

    # Create empty lists
    new_verts = []
    new_edges = []
    new_faces = []
    new_verts_np = np.array(())

    # Component bounding box
    min_c = Vector((0, 0, 0))
    max_c = Vector((0, 0, 0))
    first = True
    for v in me1.vertices:
        vert = v.co
        if vert[0] < min_c[0] or first:
            min_c[0] = vert[0]
        if vert[1] < min_c[1] or first:
            min_c[1] = vert[1]
        if vert[2] < min_c[2] or first:
            min_c[2] = vert[2]
        if vert[0] > max_c[0] or first:
            max_c[0] = vert[0]
        if vert[1] > max_c[1] or first:
            max_c[1] = vert[1]
        if vert[2] > max_c[2] or first:
            max_c[2] = vert[2]
        first = False
    bb = max_c - min_c

    # adaptive XY
    verts1 = []
    for v in me1.vertices:
        if mode == "ADAPTIVE":
            vert = v.co - min_c  # (ob1.matrix_world * v.co) - min_c
            vert[0] = (vert[0] / bb[0] if bb[0] != 0 else 0.5)
            vert[1] = (vert[1] / bb[1] if bb[1] != 0 else 0.5)
            vert[2] = (vert[2] + (-0.5 + offset * 0.5) * bb[2]) * zscale
        else:
            vert = v.co.xyz
            vert[2] = (vert[2] - min_c[2] + (-0.5 + offset * 0.5) * bb[2]) * zscale
        verts1.append(vert)

    # component vertices
    vs1 = np.array([v for v in verts1]).reshape(len(verts1), 3, 1)
    vx = vs1[:, 0]
    vy = vs1[:, 1]
    vz = vs1[:, 2]

    # Component polygons
    fs1 = [[i for i in p.vertices] for p in me1.polygons]
    new_faces = fs1[:]

    # Component edges
    es1 = [[i for i in e.vertices] for e in me1.edges if e.is_loose]
    new_edges = es1[:]

    # SHAPE KEYS
    shapekeys = []
    do_shapekeys = False
    if me1.shape_keys is not None and bool_shapekeys:
        if len(me1.shape_keys.key_blocks) > 1:
            do_shapekeys = True

            # Read active key
            active_key = ob1.active_shape_key_index
            if active_key == 0:
                active_key = 1

            for v in me1.shape_keys.key_blocks[active_key].data:
                if mode == "ADAPTIVE":
                    vert = v.co - min_c
                    vert[0] = vert[0] / bb[0]
                    vert[1] = vert[1] / bb[1]
                    vert[2] = (vert[2] + (-0.5 + offset * 0.5) * bb[2]) * zscale
                else:
                    vert = v.co.xyz
                    vert[2] = (vert[2] - min_c[2] + (-0.5 + offset * 0.5) * bb[2]) * \
                              zscale
                shapekeys.append(vert)

            # Component vertices
            key1 = np.array([v for v in shapekeys]).reshape(len(shapekeys), 3, 1)
            vx_key = key1[:, 0]
            vy_key = key1[:, 1]
            vz_key = key1[:, 2]

    # Active vertex group
    if bool_vertex_group:
        try:
            weight = []
            group_index = ob0.vertex_groups.active_index
            active_vertex_group = ob0.vertex_groups[group_index]
            for v in me0.vertices:
                try:
                    weight.append(active_vertex_group.weight(v.index))
                except:
                    weight.append(0)
        except:
            bool_vertex_group = False

    # FAN tessellation mode
    if fill_mode == 'FAN':
        fan_verts = [v.co.to_tuple() for v in me0.vertices]
        fan_polygons = []
        # selected_faces = []
        for p in base_polygons:
            # if bool_selection and not p.select: continue
            fan_center = Vector((0, 0, 0))
            for v in p.vertices:
                fan_center += me0.vertices[v].co
            fan_center /= len(p.vertices)
            last_vert = len(fan_verts)
            fan_verts.append(fan_center.to_tuple())

            # Vertex Group
            if bool_vertex_group:
                center_weight = sum([weight[i] for i in p.vertices]) / len(p.vertices)
                weight.append(center_weight)

            for i in range(len(p.vertices)):
                fan_polygons.append((p.vertices[i],
                                     p.vertices[(i + 1) % len(p.vertices)],
                                     last_vert, last_vert))
                # if bool_selection: selected_faces.append(p.select)
        fan_me = bpy.data.meshes.new('Fan.Mesh')
        fan_me.from_pydata(tuple(fan_verts), [], tuple(fan_polygons))
        me0 = fan_me
        verts0 = me0.vertices
        base_polygons = me0.polygons
        """
        for i in range(len(selected_faces)):
            fan_me.polygons[i].select = selected_faces[i]
        """
    count = 0   # necessary for UV calculation

    # TESSELLATION
    j = 0
    for p in base_polygons:
        # Random rotation
        if rotation_mode == 'RANDOM':
            shifted_vertices = []
            n_poly_verts = len(p.vertices)
            rand = random.randint(0, n_poly_verts)
            for i in range(n_poly_verts):
                shifted_vertices.append(p.vertices[(i + rand) % n_poly_verts])
            vs0 = np.array([verts0[i].co for i in shifted_vertices])
            nvs0 = np.array([verts0[i].normal for i in shifted_vertices])
            # vertex weight
            if bool_vertex_group:
                ws0 = []
                for i in shifted_vertices:
                    try:
                        ws0.append(weight[i])
                    except:
                        ws0.append(0)

                ws0 = np.array(ws0)

        # UV rotation
        elif rotation_mode == 'UV' and len(ob0.data.uv_layers) > 0 and \
                fill_mode != 'FAN':
            i = p.index
            v01 = (me0.uv_layers.active.data[count].uv +
                   me0.uv_layers.active.data[count + 1].uv)
            if len(p.vertices) > 3:
                v32 = (me0.uv_layers.active.data[count + 3].uv +
                       me0.uv_layers.active.data[count + 2].uv)
            else:
                v32 = (me0.uv_layers.active.data[count].uv +
                       me0.uv_layers.active.data[count + 2].uv)
            v0132 = v32 - v01
            v0132.normalize()

            v12 = (me0.uv_layers.active.data[count + 1].uv +
                   me0.uv_layers.active.data[count + 2].uv)
            if len(p.vertices) > 3:
                v03 = (me0.uv_layers.active.data[count].uv +
                       me0.uv_layers.active.data[count + 3].uv)
            else:
                v03 = (me0.uv_layers.active.data[count].uv +
                       me0.uv_layers.active.data[count].uv)
            v1203 = v03 - v12
            v1203.normalize()

            vertUV = []
            dot1203 = v1203.x
            dot0132 = v0132.x
            if(abs(dot1203) < abs(dot0132)):
                if (dot0132 > 0):
                    vertUV = p.vertices[1:] + p.vertices[:1]
                else:
                    vertUV = p.vertices[3:] + p.vertices[:3]
            else:
                if(dot1203 < 0):
                    vertUV = p.vertices[:]
                else:
                    vertUV = p.vertices[2:] + p.vertices[:2]
            vs0 = np.array([verts0[i].co for i in vertUV])
            nvs0 = np.array([verts0[i].normal for i in vertUV])

            # Vertex weight
            if bool_vertex_group:
                ws0 = []
                for i in vertUV:
                    try:
                        ws0.append(weight[i])
                    except:
                        ws0.append(0)
                ws0 = np.array(ws0)

            count += len(p.vertices)

        # Default rotation
        else:
            vs0 = np.array([verts0[i].co for i in p.vertices])
            nvs0 = np.array([verts0[i].normal for i in p.vertices])
            # Vertex weight
            if bool_vertex_group:
                ws0 = []
                for i in p.vertices:
                    try:
                        ws0.append(weight[i])
                    except:
                        ws0.append(0)
                ws0 = np.array(ws0)

        # considering only 4 vertices
        vs0 = np.array((vs0[0], vs0[1], vs0[2], vs0[-1]))
        nvs0 = np.array((nvs0[0], nvs0[1], nvs0[2], nvs0[-1]))

        # remapped vertex coordinates
        v0 = vs0[0] + (vs0[1] - vs0[0]) * vx
        v1 = vs0[3] + (vs0[2] - vs0[3]) * vx
        v2 = v0 + (v1 - v0) * vy

        # remapped vertex normal
        nv0 = nvs0[0] + (nvs0[1] - nvs0[0]) * vx
        nv1 = nvs0[3] + (nvs0[2] - nvs0[3]) * vx
        nv2 = nv0 + (nv1 - nv0) * vy

        # vertex z to normal
        v3 = v2 + nv2 * vz * (sqrt(p.area) if scale_mode == "ADAPTIVE" else 1)

        if bool_vertex_group:
            ws0 = np.array((ws0[0], ws0[1], ws0[2], ws0[-1]))
            # Interpolate vertex weight
            w0 = ws0[0] + (ws0[1] - ws0[0]) * vx
            w1 = ws0[3] + (ws0[2] - ws0[3]) * vx
            w2 = w0 + (w1 - w0) * vy

            # Shapekeys
            if do_shapekeys:
                # remapped vertex coordinates
                v0 = vs0[0] + (vs0[1] - vs0[0]) * vx_key
                v1 = vs0[3] + (vs0[2] - vs0[3]) * vx_key
                v2 = v0 + (v1 - v0) * vy_key
                # remapped vertex normal
                nv0 = nvs0[0] + (nvs0[1] - nvs0[0]) * vx_key
                nv1 = nvs0[3] + (nvs0[2] - nvs0[3]) * vx_key
                nv2 = nv0 + (nv1 - nv0) * vy_key
                # vertex z to normal
                v3_key = v2 + nv2 * vz_key * (sqrt(p.area) if
                                              scale_mode == "ADAPTIVE" else 1)
                v3 = v3 + (v3_key - v3) * w2

        if j == 0:
            new_verts_np = v3
            if bool_vertex_group:
                new_vertex_group_np = w2
        else:
            # Appending vertices
            new_verts_np = np.concatenate((new_verts_np, v3), axis=0)
            # Appending vertex group
            if bool_vertex_group:
                new_vertex_group_np = np.concatenate((new_vertex_group_np, w2),
                                                     axis=0)
            # Appending faces
            for p in fs1:
                new_faces.append([i + n_verts * j for i in p])
            # Appending edges
            for e in es1:
                new_edges.append([i + n_verts * j for i in e])

        j += 1

    new_verts = new_verts_np.tolist()
    new_name = ob0.name + "_" + ob1.name
    new_me = bpy.data.meshes.new(new_name)
    new_me.from_pydata(new_verts, new_edges, new_faces)
    new_me.update(calc_edges=True)
    new_ob = bpy.data.objects.new("tessellate_temp", new_me)

    # vertex group
    if bool_vertex_group:
        new_ob.vertex_groups.new(name="generator_group")
        for i in range(len(new_vertex_group_np)):
            new_ob.vertex_groups["generator_group"].add([i],
                                                        new_vertex_group_np[i],
                                                        "ADD")
    ob0.data = old_me0
    return new_ob


def store_parameters(operator, ob):
    ob.tissue_tessellate.generator = operator.generator
    ob.tissue_tessellate.component = operator.component
    ob.tissue_tessellate.zscale = operator.zscale
    ob.tissue_tessellate.offset = operator.offset
    ob.tissue_tessellate.gen_modifiers = operator.gen_modifiers
    ob.tissue_tessellate.com_modifiers = operator.com_modifiers
    ob.tissue_tessellate.mode = operator.mode
    ob.tissue_tessellate.rotation_mode = operator.rotation_mode
    ob.tissue_tessellate.merge = operator.merge
    ob.tissue_tessellate.merge_thres = operator.merge_thres
    ob.tissue_tessellate.scale_mode = operator.scale_mode
    ob.tissue_tessellate.bool_random = operator.bool_random
    ob.tissue_tessellate.random_seed = operator.random_seed
    ob.tissue_tessellate.fill_mode = operator.fill_mode
    ob.tissue_tessellate.bool_vertex_group = operator.bool_vertex_group
    ob.tissue_tessellate.bool_selection = operator.bool_selection
    ob.tissue_tessellate.bool_shapekeys = operator.bool_shapekeys
    return ob


class tissue_tessellate_prop(PropertyGroup):
    generator: StringProperty()
    component: StringProperty()
    offset: FloatProperty()
    zscale: FloatProperty(default=1)
    merge: BoolProperty()
    merge_thres: FloatProperty()
    gen_modifiers: BoolProperty()
    com_modifiers: BoolProperty()
    mode: StringProperty()
    rotation_mode: StringProperty()
    scale_mode: StringProperty()
    fill_mode: StringProperty()
    bool_random: BoolProperty()
    random_seed: IntProperty()
    vertexgroup: StringProperty()
    bool_vertex_group: BoolProperty()
    bool_selection: BoolProperty()
    bool_shapekeys: BoolProperty()


class tessellate(Operator):
    bl_idname = "object.tessellate"
    bl_label = "Tessellate"
    bl_description = ("Create a copy of selected object on the active object's "
                      "faces, adapting the shape to the different faces")
    bl_options = {'REGISTER', 'UNDO'}

    object_name: StringProperty(
            name="",
            description="Name of the generated object"
            )
    zscale: FloatProperty(
            name="Scale",
            default=1,
            soft_min=0,
            soft_max=10,
            description="Scale factor for the component thickness"
            )
    scale_mode: EnumProperty(
            items=(('CONSTANT', "Constant", ""), ('ADAPTIVE', "Proportional", "")),
            default='CONSTANT',
            name="Z-Scale according to faces size"
            )
    offset: FloatProperty(
            name="Surface Offset",
            default=0,
            min=-1, max=1,
            soft_min=-1,
            soft_max=1,
            description="Surface offset"
            )
    mode: EnumProperty(
            items=(('CONSTANT', "Constant", ""), ('ADAPTIVE', "Adaptive", "")),
            default='ADAPTIVE',
            name="Component Mode"
            )
    rotation_mode: EnumProperty(
            items=(('RANDOM', "Random", ""),
                   ('UV', "Active UV", ""),
                   ('DEFAULT', "Default", "")),
            default='DEFAULT',
            name="Component Rotation"
            )
    fill_mode: EnumProperty(
            items=(('QUAD', "Quad", ""), ('FAN', "Fan", "")),
            default='QUAD',
            name="Fill Mode"
            )
    gen_modifiers: BoolProperty(
            name="Generator Modifiers",
            default=False,
            description="Apply modifiers to base object"
            )
    com_modifiers: BoolProperty(
            name="Component Modifiers",
            default=False,
            description="Apply modifiers to component object"
            )
    merge: BoolProperty(
            name="Merge",
            default=False,
            description="Merge vertices in adjacent duplicates"
            )
    merge_thres: FloatProperty(
            name="Distance",
            default=0.001,
            soft_min=0,
            soft_max=10,
            description="Limit below which to merge vertices"
            )
    generator: StringProperty(
            name="",
            description="Base object for the tessellation"
            )
    component: StringProperty(
            name="",
            description="Component object for the tessellation"
            )
    bool_random: BoolProperty(
            name="Randomize",
            default=False,
            description="Randomize component rotation"
            )
    random_seed: IntProperty(
            name="Seed",
            default=0,
            soft_min=0,
            soft_max=10,
            description="Random seed"
            )
    bool_vertex_group: BoolProperty(
            name="Map Vertex Group",
            default=False,
            description="Map the active "
                        "Vertex Group from the Base object to generated geometry"
            )
    bool_selection: BoolProperty(
            name="On selected Faces",
            default=False,
            description="Create Tessellation only on selected faces"
            )
    bool_shapekeys: BoolProperty(
            name="Use Shape Keys",
            default=False,
            description="Use component's active Shape Key according to "
                        "active Vertex Group of the base object"
            )
    working_on = ""

    @staticmethod
    def check_gen_comp(checking):
        # note pass the stored name key in here to check it out
        return checking in bpy.data.objects.keys()

    def draw(self, context):
        try:
            bool_working = self.working_on == self.object_name and \
            self.working_on != ""
        except:
            bool_working = False

        sel = bpy.context.selected_objects

        bool_meshes = False
        if len(sel) == 2:
            bool_meshes = True
            for o in sel:
                if o.type != 'MESH':
                    bool_meshes = False

        if len(sel) != 2 and not bool_working:
            layout = self.layout
            layout.label(icon='INFO')
            layout.label(text="Please, select two different objects")
            layout.label(text="Select first the Component object, then select")
            layout.label(text="the Base mesh.")
        elif not bool_meshes and not bool_working:
            layout = self.layout
            layout.label(icon='INFO')
            layout.label(text="Please, select two Mesh objects")
        else:
            try:
                ob0 = bpy.data.Objects[self.generator]
            except:
                ob0 = bpy.context.active_object
                self.generator = ob0.name

            for o in sel:
                if (o.name == ob0.name or o.type != 'MESH'):
                    continue
                else:
                    ob1 = o
                    self.component = o.name
                    self.no_component = False
                    break

            # Checks for Tool Shelf panel, it lost the original Selection
            if bpy.context.active_object.name == self.object_name:
                # checks if the objects were deleted
                if self.check_gen_comp(
                        bpy.context.active_object.tissue_tessellate.component):
                    ob1 = bpy.data.objects[
                                bpy.context.active_object.tissue_tessellate.component
                                ]
                    self.component = ob1.name

                if self.check_gen_comp(
                        bpy.context.active_object.tissue_tessellate.generator):
                    ob0 = bpy.data.objects[
                                bpy.context.active_object.tissue_tessellate.generator
                                ]
                    self.generator = ob0.name
                    self.no_component = False

            # new object name
            if self.object_name == "":
                if self.generator == "":
                    self.object_name = "Tessellation"
                else:
                    self.object_name = self.generator + "_Tessellation"

            layout = self.layout
            # Base and Component
            col = layout.column(align=True)
            row = col.row(align=True)
            row.label(text="BASE : " + self.generator)
            row.label(text="COMPONENT : " + self.component)
            row = col.row(align=True)
            col2 = row.column(align=True)
            col2.prop(self, "gen_modifiers", text="Use Modifiers")

            if not self.check_gen_comp(self.generator) or \
                    len(bpy.data.objects[self.generator].modifiers) == 0:
                col2.enabled = False
                self.gen_modifiers = False
            col2 = row.column(align=True)
            col2.prop(self, "com_modifiers", text="Use Modifiers")

            if not self.check_gen_comp(self.component) or \
                    len(bpy.data.objects[self.component].modifiers) == 0:
                col2.enabled = False
                self.com_modifiers = False

            # On selected faces
            row = col.row(align=True)
            row.prop(self, "bool_selection", text="On selected Faces")
            col.separator()

            # General
            col = layout.column(align=True)
            col.label(text="New Object Name:")
            col.prop(self, "object_name")

            # Count number of faces
            try:
                polygons = 0
                if self.gen_modifiers:
                    me_temp = ob0.to_mesh(
                                    bpy.context.scene,
                                    apply_modifiers=True, settings='PREVIEW'
                                    )
                else:
                    me_temp = ob0.data

                for p in me_temp.polygons:
                    if not self.bool_selection or p.select:
                        if self.fill_mode == "FAN":
                            polygons += len(p.vertices)
                        else:
                            polygons += 1

                if self.com_modifiers:
                    me_temp = bpy.data.objects[self.component].to_mesh(
                                            bpy.context.scene,
                                            apply_modifiers=True,
                                            settings='PREVIEW'
                                            )
                else:
                    me_temp = bpy.data.objects[self.component].data
                polygons *= len(me_temp.polygons)

                str_polygons = '{:0,.0f}'.format(polygons)
                if polygons > 200000:
                    col.label(text=str_polygons + " polygons will be created!",
                              icon='ERROR')
                else:
                    col.label(text=str_polygons + " faces will be created!",
                              icon='INFO')
            except:
                pass
            col.separator()

            # Fill and Rotation
            row = col.row(align=True)
            row.label(text="Fill Mode:")
            row.separator()
            row.label(text="Rotation:")
            row = col.row(align=True)

            # Fill
            row.prop(
                self, "fill_mode", text="", icon='NONE', expand=False,
                slider=True, toggle=False, icon_only=False, event=False,
                full_event=False, emboss=True, index=-1)
            row.separator()

            # Rotation
            row.prop(
                self, "rotation_mode", text="", icon='NONE', expand=False,
                slider=True, toggle=False, icon_only=False, event=False,
                full_event=False, emboss=True, index=-1)
            if self.rotation_mode == 'RANDOM':
                row = col.row(align=True)
                row.prop(self, "random_seed")
            if self.rotation_mode == 'UV':
                uv_error = False
                if self.fill_mode == 'FAN':
                    row = col.row(align=True)
                    row.label(text="UV rotation doesn't work in FAN mode",
                              icon='ERROR')
                    uv_error = True

                if not self.check_gen_comp(self.generator) or \
                        len(bpy.data.objects[self.generator].data.uv_layers) == 0:
                    row = col.row(align=True)
                    check_name = bpy.data.objects[self.generator].name if \
                                self.check_gen_comp(self.generator) else "None"
                    row.label(text="'" + check_name +
                              "' doesn't have UV Maps", icon='ERROR')
                    uv_error = True
                if uv_error:
                    row = col.row(align=True)
                    row.label(text="Default rotation will be used instead",
                              icon='INFO')

            # Component XY
            row = col.row(align=True)
            row.label(text="Component XY:")
            row = col.row(align=True)
            row.prop(
                self, "mode", text="Component XY", icon='NONE', expand=True,
                slider=False, toggle=False, icon_only=False, event=False,
                full_event=False, emboss=True, index=-1)

            # Component Z
            col.label(text="Component Z:")
            row = col.row(align=True)
            row.prop(
                self, "scale_mode", text="Scale Mode", icon='NONE', expand=True,
                slider=False, toggle=False, icon_only=False, event=False,
                full_event=False, emboss=True, index=-1)
            col.prop(
                self, "zscale", text="Scale", icon='NONE', expand=False,
                slider=True, toggle=False, icon_only=False, event=False,
                full_event=False, emboss=True, index=-1)
            col.prop(
                self, "offset", text="Offset", icon='NONE', expand=False,
                slider=True, toggle=False, icon_only=False, event=False,
                full_event=False, emboss=True, index=-1)

            # Merge
            col = layout.column(align=True)
            row = col.row(align=True)
            row.prop(self, "merge")

            if self.merge:
                row.prop(self, "merge_thres")
            row = col.row(align=True)

            # ADVANCED
            col = layout.column(align=True)
            col.label(text="Advanced Settings:")
            # vertex group + shape keys
            row = col.row(align=True)
            col2 = row.column(align=True)
            col2.prop(self, "bool_vertex_group")

            if self.check_gen_comp(self.generator) and \
                    len(bpy.data.objects[self.generator].vertex_groups) == 0:
                col2.enabled = False
                self.bool_vertex_group = False

            col2 = row.column(align=True)

            if not self.check_gen_comp(self.generator) or \
                    not self.check_gen_comp(self.component):
                return

            col2.prop(self, "bool_shapekeys", text="Use Shape Keys")

            if len(bpy.data.objects[self.generator].vertex_groups) == 0 or \
                    bpy.data.objects[self.component].data.shape_keys is None:
                col2.enabled = False
                self.bool_shapekeys = False
            elif len(bpy.data.objects[self.generator].vertex_groups) == 0 or \
                    bpy.data.objects[self.component].data.shape_keys is not None:
                if len(bpy.data.objects[
                        self.component].data.shape_keys.key_blocks) < 2:
                    col2.enabled = False
                    self.bool_shapekeys = False

    def execute(self, context):
        try:
            ob0 = bpy.context.active_object
            self.generator = ob0.name
        except:
            self.report({'ERROR'}, "A Generator mesh object must be selected")

        # component object
        sel = bpy.context.selected_objects
        no_component = True
        for o in sel:
            if (o.name == ob0.name or o.type != 'MESH'):
                continue
            else:
                ob1 = o
                self.component = o.name
                no_component = False
                break

        # Checks for Tool Shelf panel, it lost the original Selection
        if bpy.context.active_object == self.object_name:
            ob1 = bpy.data.objects[
                bpy.context.active_object.tissue_tessellate.component]
            self.component = ob1.name
            ob0 = bpy.data.objects[
                bpy.context.active_object.tissue_tessellate.generator]
            self.generator = ob0.name
            no_component = False

        if no_component:
            # self.report({'ERROR'}, "A component mesh object must be selected")
            return {'CANCELLED'}

        # new object name
        if self.object_name == "":
            if self.generator == "":
                self.object_name = "Tessellation"
            else:
                self.object_name = self.generator + "_Tessellation"

        if bpy.data.objects[self.component].type != 'MESH':
            message = "Component must be Mesh Objects!"
            self.report({'ERROR'}, message)
            self.component = ""

        if bpy.data.objects[self.generator].type != 'MESH':
            message = "Generator must be Mesh Objects!"
            self.report({'ERROR'}, message)
            self.generator = ""

        if self.component != "" and self.generator != "":
            if bpy.ops.object.select_all.poll():
                bpy.ops.object.select_all(action='TOGGLE')

            new_ob = tassellate(
                    ob0, ob1, self.offset, self.zscale, self.gen_modifiers,
                    self.com_modifiers, self.mode, self.scale_mode,
                    self.rotation_mode, self.random_seed, self.fill_mode,
                    self.bool_vertex_group, self.bool_selection,
                    self.bool_shapekeys
                    )

            if new_ob == 0:
                message = "Zero faces selected in the Base mesh!"
                self.report({'ERROR'}, message)
                return {'CANCELLED'}

            new_ob.name = self.object_name
            # new_ob = bpy.data.objects.new(self.object_name, new_me)

            new_ob.location = ob0.location
            new_ob.matrix_world = ob0.matrix_world

            bpy.context.collection.objects.link(new_ob)
            new_ob.select_set(True)
            bpy.context.view_layer.objects.active = new_ob
            if self.merge:
                bpy.ops.object.mode_set(mode='EDIT')
                bpy.ops.mesh.select_mode(
                    use_extend=False, use_expand=False, type='VERT')
                bpy.ops.mesh.select_non_manifold(
                    extend=False, use_wire=False, use_boundary=True,
                    use_multi_face=False, use_non_contiguous=False,
                    use_verts=False)
                bpy.ops.mesh.remove_doubles(
                    threshold=self.merge_thres, use_unselected=False)
                bpy.ops.object.mode_set(mode='OBJECT')
            new_ob = store_parameters(self, new_ob)
            self.object_name = new_ob.name
            self.working_on = self.object_name

        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.object.mode_set(mode='OBJECT')

        # MATERIALS
        try:
            # create materials list
            polygon_materials = [p.material_index for p in ob1.data.polygons] * int(
                    len(new_ob.data.polygons) / len(ob1.data.polygons))
            # assign old material
            component_materials = [slot.material for slot in ob1.material_slots]
            for i in range(len(component_materials)):
                bpy.ops.object.material_slot_add()
                bpy.context.object.material_slots[i].material = \
                    component_materials[i]
            for i in range(len(new_ob.data.polygons)):
                new_ob.data.polygons[i].material_index = polygon_materials[i]
        except:
            pass

        return {'FINISHED'}

    def check(self, context):
        return True

    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self)


class update_tessellate(Operator):
    bl_idname = "object.update_tessellate"
    bl_label = "Refresh"
    bl_description = ("Fast update the tessellated mesh according to base and "
                      "component changes")
    bl_options = {'REGISTER', 'UNDO'}

    go = False

    @classmethod
    def poll(cls, context):
        try:
            return context.active_object.tissue_tessellate.generator != "" and \
                context.active_object.tissue_tessellate.component != ""
        except:
            return False

    @staticmethod
    def check_gen_comp(checking):
        # note pass the stored name key in here to check it out
        return checking in bpy.data.objects.keys()

    def execute(self, context):
        ob = bpy.context.active_object
        if not self.go:
            generator = ob.tissue_tessellate.generator
            component = ob.tissue_tessellate.component
            zscale = ob.tissue_tessellate.zscale
            scale_mode = ob.tissue_tessellate.scale_mode
            rotation_mode = ob.tissue_tessellate.rotation_mode
            offset = ob.tissue_tessellate.offset
            merge = ob.tissue_tessellate.merge
            merge_thres = ob.tissue_tessellate.merge_thres
            gen_modifiers = ob.tissue_tessellate.gen_modifiers
            com_modifiers = ob.tissue_tessellate.com_modifiers
            bool_random = ob.tissue_tessellate.bool_random
            random_seed = ob.tissue_tessellate.random_seed
            fill_mode = ob.tissue_tessellate.fill_mode
            bool_vertex_group = ob.tissue_tessellate.bool_vertex_group
            bool_selection = ob.tissue_tessellate.bool_selection
            bool_shapekeys = ob.tissue_tessellate.bool_shapekeys
            mode = ob.tissue_tessellate.mode

        if not self.check_gen_comp(generator) or \
                not self.check_gen_comp(component):
            self.report({'ERROR'},
                        "Base or Component Objects are missing from the data "
                        "(Most likely deleted or renamed)")
            return {'CANCELLED'}

        if (generator == "" or component == ""):
            self.report({'ERROR'},
                        "Active object must be Tessellate before Update")
            return {'CANCELLED'}

        ob0 = bpy.data.objects[generator]
        ob1 = bpy.data.objects[component]

        temp_ob = tassellate(
                ob0, ob1, offset, zscale, gen_modifiers, com_modifiers,
                mode, scale_mode, rotation_mode, random_seed, fill_mode,
                bool_vertex_group, bool_selection, bool_shapekeys
                )

        if temp_ob == 0:
            message = "Zero faces selected in the Base mesh!"
            self.report({'ERROR'}, message)
            return {'CANCELLED'}

        ob.data = temp_ob.data
        bpy.data.objects.remove(temp_ob)
        if merge:
            bpy.ops.object.mode_set(mode='EDIT')
            bpy.ops.mesh.select_mode(
                use_extend=False, use_expand=False, type='VERT')
            bpy.ops.mesh.select_non_manifold(
                extend=False, use_wire=False, use_boundary=True,
                use_multi_face=False, use_non_contiguous=False, use_verts=False)
            bpy.ops.mesh.remove_doubles(
                threshold=merge_thres, use_unselected=False)
            bpy.ops.object.mode_set(mode='OBJECT')
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.object.mode_set(mode='OBJECT')

        # MATERIALS
        try:
            # create materials list
            polygon_materials = [p.material_index for p in ob1.data.polygons] * int(
                    len(ob.data.polygons) / len(ob1.data.polygons))
            # assign old material
            component_materials = [slot.material for slot in ob1.material_slots]
            for i in range(len(component_materials)):
                bpy.ops.object.material_slot_add()
                bpy.context.object.material_slots[i].material = \
                    component_materials[i]
            for i in range(len(ob.data.polygons)):
                ob.data.polygons[i].material_index = polygon_materials[i]
        except:
            pass

        return {'FINISHED'}

    def check(self, context):
        return True


class settings_tessellate(Operator):
    bl_idname = "object.settings_tessellate"
    bl_label = "Settings"
    bl_description = ("Update the tessellated mesh according to base and component changes\n"
                      "Allow also to change tessellation's parameters")
    bl_options = {'REGISTER', 'UNDO'}

    object_name: StringProperty(
            name="",
            description="Name of the generated object"
            )
    zscale: FloatProperty(
            name="Scale",
            default=1,
            soft_min=0,
            soft_max=10,
            description="Scale factor for the component thickness"
            )
    scale_mode: EnumProperty(
            items=(('CONSTANT', "Constant", ""), ('ADAPTIVE', "Proportional", "")),
            default='ADAPTIVE',
            name="Scale variation"
            )
    offset: FloatProperty(
            name="Surface Offset",
            default=0,
            min=-1, max=1,
            soft_min=-1,
            soft_max=1,
            description="Surface offset"
            )
    mode: EnumProperty(
            items=(('CONSTANT', "Constant", ""), ('ADAPTIVE', "Adaptive", "")),
            default='ADAPTIVE',
            name="Component Mode"
            )
    rotation_mode: EnumProperty(
            items=(('RANDOM', "Random", ""), ('UV', "Active UV", ""),
                   ('DEFAULT', "Default", "")),
            default='DEFAULT',
            name="Component Rotation"
            )
    fill_mode: EnumProperty(
            items=(('QUAD', "Quad", ""), ('FAN', "Fan", "")),
            default='QUAD',
            name="Fill Mode"
            )
    gen_modifiers: BoolProperty(
            name="Generator Modifiers",
            default=False,
            description="Apply modifiers to base object"
            )
    com_modifiers: BoolProperty(
            name="Component Modifiers",
            default=False,
            description="Apply modifiers to component object"
            )
    merge: BoolProperty(
            name="Merge",
            default=False,
            description="Merge vertices in adjacent duplicates"
            )
    merge_thres: FloatProperty(
            name="Distance",
            default=0.001,
            soft_min=0,
            soft_max=10,
            description="Limit below which to merge vertices"
            )
    generator: StringProperty(
            name="",
            description="Base object for the tessellation"
            )
    component: StringProperty(
            name="",
            description="Component object for the tessellation"
            )
    bool_random: BoolProperty(
            name="Randomize",
            default=False,
            description="Randomize component rotation"
            )
    random_seed: IntProperty(
            name="Seed",
            default=0,
            soft_min=0,
            soft_max=10,
            description="Random seed"
            )
    bool_vertex_group: BoolProperty(
            name="Map Vertex Group",
            default=False,
            description="Map on generated "
                        "geometry the active Vertex Group from the base object"
            )
    bool_selection: BoolProperty(
            name="On selected Faces",
            default=False,
            description="Create Tessellation only on select faces"
            )
    bool_shapekeys: BoolProperty(
            name="Use Shape Keys",
            default=False,
            description="Use component's active Shape Key according to active "
                        "Vertex Group of the base object"
            )
    go = False

    @classmethod
    def poll(cls, context):
        try:
            return context.active_object.tissue_tessellate.generator != "" and \
                context.active_object.tissue_tessellate.component != ""
        except:
            return False

    @staticmethod
    def check_gen_comp(checking):
        # note pass the stored name key in here to check it out
        return checking in bpy.data.objects.keys()

    def draw(self, context):
        layout = self.layout
        ob0 = bpy.context.active_object

        if not self.go:
            self.generator = ob0.tissue_tessellate.generator
            self.component = ob0.tissue_tessellate.component
            self.zscale = ob0.tissue_tessellate.zscale
            self.scale_mode = ob0.tissue_tessellate.scale_mode
            self.rotation_mode = ob0.tissue_tessellate.rotation_mode
            self.offset = ob0.tissue_tessellate.offset
            self.merge = ob0.tissue_tessellate.merge
            self.merge_thres = ob0.tissue_tessellate.merge_thres
            self.gen_modifiers = ob0.tissue_tessellate.gen_modifiers
            self.com_modifiers = ob0.tissue_tessellate.com_modifiers
            self.bool_random = ob0.tissue_tessellate.bool_random
            self.random_seed = ob0.tissue_tessellate.random_seed
            self.fill_mode = ob0.tissue_tessellate.fill_mode
            self.bool_vertex_group = ob0.tissue_tessellate.bool_vertex_group
            self.bool_selection = ob0.tissue_tessellate.bool_selection
            self.bool_shapekeys = ob0.tissue_tessellate.bool_shapekeys
            self.mode = ob0.tissue_tessellate.mode

        # start drawing
        layout = self.layout

        # check for keys in data - as the objects can be deleted or renamed
        if not self.check_gen_comp(self.generator) or \
                not self.check_gen_comp(self.component):

            layout.label(text="Base or Component Objects are missing from the data",
                        icon="INFO")
            layout.label(text="(Most likely deleted or renamed)",
                        icon="BLANK1")
            layout.label(text="Settings could not be altered anymore",
                        icon="BLANK1")
            layout.label(text="Please re-run Tessellate with two new selected objects",
                        icon="BLANK1")
            return

        # ob0 = bpy.context.active_object
        # Base and Component
        col = layout.column(align=True)
        row = col.row(align=True)
        row.label(text="BASE :")
        row.label(text="COMPONENT :")
        row = col.row(align=True)

        col2 = row.column(align=True)
        col2.prop_search(self, "generator", bpy.data, "objects")
        row.separator()
        col2 = row.column(align=True)
        col2.prop_search(self, "component", bpy.data, "objects")

        row = col.row(align=True)
        col2 = row.column(align=True)
        col2.prop(self, "gen_modifiers", text="Use Modifiers")

        if len(bpy.data.objects[self.generator].modifiers) == 0:
            col2.enabled = False
            self.gen_modifiers = False

        col2 = row.column(align=True)
        col2.prop(self, "com_modifiers", text="Use Modifiers")

        if len(bpy.data.objects[self.component].modifiers) == 0:
            col2.enabled = False
            self.com_modifiers = False

        # On selected faces
        row = col.row(align=True)
        row.prop(self, "bool_selection", text="On selected Faces")
        col.separator()

        # Count number of faces
        try:
            polygons = 0
            if self.gen_modifiers:
                me_temp = bpy.data.objects[self.generator].to_mesh(
                                                    bpy.context.scene,
                                                    apply_modifiers=True,
                                                    settings='PREVIEW'
                                                    )
            else:
                me_temp = bpy.data.objects[self.generator].data

            for p in me_temp.polygons:
                if not self.bool_selection or p.select:
                    if self.fill_mode == "FAN":
                        polygons += len(p.vertices)
                    else:
                        polygons += 1

            if self.com_modifiers:
                me_temp = bpy.data.objects[self.component].to_mesh(
                                                    bpy.context.scene,
                                                    apply_modifiers=True,
                                                    settings='PREVIEW'
                                                    )
            else:
                me_temp = bpy.data.objects[self.component].data
            polygons *= len(me_temp.polygons)

            str_polygons = '{:0,.0f}'.format(polygons)
            if polygons > 200000:
                col.label(text=str_polygons + " polygons will be created!",
                          icon='ERROR')
            else:
                col.label(text=str_polygons + " faces will be created!",
                          icon='INFO')
        except:
            pass
        col.separator()

        # Fill and Rotation
        row = col.row(align=True)
        row.label(text="Fill Mode:")
        row.separator()
        row.label(text="Rotation:")
        row = col.row(align=True)

        # fill
        row.prop(self, "fill_mode", text="", icon='NONE', expand=False,
                 slider=True, toggle=False, icon_only=False, event=False,
                 full_event=False, emboss=True, index=-1)
        row.separator()

        # rotation
        row.prop(self, "rotation_mode", text="", icon='NONE', expand=False,
                 slider=True, toggle=False, icon_only=False, event=False,
                 full_event=False, emboss=True, index=-1)

        if self.rotation_mode == 'RANDOM':
            row = col.row(align=True)
            row.prop(self, "random_seed")

        if self.rotation_mode == 'UV':
            uv_error = False
            if self.fill_mode == 'FAN':
                row = col.row(align=True)
                row.label(text="UV rotation doesn't work in FAN mode",
                          icon='ERROR')
                uv_error = True

            if len(bpy.data.objects[self.generator].data.uv_layers) == 0:
                row = col.row(align=True)
                row.label(text="'" + bpy.data.objects[self.generator].name +
                          " doesn't have UV Maps", icon='ERROR')
                uv_error = True
            if uv_error:
                row = col.row(align=True)
                row.label(text="Default rotation will be used instead",
                          icon='INFO')

        # component XY
        row = col.row(align=True)
        row.label(text="Component XY:")
        row = col.row(align=True)
        row.prop(self, "mode", text="Component XY", icon='NONE', expand=True,
                 slider=False, toggle=False, icon_only=False, event=False,
                 full_event=False, emboss=True, index=-1)

        # component Z
        col.label(text="Component Z:")
        row = col.row(align=True)
        row.prop(self, "scale_mode", text="Scale Mode", icon='NONE',
                 expand=True, slider=False, toggle=False, icon_only=False,
                 event=False, full_event=False, emboss=True, index=-1)
        col.prop(self, "zscale", text="Scale", icon='NONE', expand=False,
                 slider=True, toggle=False, icon_only=False, event=False,
                 full_event=False, emboss=True, index=-1)
        col.prop(self, "offset", text="Offset", icon='NONE', expand=False,
                 slider=True, toggle=False, icon_only=False, event=False,
                 full_event=False, emboss=True, index=-1)

        # merge
        col = layout.column(align=True)
        row = col.row(align=True)
        row.prop(self, "merge")
        if self.merge:
            row.prop(self, "merge_thres")
        row = col.row(align=True)

        # ADVANCED #
        col = layout.column(align=True)
        tessellate.rotation_mode

        col.label(text="Advanced Settings:")
        # vertex group + shape keys
        row = col.row(align=True)
        col2 = row.column(align=True)
        col2.prop(self, "bool_vertex_group")

        if len(bpy.data.objects[self.generator].vertex_groups) == 0:
            col2.enabled = False
            self.bool_vertex_group = False
        col2 = row.column(align=True)
        col2.prop(self, "bool_shapekeys", text="Use Shape Keys")

        if len(bpy.data.objects[self.generator].vertex_groups) == 0 or \
                bpy.data.objects[self.component].data.shape_keys is None:
            col2.enabled = False
            self.bool_shapekeys = False
        elif len(bpy.data.objects[self.generator].vertex_groups) == 0 or \
                bpy.data.objects[self.component].data.shape_keys is not None:
            if len(bpy.data.objects[self.component].data.shape_keys.key_blocks) < 2:
                col2.enabled = False
                self.bool_shapekeys = False
        self.go = True

    def execute(self, context):
        self.ob = bpy.context.active_object

        if not self.go:
            self.generator = self.ob.tissue_tessellate.generator
            self.component = self.ob.tissue_tessellate.component
            self.zscale = self.ob.tissue_tessellate.zscale
            self.scale_mode = self.ob.tissue_tessellate.scale_mode
            self.rotation_mode = self.ob.tissue_tessellate.rotation_mode
            self.offset = self.ob.tissue_tessellate.offset
            self.merge = self.ob.tissue_tessellate.merge
            self.merge_thres = self.ob.tissue_tessellate.merge_thres
            self.gen_modifiers = self.ob.tissue_tessellate.gen_modifiers
            self.com_modifiers = self.ob.tissue_tessellate.com_modifiers
            self.bool_random = self.ob.tissue_tessellate.bool_random
            self.random_seed = self.ob.tissue_tessellate.random_seed
            self.fill_mode = self.ob.tissue_tessellate.fill_mode
            self.bool_vertex_group = self.ob.tissue_tessellate.bool_vertex_group
            self.bool_selection = self.ob.tissue_tessellate.bool_selection
            self.bool_shapekeys = self.ob.tissue_tessellate.bool_shapekeys

        if not self.check_gen_comp(self.generator) or \
                not self.check_gen_comp(self.component):
            # do nothing as the Warning was already done in it UI
            return {'CANCELLED'}

        if (self.generator == "" or self.component == ""):
            self.report({'ERROR'},
                        "Active object must be Tessellated before Update")
            return {'CANCELLED'}

        if (bpy.data.objects[self.generator].type != 'MESH'):
            self.report({'ERROR'}, "Base object must be a Mesh")
            return {'CANCELLED'}

        if (bpy.data.objects[self.component].type != 'MESH'):
            self.report({'ERROR'}, "Component object must be a Mesh")
            return {'CANCELLED'}

        ob0 = bpy.data.objects[self.generator]
        ob1 = bpy.data.objects[self.component]

        temp_ob = tassellate(
                ob0, ob1, self.offset, self.zscale, self.gen_modifiers,
                self.com_modifiers, self.mode, self.scale_mode, self.rotation_mode,
                self.random_seed, self.fill_mode, self.bool_vertex_group,
                self.bool_selection, self.bool_shapekeys
                )

        if temp_ob == 0:
            message = "Zero faces selected in the Base mesh"
            self.report({'ERROR'}, message)

            return {'CANCELLED'}

        # Transfer mesh data
        self.ob.data = temp_ob.data

        # Create object in order to transfer vertex group
        bpy.context.collection.objects.link(temp_ob)
        temp_ob.select_set(True)
        bpy.context.view_layer.objects.active = temp_ob

        try:
            bpy.ops.object.vertex_group_copy_to_linked()
        except:
            pass

        bpy.context.scene.objects.unlink(temp_ob)
        bpy.data.objects.remove(temp_ob)
        bpy.context.view_layer.objects.active = self.ob

        if self.merge:
            bpy.ops.object.mode_set(mode='EDIT')
            bpy.ops.mesh.select_mode(
                use_extend=False, use_expand=False, type='VERT')
            bpy.ops.mesh.select_non_manifold(
                extend=False, use_wire=False, use_boundary=True,
                use_multi_face=False, use_non_contiguous=False, use_verts=False)
            bpy.ops.mesh.remove_doubles(
                threshold=self.merge_thres, use_unselected=False)
            bpy.ops.object.mode_set(mode='OBJECT')
        self.ob = store_parameters(self, self.ob)
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.object.mode_set(mode='OBJECT')

        # MATERIALS
        try:
            # create materials list
            polygon_materials = [p.material_index for p in ob1.data.polygons] * \
                    int(len(self.ob.data.polygons) / len(ob1.data.polygons))
            # assign old material
            component_materials = [slot.material for slot in ob1.material_slots]
            for i in range(len(component_materials)):
                bpy.ops.object.material_slot_add()
                bpy.context.object.material_slots[i].material = \
                    component_materials[i]
            for i in range(len(self.ob.data.polygons)):
                self.ob.data.polygons[i].material_index = polygon_materials[i]
        except:
            pass

        return {'FINISHED'}

    def check(self, context):
        return True

    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self, width=400)


class tessellate_panel(Panel):
    bl_label = "Tissue"
    bl_category = "Create"
    bl_space_type = "VIEW_3D"
    bl_region_type = "TOOLS"
    bl_options = {'DEFAULT_CLOSED'}

    @classmethod
    def poll(cls, context):
        return context.mode in {'OBJECT', 'EDIT_MESH'}

    def draw(self, context):
        layout = self.layout

        col = layout.column(align=True)
        col.label(text="Tessellate Add:")
        col.operator("object.tessellate")

        col = layout.column(align=True)
        col.label(text="Tessellate Edit:")
        col.operator("object.settings_tessellate")
        col.operator("object.update_tessellate")

        col = layout.column(align=True)
        col.operator("mesh.rotate_face")

        col.separator()
        col.label(text="Other:")
        col.operator("object.lattice_along_surface", icon="OUTLINER_OB_LATTICE")

        act = context.active_object
        if act and act.type == 'MESH':
            col.operator("object.uv_to_mesh", icon="GROUP_UVS")


class rotate_face(Operator):
    bl_idname = "mesh.rotate_face"
    bl_label = "Rotate Faces"
    bl_description = "Rotate selected faces and update tessellated meshes"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return context.mode == 'EDIT_MESH'

    def execute(self, context):
        ob = bpy.context.active_object
        me = ob.data
        bpy.ops.object.mode_set(mode='OBJECT')

        for p in [f for f in me.polygons if f.select]:
            p.vertices = p.vertices[1:] + p.vertices[:1]

        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.flip_normals()
        bpy.ops.mesh.flip_normals()
        me.update(calc_edges=True)

        # update tessellated meshes
        bpy.ops.object.mode_set(mode='OBJECT')
        for o in [obj for obj in bpy.data.objects if
                  obj.tissue_tessellate.generator == ob.name]:
            bpy.context.view_layer.objects.active = o
            bpy.ops.object.update_tessellate()
        bpy.context.view_layer.objects.active = ob
        bpy.ops.object.mode_set(mode='EDIT')

        return {'FINISHED'}


def register():
    bpy.utils.register_class(tissue_tessellate_prop)
    bpy.utils.register_class(tessellate)
    bpy.utils.register_class(update_tessellate)
    bpy.utils.register_class(settings_tessellate)
    bpy.utils.register_class(tessellate_panel)
    bpy.utils.register_class(rotate_face)


def unregister():
    bpy.utils.unregister_class(tissue_tessellate_prop)
    bpy.utils.unregister_class(tessellate)
    bpy.utils.unregister_class(update_tessellate)
    bpy.utils.unregister_class(settings_tessellate)
    bpy.utils.unregister_class(tessellate_panel)
    bpy.utils.unregister_class(rotate_face)


if __name__ == "__main__":
    register()
