# SPDX-FileCopyrightText: 2022-2023 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later

#-------------------------- COLORS / GROUPS EXCHANGER -------------------------#
#                                                                              #
# Vertex Color to Vertex Group allow you to convert colors channels to weight  #
# maps.                                                                        #
# The main purpose is to use vertex colors to store information when importing #
# files from other software. The script works with the active vertex color     #
# slot.                                                                        #
# For use the command "Vertex Clors to Vertex Groups" use the search bar       #
# (space bar).                                                                 #
#                                                                              #
#                          (c)  Alessandro Zomparelli                          #
#                                     (2017)                                   #
#                                                                              #
# http://www.co-de-it.com/                                                     #
#                                                                              #
################################################################################

import bpy, bmesh, os
import numpy as np
import math, timeit, time
from math import pi
from statistics import mean, stdev
from mathutils import Vector
from mathutils.kdtree import KDTree
from numpy import *
try: from .numba_functions import numba_reaction_diffusion, numba_reaction_diffusion_anisotropic, integrate_field
except: pass
#from .numba_functions import integrate_field
#from .numba_functions import numba_reaction_diffusion
try: import numexpr as ne
except: pass

# Reaction-Diffusion cache
from pathlib import Path
import random as rnd
import string

from bpy.types import (
        Operator,
        Panel,
        PropertyGroup,
        )

from bpy.props import (
    BoolProperty,
    EnumProperty,
    FloatProperty,
    IntProperty,
    StringProperty,
    FloatVectorProperty,
    IntVectorProperty
)

from .utils import *

def reaction_diffusion_add_handler(self, context):
    # remove existing handlers
    reaction_diffusion_remove_handler(self, context)
    # add new handler
    bpy.app.handlers.frame_change_post.append(reaction_diffusion_scene)

def reaction_diffusion_remove_handler(self, context):
    # remove existing handlers
    old_handlers = []
    for h in bpy.app.handlers.frame_change_post:
        if "reaction_diffusion" in str(h):
            old_handlers.append(h)
    for h in old_handlers: bpy.app.handlers.frame_change_post.remove(h)

class formula_prop(PropertyGroup):
    name : StringProperty()
    formula : StringProperty()
    float_var : FloatVectorProperty(name="", description="", default=(0, 0, 0, 0, 0), size=5)
    int_var : IntVectorProperty(name="", description="", default=(0, 0, 0, 0, 0), size=5)

class reaction_diffusion_prop(PropertyGroup):
    run : BoolProperty(default=False, update = reaction_diffusion_add_handler,
        description='Compute a new iteration on frame changes. Currently is not working during  Render Animation')

    time_steps : IntProperty(
        name="Steps", default=10, min=0, soft_max=50,
        description="Number of Steps")

    dt : FloatProperty(
        name="dt", default=1, min=0, soft_max=0.2,
        description="Time Step")

    diff_a : FloatProperty(
        name="Diff A", default=0.1, min=0, soft_max=2, precision=3,
        description="Diffusion A")

    diff_b : FloatProperty(
        name="Diff B", default=0.05, min=0, soft_max=2, precision=3,
        description="Diffusion B")

    f : FloatProperty(
        name="f", default=0.055, soft_min=0.01, soft_max=0.06, precision=4, step=0.05,
        description="Feed Rate")

    k : FloatProperty(
        name="k", default=0.062, soft_min=0.035, soft_max=0.065, precision=4, step=0.05,
        description="Kill Rate")

    diff_mult : FloatProperty(
        name="Scale", default=1, min=0, soft_max=1, max=10, precision=2,
        description="Multiplier for the diffusion of both substances")

    vertex_group_diff_a : StringProperty(
        name="Diff A", default='',
        description="Vertex Group used for A diffusion")

    vertex_group_diff_b : StringProperty(
        name="Diff B", default='',
        description="Vertex Group used for B diffusion")

    vertex_group_scale : StringProperty(
        name="Scale", default='',
        description="Vertex Group used for Scale value")

    vertex_group_f : StringProperty(
        name="f", default='',
        description="Vertex Group used for Feed value (f)")

    vertex_group_k : StringProperty(
        name="k", default='',
        description="Vertex Group used for Kill value (k)")

    vertex_group_brush : StringProperty(
        name="Brush", default='',
        description="Vertex Group used for adding/removing B")

    invert_vertex_group_diff_a : BoolProperty(default=False,
        description='Inverte the value of the Vertex Group Diff A')

    invert_vertex_group_diff_b : BoolProperty(default=False,
        description='Inverte the value of the Vertex Group Diff B')

    invert_vertex_group_scale : BoolProperty(default=False,
        description='Inverte the value of the Vertex Group Scale')

    invert_vertex_group_f : BoolProperty(default=False,
        description='Inverte the value of the Vertex Group f')

    invert_vertex_group_k : BoolProperty(default=False,
        description='Inverte the value of the Vertex Group k')

    min_diff_a : FloatProperty(
        name="Min Diff A", default=0.1, min=0, soft_max=2, precision=3,
        description="Min Diff A")

    max_diff_a : FloatProperty(
        name="Max Diff A", default=0.1, min=0, soft_max=2, precision=3,
        description="Max Diff A")

    min_diff_b : FloatProperty(
        name="Min Diff B", default=0.1, min=0, soft_max=2, precision=3,
        description="Min Diff B")

    max_diff_b : FloatProperty(
        name="Max Diff B", default=0.1, min=0, soft_max=2, precision=3,
        description="Max Diff B")

    min_scale : FloatProperty(
        name="Scale", default=0.35, min=0, soft_max=1, max=10, precision=2,
        description="Min Scale Value")

    max_scale : FloatProperty(
        name="Scale", default=1, min=0, soft_max=1, max=10, precision=2,
        description="Max Scale value")

    min_f : FloatProperty(
        name="Min f", default=0.02, min=0, soft_min=0.01, soft_max=0.06, max=0.1, precision=4, step=0.05,
        description="Min Feed Rate")

    max_f : FloatProperty(
        name="Max f", default=0.055, min=0, soft_min=0.01, soft_max=0.06, max=0.1, precision=4, step=0.05,
        description="Max Feed Rate")

    min_k : FloatProperty(
        name="Min k", default=0.035, min=0, soft_min=0.035, soft_max=0.065, max=0.1, precision=4, step=0.05,
        description="Min Kill Rate")

    max_k : FloatProperty(
        name="Max k", default=0.062, min=0, soft_min=0.035, soft_max=0.065, max=0.1, precision=4, step=0.05,
        description="Max Kill Rate")

    brush_mult : FloatProperty(
        name="Mult", default=0.5, min=-1, max=1, precision=3, step=0.05,
        description="Multiplier for brush value")

    bool_mod : BoolProperty(
        name="Use Modifiers", default=False,
        description="Read modifiers affect the vertex groups")

    bool_cache : BoolProperty(
        name="Use Cache", default=False,
        description="Read modifiers affect the vertex groups")

    cache_frame_start : IntProperty(
        name="Start", default=1,
        description="Frame on which the simulation starts")

    cache_frame_end : IntProperty(
        name="End", default=250,
        description="Frame on which the simulation ends")

    cache_dir : StringProperty(
        name="Cache directory", default="", subtype='FILE_PATH',
        description = 'Directory that contains Reaction-Diffusion cache files'
        )

    update_weight_a : BoolProperty(
        name="Update Vertex Group A", default=True,
        description="Transfer Cache to the Vertex Groups named A")

    update_weight_b : BoolProperty(
        name="Update Vertex Group B", default=True,
        description="Transfer Cache to the Vertex Groups named B")

    update_colors_a : BoolProperty(
        name="Update Vertex Color A", default=False,
        description="Transfer Cache to the Vertex Color named A")

    update_colors_b : BoolProperty(
        name="Update Vertex Color B", default=False,
        description="Transfer Cache to the Vertex Color named B")

    update_colors : BoolProperty(
        name="Update Vertex Color AB", default=False,
        description="Transfer Cache to the Vertex Color named AB")

    update_uv : BoolProperty(
        name="Update UV", default=False,
        description="Transfer Cache to the UV Map Layer named AB")

    normalize : BoolProperty(
        name="Normalize values", default=False,
        description="Normalize values from 0 to 1")

    fast_bake : BoolProperty(
        name="Fast Bake", default=True,
        description="Do not update modifiers or vertex groups while baking. Much faster!")


from numpy import *
def compute_formula(ob=None, formula="rx", float_var=(0,0,0,0,0), int_var=(0,0,0,0,0)):
    verts = ob.data.vertices
    n_verts = len(verts)

    f1,f2,f3,f4,f5 = float_var
    i1,i2,i3,i4,i5 = int_var

    do_groups = "w[" in formula
    do_local = "lx" in formula or "ly" in formula or "lz" in formula
    do_global = "gx" in formula or "gy" in formula or "gz" in formula
    do_relative = "rx" in formula or "ry" in formula or "rz" in formula
    do_normal = "nx" in formula or "ny" in formula or "nz" in formula
    mat = ob.matrix_world

    for i in range(1000):
        if "w["+str(i)+"]" in formula and i > len(ob.vertex_groups)-1:
            return "w["+str(i)+"] not found"

    w = []
    for i in range(len(ob.vertex_groups)):
        w.append([])
        if "w["+str(i)+"]" in formula:
            vg = ob.vertex_groups[i]
            for v in verts:
                try:
                    w[i].append(vg.weight(v.index))
                except:
                    w[i].append(0)
            w[i] = array(w[i])

    start_time = timeit.default_timer()
    # compute vertex coordinates
    if do_local or do_relative or do_global:
        co = [0]*n_verts*3
        verts.foreach_get('co', co)
        np_co = array(co).reshape((n_verts, 3))
        lx, ly, lz = array(np_co).transpose()
        if do_relative:
            rx = np.interp(lx, (lx.min(), lx.max()), (0, +1))
            ry = np.interp(ly, (ly.min(), ly.max()), (0, +1))
            rz = np.interp(lz, (lz.min(), lz.max()), (0, +1))
        if do_global:
            co = [v.co for v in verts]
            global_co = []
            for v in co:
                global_co.append(mat @ v)
            global_co = array(global_co).reshape((n_verts, 3))
            gx, gy, gz = array(global_co).transpose()
    # compute vertex normals
    if do_normal:
        normal = [0]*n_verts*3
        verts.foreach_get('normal', normal)
        normal = array(normal).reshape((n_verts, 3))
        nx, ny, nz = array(normal).transpose()

    try:
        weight = eval(formula)
        return weight
    except:
        return "There is something wrong"
    print("Weight Formula: " + str(timeit.default_timer() - start_time))

class weight_formula_wiki(Operator):
    bl_idname = "scene.weight_formula_wiki"
    bl_label = "Online Documentation"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        bpy.ops.wm.url_open(url="https://github.com/alessandro-zomparelli/tissue/wiki/Weight-Tools#weight-formula")
        return {'FINISHED'}

class weight_formula(Operator):
    bl_idname = "object.weight_formula"
    bl_label = "Weight Formula"
    bl_description = "Generate a Vertex Group according to a mathematical formula"
    bl_options = {'REGISTER', 'UNDO'}

    ex_items = [
        ('cos(arctan(nx/ny)*i1*2 + sin(rz*i3))/i2 + cos(arctan(nx/ny)*i1*2 - sin(rz*i3))/i2 + 0.5','Vertical Spots'),
        ('cos(arctan(nx/ny)*i1*2 + sin(rz*i2))/2 + cos(arctan(nx/ny)*i1*2 - sin(rz*i2))/2','Vertical Spots'),
        ('(sin(arctan(nx/ny)*i1*2)*sin(nz*i1*2)+1)/2','Grid Spots'),
        ('cos(arctan(nx/ny)*f1)','Vertical Stripes'),
        ('cos(arctan(lx/ly)*f1 + sin(rz*f2)*f3)','Curly Stripes'),
        ('sin(rz*pi*i1+arctan2(nx,ny))/2+0.5', 'Vertical Spiral'),
        ('sin(nx*15)<sin(ny*15)','Chess'),
        ('cos(ny*rz**2*i1)','Hyperbolic'),
        ('sin(rx*30) > 0','Step Stripes'),
        ('sin(nz*i1)','Normal Stripes'),
        ('w[0]**2','Vertex Group square'),
        ('abs(0.5-rz)*2','Double vertical gradient'),
        ('rz', 'Vertical Gradient')
    ]
    _ex_items = list((str(i),'{}   ( {} )'.format(s[0],s[1]),s[1]) for i,s in enumerate(ex_items))
    _ex_items.append(('CUSTOM', "User Formula", ""))

    examples : EnumProperty(
        items = _ex_items, default='CUSTOM', name="Examples")

    old_ex = ""

    formula : StringProperty(
        name="Formula", default="", description="Formula to Evaluate")

    slider_f01 : FloatProperty(
        name="f1", default=1, description="Slider Float 1")
    slider_f02 : FloatProperty(
        name="f2", default=1, description="Slider Float 2")
    slider_f03 : FloatProperty(
        name="f3", default=1, description="Slider Float 3")
    slider_f04 : FloatProperty(
        name="f4", default=1, description="Slider Float 4")
    slider_f05 : FloatProperty(
        name="f5", default=1, description="Slider Float 5")
    slider_i01 : IntProperty(
        name="i1", default=1, description="Slider Integer 1")
    slider_i02 : IntProperty(
        name="i2", default=1, description="Slider Integer 2")
    slider_i03 : IntProperty(
        name="i3", default=1, description="Slider Integer 3")
    slider_i04 : IntProperty(
        name="i4", default=1, description="Slider Integer 4")
    slider_i05 : IntProperty(
        name="i5", default=1, description="Slider Integer 5")

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

    def draw(self, context):
        layout = self.layout
        #layout.label(text="Examples")
        layout.prop(self, "examples", text="Examples")
        #if self.examples == 'CUSTOM':
        layout.label(text="Formula")
        layout.prop(self, "formula", text="")
        #try: self.examples = self.formula
        #except: pass

        if self.examples != 'CUSTOM':
            example = self.ex_items[int(self.examples)][0]
            if example != self.old_ex:
                self.formula = example
                self.old_ex = example
            elif self.formula != example:
                self.examples = 'CUSTOM'
        formula = self.formula

        layout.separator()
        if "f1" in formula: layout.prop(self, "slider_f01")
        if "f2" in formula: layout.prop(self, "slider_f02")
        if "f3" in formula: layout.prop(self, "slider_f03")
        if "f4" in formula: layout.prop(self, "slider_f04")
        if "f5" in formula: layout.prop(self, "slider_f05")
        if "i1" in formula: layout.prop(self, "slider_i01")
        if "i2" in formula: layout.prop(self, "slider_i02")
        if "i3" in formula: layout.prop(self, "slider_i03")
        if "i4" in formula: layout.prop(self, "slider_i04")
        if "i5" in formula: layout.prop(self, "slider_i05")

        layout.label(text="Variables (for each vertex):")
        layout.label(text="lx, ly, lz: Local Coordinates", icon='ORIENTATION_LOCAL')
        layout.label(text="gx, gy, gz: Global Coordinates", icon='WORLD')
        layout.label(text="rx, ry, rz: Local Coordinates (0 to 1)", icon='NORMALIZE_FCURVES')
        layout.label(text="nx, ny, nz: Normal Coordinates", icon='SNAP_NORMAL')
        layout.label(text="w[0], w[1], w[2], ... : Vertex Groups", icon="GROUP_VERTEX")
        layout.separator()
        layout.label(text="f1, f2, f3, f4, f5: Float Sliders", icon='MOD_HUE_SATURATION')#PROPERTIES
        layout.label(text="i1, i2, i3, i4, i5: Integer Sliders", icon='MOD_HUE_SATURATION')
        layout.separator()
        #layout.label(text="All mathematical functions are based on Numpy", icon='INFO')
        #layout.label(text="https://docs.scipy.org/doc/numpy-1.13.0/reference/routines.math.html", icon='INFO')
        layout.operator("scene.weight_formula_wiki", icon="HELP")
        #layout.label(text="(where 'i' is the index of the Vertex Group)")

    def execute(self, context):
        ob = context.active_object
        n_verts = len(ob.data.vertices)
        #if self.examples == 'CUSTOM':
        #    formula = self.formula
        #else:
        #self.formula = self.examples
        #    formula = self.examples

        #f1, f2, f3, f4, f5 = self.slider_f01, self.slider_f02, self.slider_f03, self.slider_f04, self.slider_f05
        #i1, i2, i3, i4, i5 = self.slider_i01, self.slider_i02, self.slider_i03, self.slider_i04, self.slider_i05
        f_sliders = self.slider_f01, self.slider_f02, self.slider_f03, self.slider_f04, self.slider_f05
        i_sliders = self.slider_i01, self.slider_i02, self.slider_i03, self.slider_i04, self.slider_i05

        if self.examples != 'CUSTOM':
            example = self.ex_items[int(self.examples)][0]
            if example != self.old_ex:
                self.formula = example
                self.old_ex = example
            elif self.formula != example:
                self.examples = 'CUSTOM'
        formula = self.formula

        if formula == "": return {'FINISHED'}
        # replace numeric sliders value
        for i, slider in enumerate(f_sliders):
            formula = formula.replace('f'+str(i+1),"{0:.2f}".format(slider))
        for i, slider in enumerate(i_sliders):
            formula =formula.replace('i'+str(i+1),str(slider))
        vertex_group_name = "" + formula
        ob.vertex_groups.new(name=vertex_group_name)

        weight = compute_formula(ob, formula=formula, float_var=f_sliders, int_var=i_sliders)
        if type(weight) == str:
            self.report({'ERROR'}, weight)
            return {'CANCELLED'}

        #start_time = timeit.default_timer()
        weight = nan_to_num(weight)
        vg = ob.vertex_groups[-1]
        if type(weight) == int or type(weight) == float:
            for i in range(n_verts):
                vg.add([i], weight, 'REPLACE')
        elif type(weight) == ndarray:
            for i in range(n_verts):
                vg.add([i], weight[i], 'REPLACE')
        ob.data.update()
        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')

        # Store formula settings
        new_formula = ob.formula_settings.add()
        new_formula.name = ob.vertex_groups[-1].name
        new_formula.formula = formula
        new_formula.int_var = i_sliders
        new_formula.float_var = f_sliders

        #for f in ob.formula_settings:
        #    print(f.name, f.formula, f.int_var, f.float_var)
        return {'FINISHED'}


class update_weight_formula(Operator):
    bl_idname = "object.update_weight_formula"
    bl_label = "Update Weight Formula"
    bl_description = "Update an existing Vertex Group. Make sure that the name\nof the active Vertex Group is a valid formula"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return len(context.object.vertex_groups) > 0

    def execute(self, context):
        ob = context.active_object
        n_verts = len(ob.data.vertices)

        vg = ob.vertex_groups.active
        formula = vg.name
        weight = compute_formula(ob, formula=formula)
        if type(weight) == str:
            self.report({'ERROR'}, "The name of the active Vertex Group\nis not a valid Formula")
            return {'CANCELLED'}

        #start_time = timeit.default_timer()
        weight = nan_to_num(weight)
        if type(weight) == int or type(weight) == float:
            for i in range(n_verts):
                vg.add([i], weight, 'REPLACE')
        elif type(weight) == ndarray:
            for i in range(n_verts):
                vg.add([i], weight[i], 'REPLACE')
        ob.data.update()
        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
        return {'FINISHED'}


class _weight_laplacian(Operator):
    bl_idname = "object._weight_laplacian"
    bl_label = "Weight Laplacian"
    bl_description = ("Compute the Vertex Group Laplacian")
    bl_options = {'REGISTER', 'UNDO'}

    bounds : EnumProperty(
        items=(('MANUAL', "Manual Bounds", ""),
            ('POSITIVE', "Positive Only", ""),
            ('NEGATIVE', "Negative Only", ""),
            ('AUTOMATIC', "Automatic Bounds", "")),
        default='AUTOMATIC', name="Bounds")

    mode : EnumProperty(
        items=(('LENGTH', "Length Weight", ""),
            ('SIMPLE', "Simple", "")),
        default='SIMPLE', name="Evaluation Mode")

    min_def : FloatProperty(
        name="Min", default=0, soft_min=-1, soft_max=0,
        description="Laplacian value with 0 weight")

    max_def : FloatProperty(
        name="Max", default=0.5, soft_min=0, soft_max=5,
        description="Laplacian value with 1 weight")

    bounds_string = ""

    frame = None

    @classmethod
    def poll(cls, context):
        return len(context.object.vertex_groups) > 0

    def draw(self, context):
        layout = self.layout
        col = layout.column(align=True)
        col.label(text="Evaluation Mode")
        col.prop(self, "mode", text="")
        col.label(text="Bounds")
        col.prop(self, "bounds", text="")
        if self.bounds == 'MANUAL':
            col.label(text="Strain Rate \u03B5:")
            col.prop(self, "min_def")
            col.prop(self, "max_def")
        col.label(text="\u03B5" + ": from " + self.bounds_string)


    def execute(self, context):
        try: ob = context.object
        except:
            self.report({'ERROR'}, "Please select an Object")
            return {'CANCELLED'}

        group_id = ob.vertex_groups.active_index
        input_group = ob.vertex_groups[group_id].name

        group_name = "Laplacian"
        ob.vertex_groups.new(name=group_name)
        me = ob.data
        bm = bmesh.new()
        bm.from_mesh(me)
        bm.edges.ensure_lookup_table()

        # store weight values
        weight = []
        for v in me.vertices:
            try:
                weight.append(ob.vertex_groups[input_group].weight(v.index))
            except:
                weight.append(0)

        n_verts = len(bm.verts)
        lap = [0]*n_verts
        for e in bm.edges:
            if self.mode == 'LENGTH':
                length = e.calc_length()
                if length == 0: continue
                id0 = e.verts[0].index
                id1 = e.verts[1].index
                lap[id0] += weight[id1]/length - weight[id0]/length
                lap[id1] += weight[id0]/length - weight[id1]/length
            else:
                id0 = e.verts[0].index
                id1 = e.verts[1].index
                lap[id0] += weight[id1] - weight[id0]
                lap[id1] += weight[id0] - weight[id1]

        mean_lap = mean(lap)
        stdev_lap = stdev(lap)
        filter_lap = [i for i in lap if mean_lap-2*stdev_lap < i < mean_lap+2*stdev_lap]
        if self.bounds == 'MANUAL':
            min_def = self.min_def
            max_def = self.max_def
        elif self.bounds == 'AUTOMATIC':
            min_def = min(filter_lap)
            max_def = max(filter_lap)
            self.min_def = min_def
            self.max_def = max_def
        elif self.bounds == 'NEGATIVE':
            min_def = 0
            max_def = min(filter_lap)
            self.min_def = min_def
            self.max_def = max_def
        elif self.bounds == 'POSITIVE':
            min_def = 0
            max_def = max(filter_lap)
            self.min_def = min_def
            self.max_def = max_def
        delta_def = max_def - min_def

        # check undeformed errors
        if delta_def == 0: delta_def = 0.0001

        for i in range(len(lap)):
            val = (lap[i]-min_def)/delta_def
            if val > 0.7: print(str(val) + " " + str(lap[i]))
            #val = weight[i] + 0.2*lap[i]
            ob.vertex_groups[-1].add([i], val, 'REPLACE')
        self.bounds_string = str(round(min_def,2)) + " to " + str(round(max_def,2))
        ob.vertex_groups[-1].name = group_name + " " + self.bounds_string
        ob.vertex_groups.update()
        ob.data.update()
        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
        bm.free()
        return {'FINISHED'}

class ok_weight_laplacian(Operator):
    bl_idname = "object.weight_laplacian"
    bl_label = "Weight Laplacian"
    bl_description = ("Compute the Vertex Group Laplacian")
    bl_options = {'REGISTER', 'UNDO'}

    bounds_string = ""

    frame = None

    @classmethod
    def poll(cls, context):
        return len(context.object.vertex_groups) > 0


    def execute(self, context):
        try: ob = context.object
        except:
            self.report({'ERROR'}, "Please select an Object")
            return {'CANCELLED'}

        me = ob.data
        bm = bmesh.new()
        bm.from_mesh(me)
        bm.edges.ensure_lookup_table()

        group_id = ob.vertex_groups.active_index
        input_group = ob.vertex_groups[group_id].name

        group_name = "Laplacian"
        ob.vertex_groups.new(name=group_name)

        # store weight values
        a = []
        for v in me.vertices:
            try:
                a.append(ob.vertex_groups[input_group].weight(v.index))
            except:
                a.append(0)

        a = array(a)


        # initialize
        n_verts = len(bm.verts)
        # find max number of edges for vertex
        max_edges = 0
        n_neighbors = []
        id_neighbors = []
        for v in bm.verts:
            n_edges = len(v.link_edges)
            max_edges = max(max_edges, n_edges)
            n_neighbors.append(n_edges)
            neighbors = []
            for e in v.link_edges:
                for v1 in e.verts:
                    if v != v1: neighbors.append(v1.index)
            id_neighbors.append(neighbors)
        n_neighbors = array(n_neighbors)


        lap_map = [[] for i in range(n_verts)]
        #lap_map = []
        '''
        for e in bm.edges:
            id0 = e.verts[0].index
            id1 = e.verts[1].index
            lap_map[id0].append(id1)
            lap_map[id1].append(id0)
        '''
        lap = zeros((n_verts))#[0]*n_verts
        n_records = zeros((n_verts))
        for e in bm.edges:
            id0 = e.verts[0].index
            id1 = e.verts[1].index
            length = e.calc_length()
            if length == 0: continue
            #lap[id0] += abs(a[id1] - a[id0])/length
            #lap[id1] += abs(a[id0] - a[id1])/length
            lap[id0] += (a[id1] - a[id0])/length
            lap[id1] += (a[id0] - a[id1])/length
            n_records[id0]+=1
            n_records[id1]+=1
        lap /= n_records
        lap /= max(lap)

        for i in range(n_verts):
            ob.vertex_groups['Laplacian'].add([i], lap[i], 'REPLACE')
        ob.vertex_groups.update()
        ob.data.update()
        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
        bm.free()
        return {'FINISHED'}

class weight_laplacian(Operator):
    bl_idname = "object.weight_laplacian"
    bl_label = "Weight Laplacian"
    bl_description = ("Compute the Vertex Group Laplacian")
    bl_options = {'REGISTER', 'UNDO'}

    bounds_string = ""

    frame = None

    @classmethod
    def poll(cls, context):
        return len(context.object.vertex_groups) > 0


    def execute(self, context):
        try: ob = context.object
        except:
            self.report({'ERROR'}, "Please select an Object")
            return {'CANCELLED'}

        me = ob.data
        bm = bmesh.new()
        bm.from_mesh(me)
        bm.edges.ensure_lookup_table()
        n_verts = len(me.vertices)

        group_id = ob.vertex_groups.active_index
        input_group = ob.vertex_groups[group_id].name

        group_name = "Laplacian"
        vg = ob.vertex_groups.new(name=group_name)

        # store weight values
        dvert_lay = bm.verts.layers.deform.active
        weight = bmesh_get_weight_numpy(group_id, dvert_lay, bm.verts)

        #verts, normals = get_vertices_and_normals_numpy(me)

        #lap = zeros((n_verts))#[0]*n_verts
        lap = [Vector((0,0,0)) for i in range(n_verts)]
        n_records = zeros((n_verts))
        for e in bm.edges:
            vert0 = e.verts[0]
            vert1 = e.verts[1]
            id0 = vert0.index
            id1 = vert1.index
            v0 = vert0.co
            v1 = vert1.co
            v01 = v1-v0
            v10 = -v01
            v01 -= v01.project(vert0.normal)
            v10 -= v10.project(vert1.normal)
            length = e.calc_length()
            if length == 0: continue
            dw = (weight[id1] - weight[id0])/length
            lap[id0] += v01.normalized() * dw
            lap[id1] -= v10.normalized() * dw
            n_records[id0]+=1
            n_records[id1]+=1
        #lap /= n_records[:,np.newaxis]
        lap = [l.length/r for r,l in zip(n_records,lap)]

        lap = np.array(lap)
        lap /= np.max(lap)
        lap = list(lap)
        print(lap)

        for i in range(n_verts):
            vg.add([i], lap[i], 'REPLACE')
        ob.vertex_groups.update()
        ob.data.update()
        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
        bm.free()
        return {'FINISHED'}


class reaction_diffusion(Operator):
    bl_idname = "object.reaction_diffusion"
    bl_label = "Reaction Diffusion"
    bl_description = ("Run a Reaction-Diffusion based on existing Vertex Groups: A and B")
    bl_options = {'REGISTER', 'UNDO'}

    steps : IntProperty(
        name="Steps", default=10, min=0, soft_max=50,
        description="Number of Steps")

    dt : FloatProperty(
        name="dt", default=0.2, min=0, soft_max=0.2,
        description="Time Step")

    diff_a : FloatProperty(
        name="Diff A", default=1, min=0, soft_max=2,
        description="Diffusion A")

    diff_b : FloatProperty(
        name="Diff B", default=0.5, min=0, soft_max=2,
        description="Diffusion B")

    f : FloatProperty(
        name="f", default=0.055, min=0, soft_min=0.01, soft_max=0.06, max=0.1, precision=4,
        description="Feed Rate")

    k : FloatProperty(
        name="k", default=0.062, min=0, soft_min=0.035, soft_max=0.065, max=0.1, precision=4,
        description="Kill Rate")

    bounds_string = ""

    frame = None

    @classmethod
    def poll(cls, context):
        return len(context.object.vertex_groups) > 0


    def execute(self, context):
        #bpy.app.handlers.frame_change_post.remove(reaction_diffusion_def)
        reaction_diffusion_add_handler(self, context)
        set_animatable_fix_handler(self, context)
        try: ob = context.object
        except:
            self.report({'ERROR'}, "Please select an Object")
            return {'CANCELLED'}

        me = ob.data
        bm = bmesh.new()
        bm.from_mesh(me)
        bm.edges.ensure_lookup_table()

        # store weight values
        a = []
        b = []
        for v in me.vertices:
            try:
                a.append(ob.vertex_groups["A"].weight(v.index))
            except:
                a.append(0)
            try:
                b.append(ob.vertex_groups["B"].weight(v.index))
            except:
                b.append(0)

        a = array(a)
        b = array(b)
        f = self.f
        k = self.k
        diff_a = self.diff_a
        diff_b = self.diff_b
        dt = self.dt
        n_verts = len(bm.verts)

        for i in range(self.steps):

            lap_a = zeros((n_verts))#[0]*n_verts
            lap_b = zeros((n_verts))#[0]*n_verts
            for e in bm.edges:
                id0 = e.verts[0].index
                id1 = e.verts[1].index
                lap_a[id0] += a[id1] - a[id0]
                lap_a[id1] += a[id0] - a[id1]
                lap_b[id0] += b[id1] - b[id0]
                lap_b[id1] += b[id0] - b[id1]
            ab2 = a*b**2
            a += (diff_a*lap_a - ab2 + f*(1-a))*dt
            b += (diff_b*lap_b + ab2 - (k+f)*b)*dt

            for i in range(n_verts):
                ob.vertex_groups['A'].add([i], a[i], 'REPLACE')
                ob.vertex_groups['B'].add([i], b[i], 'REPLACE')
            ob.vertex_groups.update()
            ob.data.update()

            bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)

        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
        bm.free()
        return {'FINISHED'}


class edges_deformation(Operator):
    bl_idname = "object.edges_deformation"
    bl_label = "Edges Deformation"
    bl_description = (
        "Compute Weight based on the deformation of edges "
        "according to visible modifiers"
    )
    bl_options = {'REGISTER', 'UNDO'}

    bounds : EnumProperty(
        items=(('MANUAL', "Manual Bounds", ""),
            ('COMPRESSION', "Compressed Only", ""),
            ('TENSION', "Extended Only", ""),
            ('AUTOMATIC', "Automatic Bounds", "")),
        default='AUTOMATIC', name="Bounds")

    mode : EnumProperty(
        items=(('MAX', "Max Deformation", ""),
            ('MEAN', "Average Deformation", "")),
        default='MEAN', name="Evaluation Mode")

    min_def : FloatProperty(
        name="Min", default=0, soft_min=-1, soft_max=0,
        description="Deformations with 0 weight")

    max_def : FloatProperty(
        name="Max", default=0.5, soft_min=0, soft_max=5,
        description="Deformations with 1 weight")

    bounds_string = ""

    frame = None

    @classmethod
    def poll(cls, context):
        return len(context.object.modifiers) > 0

    def draw(self, context):
        layout = self.layout
        col = layout.column(align=True)
        col.label(text="Evaluation Mode")
        col.prop(self, "mode", text="")
        col.label(text="Bounds")
        col.prop(self, "bounds", text="")
        if self.bounds == 'MANUAL':
            col.label(text="Strain Rate \u03B5:")
            col.prop(self, "min_def")
            col.prop(self, "max_def")
        col.label(text="\u03B5" + ": from " + self.bounds_string)

    def execute(self, context):
        try: ob = context.object
        except:
            self.report({'ERROR'}, "Please select an Object")
            return {'CANCELLED'}

        # check if the object is Cloth or Softbody
        physics = False
        for m in ob.modifiers:
            if m.type == 'CLOTH' or m.type == 'SOFT_BODY':
                physics = True
                if context.scene.frame_current == 1 and self.frame != None:
                    context.scene.frame_current = self.frame
                break
        if not physics: self.frame = None

        if self.mode == 'MEAN': group_name = "Average Deformation"
        elif self.mode == 'MAX': group_name = "Max Deformation"
        ob.vertex_groups.new(name=group_name)
        me0 = ob.data

        me = simple_to_mesh(ob) #ob.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy()
        if len(me.vertices) != len(me0.vertices) or len(me.edges) != len(me0.edges):
            self.report({'ERROR'}, "The topology of the object should be" +
                "unaltered")
            return {'CANCELLED'}

        bm0 = bmesh.new()
        bm0.from_mesh(me0)
        bm = bmesh.new()
        bm.from_mesh(me)
        deformations = []
        for e0, e in zip(bm0.edges, bm.edges):
            try:
                l0 = e0.calc_length()
                l1 = e.calc_length()
                epsilon = (l1 - l0)/l0
                deformations.append(epsilon)
            except: deformations.append(1)
        v_deformations = []
        for v in bm.verts:
            vdef = []
            for e in v.link_edges:
                vdef.append(deformations[e.index])
            if self.mode == 'MEAN': v_deformations.append(mean(vdef))
            elif self.mode == 'MAX': v_deformations.append(max(vdef, key=abs))
            #elif self.mode == 'MIN': v_deformations.append(min(vdef, key=abs))

        if self.bounds == 'MANUAL':
            min_def = self.min_def
            max_def = self.max_def
        elif self.bounds == 'AUTOMATIC':
            min_def = min(v_deformations)
            max_def = max(v_deformations)
            self.min_def = min_def
            self.max_def = max_def
        elif self.bounds == 'COMPRESSION':
            min_def = 0
            max_def = min(v_deformations)
            self.min_def = min_def
            self.max_def = max_def
        elif self.bounds == 'TENSION':
            min_def = 0
            max_def = max(v_deformations)
            self.min_def = min_def
            self.max_def = max_def
        delta_def = max_def - min_def

        # check undeformed errors
        if delta_def == 0:
            if self.bounds == 'MANUAL':
                delta_def = 0.0001
            else:
                message = "The object doesn't have deformations."
                if physics:
                    message = message + ("\nIf you are using Physics try to " +
                        "save it in the cache before.")
                self.report({'ERROR'}, message)
                return {'CANCELLED'}
        else:
            if physics:
                self.frame = context.scene.frame_current

        for i in range(len(v_deformations)):
            weight = (v_deformations[i] - min_def)/delta_def
            ob.vertex_groups[-1].add([i], weight, 'REPLACE')
        self.bounds_string = str(round(min_def,2)) + " to " + str(round(max_def,2))
        ob.vertex_groups[-1].name = group_name + " " + self.bounds_string
        ob.vertex_groups.update()
        ob.data.update()
        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
        bpy.data.meshes.remove(me)
        bm.free()
        bm0.free()
        return {'FINISHED'}

class edges_bending(Operator):
    bl_idname = "object.edges_bending"
    bl_label = "Edges Bending"
    bl_description = (
        "Compute Weight based on the bending of edges"
        "according to visible modifiers"
    )
    bl_options = {'REGISTER', 'UNDO'}

    bounds : EnumProperty(
        items=(('MANUAL', "Manual Bounds", ""),
            ('POSITIVE', "Positive Only", ""),
            ('NEGATIVE', "Negative Only", ""),
            ('UNSIGNED', "Absolute Bending", ""),
            ('AUTOMATIC', "Signed Bending", "")),
        default='AUTOMATIC', name="Bounds")

    min_def : FloatProperty(
        name="Min", default=-10, soft_min=-45, soft_max=45,
        description="Deformations with 0 weight")

    max_def : FloatProperty(
        name="Max", default=10, soft_min=-45, soft_max=45,
        description="Deformations with 1 weight")

    bounds_string = ""
    frame = None

    @classmethod
    def poll(cls, context):
        return len(context.object.modifiers) > 0

    def draw(self, context):
        layout = self.layout
        layout.label(text="Bounds")
        layout.prop(self, "bounds", text="")
        if self.bounds == 'MANUAL':
            layout.prop(self, "min_def")
            layout.prop(self, "max_def")

    def execute(self, context):
        try: ob = context.object
        except:
            self.report({'ERROR'}, "Please select an Object")
            return {'CANCELLED'}

        group_name = "Edges Bending"
        ob.vertex_groups.new(name=group_name)

        # check if the object is Cloth or Softbody
        physics = False
        for m in ob.modifiers:
            if m.type == 'CLOTH' or m.type == 'SOFT_BODY':
                physics = True
                if context.scene.frame_current == 1 and self.frame != None:
                    context.scene.frame_current = self.frame
                break
        if not physics: self.frame = None

        #ob.data.update()
        #context.scene.update()
        me0 = ob.data
        me = simple_to_mesh(ob) #ob.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy()
        if len(me.vertices) != len(me0.vertices) or len(me.edges) != len(me0.edges):
            self.report({'ERROR'}, "The topology of the object should be" +
                "unaltered")
        bm0 = bmesh.new()
        bm0.from_mesh(me0)
        bm = bmesh.new()
        bm.from_mesh(me)
        deformations = []
        for e0, e in zip(bm0.edges, bm.edges):
            try:
                ang = e.calc_face_angle_signed()
                ang0 = e0.calc_face_angle_signed()
                if self.bounds == 'UNSIGNED':
                    deformations.append(abs(ang-ang0))
                else:
                    deformations.append(ang-ang0)
            except: deformations.append(0)
        v_deformations = []
        for v in bm.verts:
            vdef = []
            for e in v.link_edges:
                vdef.append(deformations[e.index])
            v_deformations.append(mean(vdef))
        if self.bounds == 'MANUAL':
            min_def = radians(self.min_def)
            max_def = radians(self.max_def)
        elif self.bounds == 'AUTOMATIC':
            min_def = min(v_deformations)
            max_def = max(v_deformations)
        elif self.bounds == 'POSITIVE':
            min_def = 0
            max_def = min(v_deformations)
        elif self.bounds == 'NEGATIVE':
            min_def = 0
            max_def = max(v_deformations)
        elif self.bounds == 'UNSIGNED':
            min_def = 0
            max_def = max(v_deformations)
        delta_def = max_def - min_def

        # check undeformed errors
        if delta_def == 0:
            if self.bounds == 'MANUAL':
                delta_def = 0.0001
            else:
                message = "The object doesn't have deformations."
                if physics:
                    message = message + ("\nIf you are using Physics try to " +
                        "save it in the cache before.")
                self.report({'ERROR'}, message)
                return {'CANCELLED'}
        else:
            if physics:
                self.frame = context.scene.frame_current

        for i in range(len(v_deformations)):
            weight = (v_deformations[i] - min_def)/delta_def
            ob.vertex_groups[-1].add([i], weight, 'REPLACE')
        self.bounds_string = str(round(min_def,2)) + " to " + str(round(max_def,2))
        ob.vertex_groups[-1].name = group_name + " " + self.bounds_string
        ob.vertex_groups.update()
        ob.data.update()
        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
        bpy.data.meshes.remove(me)
        bm0.free()
        bm.free()
        return {'FINISHED'}

class weight_contour_displace(Operator):
    bl_idname = "object.weight_contour_displace"
    bl_label = "Contour Displace"
    bl_description = ("")
    bl_options = {'REGISTER', 'UNDO'}

    use_modifiers : BoolProperty(
        name="Use Modifiers", default=True,
        description="Apply all the modifiers")
    min_iso : FloatProperty(
        name="Min Iso Value", default=0.49, min=0, max=1,
        description="Threshold value")
    max_iso : FloatProperty(
        name="Max Iso Value", default=0.51, min=0, max=1,
        description="Threshold value")
    n_cuts : IntProperty(
        name="Cuts", default=2, min=1, soft_max=10,
        description="Number of cuts in the selected range of values")
    bool_displace : BoolProperty(
        name="Add Displace", default=True, description="Add Displace Modifier")
    bool_flip : BoolProperty(
        name="Flip", default=False, description="Flip Output Weight")

    weight_mode : EnumProperty(
        items=[('Remapped', 'Remapped', 'Remap values'),
               ('Alternate', 'Alternate', 'Alternate 0 and 1'),
               ('Original', 'Original', 'Keep original Vertex Group')],
        name="Weight", description="Choose how to convert vertex group",
        default="Remapped", options={'LIBRARY_EDITABLE'})

    @classmethod
    def poll(cls, context):
        return len(context.object.vertex_groups) > 0

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

    def execute(self, context):
        start_time = timeit.default_timer()
        try:
            check = context.object.vertex_groups[0]
        except:
            self.report({'ERROR'}, "The object doesn't have Vertex Groups")
            return {'CANCELLED'}

        ob0 = context.object

        group_id = ob0.vertex_groups.active_index
        vertex_group_name = ob0.vertex_groups[group_id].name

        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.select_all(action='SELECT')
        bpy.ops.object.mode_set(mode='OBJECT')
        if self.use_modifiers:
            #me0 = ob0.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy()
            me0 = simple_to_mesh(ob0)
        else:
            me0 = ob0.data.copy()

        # generate new bmesh
        bm = bmesh.new()
        bm.from_mesh(me0)
        bm.verts.ensure_lookup_table()
        bm.edges.ensure_lookup_table()
        bm.faces.ensure_lookup_table()

        # store weight values
        weight = []
        ob = bpy.data.objects.new("temp", me0)
        for g in ob0.vertex_groups:
            ob.vertex_groups.new(name=g.name)
        for v in me0.vertices:
            try:
                weight.append(ob.vertex_groups[vertex_group_name].weight(v.index))
            except:
                weight.append(0)

        # define iso values
        iso_values = []
        for i_cut in range(self.n_cuts):
            delta_iso = abs(self.max_iso - self.min_iso)
            min_iso = min(self.min_iso, self.max_iso)
            max_iso = max(self.min_iso, self.max_iso)
            if delta_iso == 0: iso_val = min_iso
            elif self.n_cuts > 1: iso_val = i_cut/(self.n_cuts-1)*delta_iso + min_iso
            else: iso_val = (self.max_iso + self.min_iso)/2
            iso_values.append(iso_val)

        # Start Cuts Iterations
        filtered_edges = bm.edges
        for iso_val in iso_values:
            delete_edges = []

            faces_mask = []
            for f in bm.faces:
                w_min = 2
                w_max = 2
                for v in f.verts:
                    w = weight[v.index]
                    if w_min == 2:
                        w_max = w_min = w
                    if w > w_max: w_max = w
                    if w < w_min: w_min = w
                    if w_min < iso_val and w_max > iso_val:
                        faces_mask.append(f)
                        break

            #link_faces = [[f for f in e.link_faces] for e in bm.edges]

            #faces_todo = [f.select for f in bm.faces]
            #faces_todo = [True for f in bm.faces]
            verts = []
            edges = []
            edges_id = {}
            _filtered_edges = []
            n_verts = len(bm.verts)
            count = n_verts
            for e in filtered_edges:
                #id0 = e.vertices[0]
                #id1 = e.vertices[1]
                id0 = e.verts[0].index
                id1 = e.verts[1].index
                w0 = weight[id0]
                w1 = weight[id1]

                if w0 == w1: continue
                elif w0 > iso_val and w1 > iso_val:
                    _filtered_edges.append(e)
                    continue
                elif w0 < iso_val and w1 < iso_val: continue
                elif w0 == iso_val or w1 == iso_val:
                    _filtered_edges.append(e)
                    continue
                else:
                    v0 = bm.verts[id0].co
                    v1 = bm.verts[id1].co
                    v = v0.lerp(v1, (iso_val-w0)/(w1-w0))
                    if e not in delete_edges:
                        delete_edges.append(e)
                    verts.append(v)
                    edges_id[str(id0)+"_"+str(id1)] = count
                    edges_id[str(id1)+"_"+str(id0)] = count
                    count += 1
                    _filtered_edges.append(e)
            filtered_edges = _filtered_edges
            splitted_faces = []

            switch = False
            # splitting faces
            for f in faces_mask:
                # create sub-faces slots. Once a new vertex is reached it will
                # change slot, storing the next vertices for a new face.
                build_faces = [[],[]]
                #switch = False
                verts0 = [v.index for v in f.verts]
                verts1 = list(verts0)
                verts1.append(verts1.pop(0)) # shift list
                for id0, id1 in zip(verts0, verts1):

                    # add first vertex to active slot
                    build_faces[switch].append(id0)

                    # try to split edge
                    try:
                        # check if the edge must be splitted
                        new_vert = edges_id[str(id0)+"_"+str(id1)]
                        # add new vertex
                        build_faces[switch].append(new_vert)
                        # if there is an open face on the other slot
                        if len(build_faces[not switch]) > 0:
                            # store actual face
                            splitted_faces.append(build_faces[switch])
                            # reset actual faces and switch
                            build_faces[switch] = []
                            # change face slot
                        switch = not switch
                        # continue previous face
                        build_faces[switch].append(new_vert)
                    except: pass
                if len(build_faces[not switch]) == 2:
                    build_faces[not switch].append(id0)
                if len(build_faces[not switch]) > 2:
                    splitted_faces.append(build_faces[not switch])
                # add last face
                splitted_faces.append(build_faces[switch])
                #del_faces.append(f.index)

            # adding new vertices
            _new_vert = bm.verts.new
            for v in verts: new_vert = _new_vert(v)
            bm.verts.index_update()
            bm.verts.ensure_lookup_table()
            # adding new faces
            _new_face = bm.faces.new
            missed_faces = []
            added_faces = []
            for f in splitted_faces:
                try:
                    face_verts = [bm.verts[i] for i in f]
                    new_face = _new_face(face_verts)
                    for e in new_face.edges:
                        filtered_edges.append(e)
                except:
                    missed_faces.append(f)

            bm.faces.ensure_lookup_table()
            # updating weight values
            weight = weight + [iso_val]*len(verts)

            # deleting old edges/faces
            _remove_edge = bm.edges.remove
            bm.edges.ensure_lookup_table()
            for e in delete_edges:
                _remove_edge(e)
            _filtered_edges = []
            for e in filtered_edges:
                if e not in delete_edges: _filtered_edges.append(e)
            filtered_edges = _filtered_edges

        name = ob0.name + '_ContourDisp'
        me = bpy.data.meshes.new(name)
        bm.to_mesh(me)
        bm.free()
        ob = bpy.data.objects.new(name, me)

        # Link object to scene and make active
        scn = context.scene
        context.collection.objects.link(ob)
        context.view_layer.objects.active = ob
        ob.select_set(True)
        ob0.select_set(False)

        # generate new vertex group
        for g in ob0.vertex_groups:
            ob.vertex_groups.new(name=g.name)
        #ob.vertex_groups.new(name=vertex_group_name)

        all_weight = weight + [iso_val]*len(verts)
        #mult = 1/(1-iso_val)
        for id in range(len(all_weight)):
            #if False: w = (all_weight[id]-iso_val)*mult
            w = all_weight[id]
            if self.weight_mode == 'Alternate':
                direction = self.bool_flip
                for i in range(len(iso_values)-1):
                    val0, val1 = iso_values[i], iso_values[i+1]
                    if val0 < w <= val1:
                        if direction: w1 = (w-val0)/(val1-val0)
                        else: w1 = (val1-w)/(val1-val0)
                    direction = not direction
                if w < iso_values[0]: w1 = not self.bool_flip
                if w > iso_values[-1]: w1 = not direction
            elif self.weight_mode == 'Remapped':
                if w < min_iso: w1 = 0
                elif w > max_iso: w1 = 1
                else: w1 = (w - min_iso)/delta_iso
            else:
                if self.bool_flip: w1 = 1-w
                else: w1 = w
            ob.vertex_groups[vertex_group_name].add([id], w1, 'REPLACE')

        ob.vertex_groups.active_index = group_id

        # align new object
        ob.matrix_world = ob0.matrix_world

        # Displace Modifier
        if self.bool_displace:
            displace_modifier = ob.modifiers.new(type='DISPLACE', name='Displace')
            displace_modifier.mid_level = 0
            displace_modifier.strength = 0.1
            displace_modifier.vertex_group = vertex_group_name

        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
        print("Contour Displace time: " + str(timeit.default_timer() - start_time) + " sec")

        bpy.data.meshes.remove(me0)

        return {'FINISHED'}

class weight_contour_mask(Operator):
    bl_idname = "object.weight_contour_mask"
    bl_label = "Contour Mask"
    bl_description = ("")
    bl_options = {'REGISTER', 'UNDO'}

    use_modifiers : BoolProperty(
        name="Use Modifiers", default=True,
        description="Apply all the modifiers")
    iso : FloatProperty(
        name="Iso Value", default=0.5, soft_min=0, soft_max=1,
        description="Threshold value")
    bool_solidify : BoolProperty(
        name="Solidify", default=True, description="Add Solidify Modifier")
    offset : FloatProperty(
        name="Offset", default=1, min=0, max=1,
        description="Offset")
    thickness : FloatProperty(
        name="Thickness", default=0.5, soft_min=0, soft_max=1,
        description="Thickness")
    normalize_weight : BoolProperty(
        name="Normalize Weight", default=True,
        description="Normalize weight of remaining vertices")

    @classmethod
    def poll(cls, context):
        return len(context.object.vertex_groups) > 0

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

    def execute(self, context):
        start_time = timeit.default_timer()
        try:
            check = context.object.vertex_groups[0]
        except:
            self.report({'ERROR'}, "The object doesn't have Vertex Groups")
            return {'CANCELLED'}

        ob0 = bpy.context.object

        iso_val = self.iso
        group_id = ob0.vertex_groups.active_index
        vertex_group_name = ob0.vertex_groups[group_id].name

        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.select_all(action='SELECT')
        bpy.ops.object.mode_set(mode='OBJECT')
        if self.use_modifiers:
            me0 = simple_to_mesh(ob0)#ob0.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy()
        else:
            me0 = ob0.data.copy()

        # generate new bmesh
        bm = bmesh.new()
        bm.from_mesh(me0)
        bm.verts.ensure_lookup_table()
        bm.edges.ensure_lookup_table()
        bm.faces.ensure_lookup_table()

        # store weight values
        weight = []
        ob = bpy.data.objects.new("temp", me0)
        for g in ob0.vertex_groups:
            ob.vertex_groups.new(name=g.name)
        for v in me0.vertices:
            try:
                #weight.append(v.groups[vertex_group_name].weight)
                weight.append(ob.vertex_groups[vertex_group_name].weight(v.index))
            except:
                weight.append(0)

        faces_mask = []
        for f in bm.faces:
            w_min = 2
            w_max = 2
            for v in f.verts:
                w = weight[v.index]
                if w_min == 2:
                    w_max = w_min = w
                if w > w_max: w_max = w
                if w < w_min: w_min = w
                if w_min < iso_val and w_max > iso_val:
                    faces_mask.append(f)
                    break

        filtered_edges = bm.edges# me0.edges
        faces_todo = [f.select for f in bm.faces]
        verts = []
        edges = []
        delete_edges = []
        edges_id = {}
        _filtered_edges = []
        n_verts = len(bm.verts)
        count = n_verts
        for e in filtered_edges:
            id0 = e.verts[0].index
            id1 = e.verts[1].index
            w0 = weight[id0]
            w1 = weight[id1]

            if w0 == w1: continue
            elif w0 > iso_val and w1 > iso_val:
                continue
            elif w0 < iso_val and w1 < iso_val: continue
            elif w0 == iso_val or w1 == iso_val: continue
            else:
                v0 = me0.vertices[id0].co
                v1 = me0.vertices[id1].co
                v = v0.lerp(v1, (iso_val-w0)/(w1-w0))
                delete_edges.append(e)
                verts.append(v)
                edges_id[str(id0)+"_"+str(id1)] = count
                edges_id[str(id1)+"_"+str(id0)] = count
                count += 1

        splitted_faces = []

        switch = False
        # splitting faces
        for f in faces_mask:
            # create sub-faces slots. Once a new vertex is reached it will
            # change slot, storing the next vertices for a new face.
            build_faces = [[],[]]
            #switch = False
            verts0 = list(me0.polygons[f.index].vertices)
            verts1 = list(verts0)
            verts1.append(verts1.pop(0)) # shift list
            for id0, id1 in zip(verts0, verts1):

                # add first vertex to active slot
                build_faces[switch].append(id0)

                # try to split edge
                try:
                    # check if the edge must be splitted
                    new_vert = edges_id[str(id0)+"_"+str(id1)]
                    # add new vertex
                    build_faces[switch].append(new_vert)
                    # if there is an open face on the other slot
                    if len(build_faces[not switch]) > 0:
                        # store actual face
                        splitted_faces.append(build_faces[switch])
                        # reset actual faces and switch
                        build_faces[switch] = []
                        # change face slot
                    switch = not switch
                    # continue previous face
                    build_faces[switch].append(new_vert)
                except: pass
            if len(build_faces[not switch]) == 2:
                build_faces[not switch].append(id0)
            if len(build_faces[not switch]) > 2:
                splitted_faces.append(build_faces[not switch])
            # add last face
            splitted_faces.append(build_faces[switch])

        # adding new vertices
        _new_vert = bm.verts.new
        for v in verts: _new_vert(v)
        bm.verts.ensure_lookup_table()

        # deleting old edges/faces
        _remove_edge = bm.edges.remove
        bm.edges.ensure_lookup_table()
        remove_edges = []
        for e in delete_edges: _remove_edge(e)

        bm.verts.ensure_lookup_table()
        # adding new faces
        _new_face = bm.faces.new
        missed_faces = []
        for f in splitted_faces:
            try:
                face_verts = [bm.verts[i] for i in f]
                _new_face(face_verts)
            except:
                missed_faces.append(f)

        # Mask geometry
        if(True):
            _remove_vert = bm.verts.remove
            all_weight = weight + [iso_val+0.0001]*len(verts)
            weight = []
            for w, v in zip(all_weight, bm.verts):
                if w < iso_val: _remove_vert(v)
                else: weight.append(w)

        # Create mesh and object
        name = ob0.name + '_ContourMask_{:.3f}'.format(iso_val)
        me = bpy.data.meshes.new(name)
        bm.to_mesh(me)
        bm.free()
        ob = bpy.data.objects.new(name, me)

        # Link object to scene and make active
        scn = context.scene
        context.collection.objects.link(ob)
        context.view_layer.objects.active = ob
        ob.select_set(True)
        ob0.select_set(False)

        # generate new vertex group
        for g in ob0.vertex_groups:
            ob.vertex_groups.new(name=g.name)

        if iso_val != 1: mult = 1/(1-iso_val)
        else: mult = 1
        for id in range(len(weight)):
            if self.normalize_weight: w = (weight[id]-iso_val)*mult
            else: w = weight[id]
            ob.vertex_groups[vertex_group_name].add([id], w, 'REPLACE')
        ob.vertex_groups.active_index = group_id

        # align new object
        ob.matrix_world = ob0.matrix_world

        # Add Solidify
        if self.bool_solidify and True:
            ob.modifiers.new(type='SOLIDIFY', name='Solidify')
            ob.modifiers['Solidify'].thickness = self.thickness
            ob.modifiers['Solidify'].offset = self.offset
            ob.modifiers['Solidify'].vertex_group = vertex_group_name

        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
        print("Contour Mask time: " + str(timeit.default_timer() - start_time) + " sec")

        bpy.data.meshes.remove(me0)

        return {'FINISHED'}


class weight_contour_mask_wip(Operator):
    bl_idname = "object.weight_contour_mask"
    bl_label = "Contour Mask"
    bl_description = ("")
    bl_options = {'REGISTER', 'UNDO'}

    use_modifiers : BoolProperty(
        name="Use Modifiers", default=True,
        description="Apply all the modifiers")
    iso : FloatProperty(
        name="Iso Value", default=0.5, soft_min=0, soft_max=1,
        description="Threshold value")
    bool_solidify : BoolProperty(
        name="Solidify", default=True, description="Add Solidify Modifier")
    normalize_weight : BoolProperty(
        name="Normalize Weight", default=True,
        description="Normalize weight of remaining vertices")

    @classmethod
    def poll(cls, context):
        return len(context.object.vertex_groups) > 0

    def execute(self, context):
        start_time = timeit.default_timer()
        try:
            check = context.object.vertex_groups[0]
        except:
            self.report({'ERROR'}, "The object doesn't have Vertex Groups")
            return {'CANCELLED'}

        ob0 = bpy.context.object

        iso_val = self.iso
        group_id = ob0.vertex_groups.active_index
        vertex_group_name = ob0.vertex_groups[group_id].name

        #bpy.ops.object.mode_set(mode='EDIT')
        #bpy.ops.mesh.select_all(action='SELECT')
        #bpy.ops.object.mode_set(mode='OBJECT')
        if self.use_modifiers:
            me0 = simple_to_mesh(ob0)#ob0.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy()
        else:
            me0 = ob0.data.copy()

        # generate new bmesh
        bm = bmesh.new()
        bm.from_mesh(me0)

        # store weight values
        weight = []
        ob = bpy.data.objects.new("temp", me0)
        for g in ob0.vertex_groups:
            ob.vertex_groups.new(name=g.name)
        weight = get_weight_numpy(ob.vertex_groups[vertex_group_name], len(me0.vertices))

        me0, bm, weight = contour_bmesh(me0, bm, weight, iso_val)

        # Mask geometry
        mask = weight >= iso_val
        weight = weight[mask]
        mask = np.logical_not(mask)
        delete_verts = np.array(bm.verts)[mask]
        #for v in delete_verts: bm.verts.remove(v)

        # Create mesh and object
        name = ob0.name + '_ContourMask_{:.3f}'.format(iso_val)
        me = bpy.data.meshes.new(name)
        bm.to_mesh(me)
        bm.free()
        ob = bpy.data.objects.new(name, me)

        # Link object to scene and make active
        scn = context.scene
        context.collection.objects.link(ob)
        context.view_layer.objects.active = ob
        ob.select_set(True)
        ob0.select_set(False)

        # generate new vertex group
        for g in ob0.vertex_groups:
            ob.vertex_groups.new(name=g.name)

        if iso_val != 1: mult = 1/(1-iso_val)
        else: mult = 1
        for id in range(len(weight)):
            if self.normalize_weight: w = (weight[id]-iso_val)*mult
            else: w = weight[id]
            ob.vertex_groups[vertex_group_name].add([id], w, 'REPLACE')
        ob.vertex_groups.active_index = group_id

        # align new object
        ob.matrix_world = ob0.matrix_world

        # Add Solidify
        if self.bool_solidify and True:
            ob.modifiers.new(type='SOLIDIFY', name='Solidify')
            ob.modifiers['Solidify'].thickness = 0.05
            ob.modifiers['Solidify'].offset = 0
            ob.modifiers['Solidify'].vertex_group = vertex_group_name

        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
        print("Contour Mask time: " + str(timeit.default_timer() - start_time) + " sec")

        bpy.data.meshes.remove(me0)

        return {'FINISHED'}


class weight_contour_curves(Operator):
    bl_idname = "object.weight_contour_curves"
    bl_label = "Contour Curves"
    bl_description = ("")
    bl_options = {'REGISTER', 'UNDO'}

    use_modifiers : BoolProperty(
        name="Use Modifiers", default=True,
        description="Apply all the modifiers")

    min_iso : FloatProperty(
        name="Min Value", default=0., soft_min=0, soft_max=1,
        description="Minimum weight value")
    max_iso : FloatProperty(
        name="Max Value", default=1, soft_min=0, soft_max=1,
        description="Maximum weight value")
    n_curves : IntProperty(
        name="Curves", default=3, soft_min=1, soft_max=10,
        description="Number of Contour Curves")

    min_rad : FloatProperty(
        name="Min Radius", default=1, soft_min=0, soft_max=1,
        description="Change radius according to Iso Value")
    max_rad : FloatProperty(
        name="Max Radius", default=1, soft_min=0, soft_max=1,
        description="Change radius according to Iso Value")

    @classmethod
    def poll(cls, context):
        ob = context.object
        return len(ob.vertex_groups) > 0 or ob.type == 'CURVE'

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

    def execute(self, context):
        start_time = timeit.default_timer()
        try:
            check = context.object.vertex_groups[0]
        except:
            self.report({'ERROR'}, "The object doesn't have Vertex Groups")
            return {'CANCELLED'}
        ob0 = context.object

        group_id = ob0.vertex_groups.active_index
        vertex_group_name = ob0.vertex_groups[group_id].name

        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.select_all(action='SELECT')
        bpy.ops.object.mode_set(mode='OBJECT')
        if self.use_modifiers:
            me0 = simple_to_mesh(ob0) #ob0.to_mesh(preserve_all_data_layers=True, depsgraph=bpy.context.evaluated_depsgraph_get()).copy()
        else:
            me0 = ob0.data.copy()

        # generate new bmesh
        bm = bmesh.new()
        bm.from_mesh(me0)
        bm.verts.ensure_lookup_table()
        bm.edges.ensure_lookup_table()
        bm.faces.ensure_lookup_table()

        # store weight values
        weight = []
        ob = bpy.data.objects.new("temp", me0)
        for g in ob0.vertex_groups:
            ob.vertex_groups.new(name=g.name)
        weight = get_weight_numpy(ob.vertex_groups[vertex_group_name], len(bm.verts))

        #filtered_edges = bm.edges
        total_verts = np.zeros((0,3))
        total_segments = []
        radius = []

        # start iterate contours levels
        vertices = get_vertices_numpy(me0)
        filtered_edges = get_edges_id_numpy(me0)

        faces_weight = [np.array([weight[v] for v in p.vertices]) for p in me0.polygons]
        fw_min = np.array([np.min(fw) for fw in faces_weight])
        fw_max = np.array([np.max(fw) for fw in faces_weight])

        bm_faces = np.array(bm.faces)

        ### Spiral
        normals = np.array([v.normal for v in me0.vertices])

        for c in range(self.n_curves):
            min_iso = min(self.min_iso, self.max_iso)
            max_iso = max(self.min_iso, self.max_iso)
            try:
                delta_iso = (max_iso-min_iso)/(self.n_curves-1)
                iso_val = c*delta_iso + min_iso
                if iso_val < 0: iso_val = (min_iso + max_iso)/2
            except:
                iso_val = (min_iso + max_iso)/2

            # remove passed faces
            bool_mask = iso_val < fw_max
            bm_faces = bm_faces[bool_mask]
            fw_min = fw_min[bool_mask]
            fw_max = fw_max[bool_mask]

            # mask faces
            bool_mask = fw_min < iso_val
            faces_mask = bm_faces[bool_mask]

            n_verts = len(bm.verts)
            count = len(total_verts)

            # vertices indexes
            id0 = filtered_edges[:,0]
            id1 = filtered_edges[:,1]
            # vertices weight
            w0 = weight[id0]
            w1 = weight[id1]
            # weight condition
            bool_w0 = w0 < iso_val
            bool_w1 = w1 < iso_val

            # mask all edges that have one weight value below the iso value
            mask_new_verts = np.logical_xor(bool_w0, bool_w1)

            id0 = id0[mask_new_verts]
            id1 = id1[mask_new_verts]
            # filter arrays
            v0 = vertices[id0]
            v1 = vertices[id1]
            w0 = w0[mask_new_verts]
            w1 = w1[mask_new_verts]
            div = (w1-w0)
            if div == 0: div = 0.000001

            param = np.expand_dims((iso_val-w0)/div,axis=1)
            verts = v0 + (v1-v0)*param

            # indexes of edges with new vertices
            edges_index = filtered_edges[mask_new_verts][:,2]
            edges_id = {}
            for i, id in enumerate(edges_index): edges_id[id] = i+len(total_verts)

            # remove all edges completely below the iso value
            mask_edges = np.logical_not(np.logical_and(bool_w0, bool_w1))
            filtered_edges = filtered_edges[mask_edges]
            if len(verts) == 0: continue

            # finding segments
            segments = []
            for f in faces_mask:
                seg = []
                for e in f.edges:
                    try:
                        seg.append(edges_id[e.index])
                        if len(seg) == 2:
                            segments.append(seg)
                            seg = []
                    except: pass


            #curves_points_indexes = find_curves(segments)
            total_segments = total_segments + segments
            total_verts = np.concatenate((total_verts,verts))

            if self.min_rad != self.max_rad:
                try:
                    iso_rad = c*(self.max_rad-self.min_rad)/(self.n_curves-1)+self.min_rad
                    if iso_rad < 0: iso_rad = (self.min_rad + self.max_rad)/2
                except:
                    iso_rad = (self.min_rad + self.max_rad)/2
                radius = radius + [iso_rad]*len(verts)
        print("Contour Curves, computing time: " + str(timeit.default_timer() - start_time) + " sec")
        bm.free()
        bm = bmesh.new()
        # adding new vertices  _local for fast access
        _new_vert = bm.verts.new
        for v in total_verts: _new_vert(v)
        bm.verts.ensure_lookup_table()

        # adding new edges
        _new_edge = bm.edges.new
        for s in total_segments:
            try:
                pts = [bm.verts[i] for i in s]
                _new_edge(pts)
            except: pass


        try:
            name = ob0.name + '_ContourCurves'
            me = bpy.data.meshes.new(name)
            bm.to_mesh(me)
            bm.free()
            ob = bpy.data.objects.new(name, me)
            # Link object to scene and make active
            scn = context.scene
            context.collection.objects.link(ob)
            context.view_layer.objects.active = ob
            ob.select_set(True)
            ob0.select_set(False)

            print("Contour Curves, bmesh time: " + str(timeit.default_timer() - start_time) + " sec")
            bpy.ops.object.convert(target='CURVE')
            ob = context.object
            if not (self.min_rad == 0 and self.max_rad == 0):
                if self.min_rad != self.max_rad:
                    count = 0
                    for s in ob.data.splines:
                        for p in s.points:
                            p.radius = radius[count]
                            count += 1
                else:
                    for s in ob.data.splines:
                        for p in s.points:
                            p.radius = self.min_rad
            ob.data.bevel_depth = 0.01
            ob.data.fill_mode = 'FULL'
            ob.data.bevel_resolution = 3
        except:
            self.report({'ERROR'}, "There are no values in the chosen range")
            return {'CANCELLED'}

        # align new object
        ob.matrix_world = ob0.matrix_world
        print("Contour Curves time: " + str(timeit.default_timer() - start_time) + " sec")

        bpy.data.meshes.remove(me0)
        bpy.data.meshes.remove(me)

        return {'FINISHED'}

class tissue_weight_contour_curves_pattern(Operator):
    bl_idname = "object.tissue_weight_contour_curves_pattern"
    bl_label = "Contour Curves"
    bl_description = ("")
    bl_options = {'REGISTER', 'UNDO'}

    use_modifiers : BoolProperty(
        name="Use Modifiers", default=True,
        description="Apply all the modifiers")

    auto_bevel : BoolProperty(
        name="Automatic Bevel", default=False,
        description="Bevel depends on weight density")

    min_iso : FloatProperty(
        name="Min Value", default=0., soft_min=0, soft_max=1,
        description="Minimum weight value")
    max_iso : FloatProperty(
        name="Max Value", default=1, soft_min=0, soft_max=1,
        description="Maximum weight value")
    n_curves : IntProperty(
        name="Curves", default=10, soft_min=1, soft_max=100,
        description="Number of Contour Curves")
    min_rad = 1
    max_rad = 1

    in_displace : FloatProperty(
        name="Displace A", default=0, soft_min=-10, soft_max=10,
        description="Pattern displace strength")
    out_displace : FloatProperty(
        name="Displace B", default=2, soft_min=-10, soft_max=10,
        description="Pattern displace strength")

    in_steps : IntProperty(
        name="Steps A", default=1, min=0, soft_max=10,
        description="Number of layers to move inwards")
    out_steps : IntProperty(
        name="Steps B", default=1, min=0, soft_max=10,
        description="Number of layers to move outwards")
    limit_z : BoolProperty(
        name="Limit Z", default=False,
        description="Limit Pattern in Z")

    merge : BoolProperty(
        name="Merge Vertices", default=True,
        description="Merge points")
    merge_thres : FloatProperty(
        name="Merge Threshold", default=0.01, min=0, soft_max=1,
        description="Minimum Curve Radius")

    bevel_depth : FloatProperty(
        name="Bevel Depth", default=0, min=0, soft_max=1,
        description="")
    min_bevel_depth : FloatProperty(
        name="Min Bevel Depth", default=0.1, min=0, soft_max=1,
        description="")
    max_bevel_depth : FloatProperty(
        name="Max Bevel Depth", default=1, min=0, soft_max=1,
        description="")
    remove_open_curves : BoolProperty(
        name="Remove Open Curves", default=False,
        description="Remove Open Curves")

    vertex_group_pattern : StringProperty(
        name="Displace", default='',
        description="Vertex Group used for pattern displace")

    vertex_group_bevel : StringProperty(
        name="Bevel", default='',
        description="Variable Bevel depth")

    object_name : StringProperty(
        name="Active Object", default='',
        description="")

    try: vg_name = bpy.context.object.vertex_groups.active.name
    except: vg_name = ''

    vertex_group_contour : StringProperty(
        name="Contour", default=vg_name,
        description="Vertex Group used for contouring")
    clean_distance : FloatProperty(
        name="Clean Distance", default=0, min=0, soft_max=10,
        description="Remove short segments")


    spiralized: BoolProperty(
        name='Spiralized', default=False,
        description='Create a Spiral Contour. Works better with dense meshes'
    )
    spiral_axis: FloatVectorProperty(
        name="Spiral Axis", default=(0,0,1),
        description="Axis of the Spiral (in local coordinates)"
    )
    spiral_rotation : FloatProperty(
        name="Spiral Rotation", default=0, min=0, max=2*pi,
        description=""
    )

    @classmethod
    def poll(cls, context):
        ob = context.object
        return ob and len(ob.vertex_groups) > 0 or ob.type == 'CURVE'

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

    def draw(self, context):
        if not context.object.type == 'CURVE':
            self.object_name = context.object.name
        ob = bpy.data.objects[self.object_name]
        if self.vertex_group_contour not in [vg.name for vg in ob.vertex_groups]:
            self.vertex_group_contour = ob.vertex_groups.active.name
        layout = self.layout
        col = layout.column(align=True)
        col.prop(self, "use_modifiers")
        col.label(text="Contour Curves:")
        col.prop_search(self, 'vertex_group_contour', ob, "vertex_groups", text='')
        row = col.row(align=True)
        row.prop(self,'min_iso')
        row.prop(self,'max_iso')
        col.prop(self,'n_curves')
        col.separator()
        col.label(text='Curves Bevel:')
        col.prop(self,'auto_bevel')
        if not self.auto_bevel:
            col.prop_search(self, 'vertex_group_bevel', ob, "vertex_groups", text='')
        if self.vertex_group_bevel != '' or self.auto_bevel:
            row = col.row(align=True)
            row.prop(self,'min_bevel_depth')
            row.prop(self,'max_bevel_depth')
        else:
            col.prop(self,'bevel_depth')
        col.separator()

        col.label(text="Displace Pattern:")
        col.prop_search(self, 'vertex_group_pattern', ob, "vertex_groups", text='')
        if self.vertex_group_pattern != '':
            row = col.row(align=True)
            row.prop(self,'in_steps')
            row.prop(self,'out_steps')
            row = col.row(align=True)
            row.prop(self,'in_displace')
            row.prop(self,'out_displace')
            col.prop(self,'limit_z')
        col.separator()
        row=col.row(align=True)
        row.prop(self,'spiralized')
        row.label(icon='MOD_SCREW')
        if self.spiralized:
            #row=col.row(align=True)
            #row.prop(self,'spiral_axis')
            #col.separator()
            col.prop(self,'spiral_rotation')
        col.separator()

        col.label(text='Clean Curves:')
        col.prop(self,'clean_distance')
        col.prop(self,'remove_open_curves')

    def execute(self, context):
        n_curves = self.n_curves
        start_time = timeit.default_timer()
        try:
            check = context.object.vertex_groups[0]
        except:
            self.report({'ERROR'}, "The object doesn't have Vertex Groups")
            return {'CANCELLED'}
        ob0 = bpy.data.objects[self.object_name]

        dg = context.evaluated_depsgraph_get()
        ob = ob0.evaluated_get(dg)
        me0 = ob.data

        # generate new bmesh
        bm = bmesh.new()
        bm.from_mesh(me0)
        n_verts = len(bm.verts)

        # store weight values
        try:
            weight = get_weight_numpy(ob.vertex_groups[self.vertex_group_contour], len(me0.vertices))
        except:
            bm.free()
            self.report({'ERROR'}, "Please select a Vertex Group for contouring")
            return {'CANCELLED'}

        try:
            pattern_weight = get_weight_numpy(ob.vertex_groups[self.vertex_group_pattern], len(me0.vertices))
        except:
            #self.report({'WARNING'}, "There is no Vertex Group assigned to the pattern displace")
            pattern_weight = np.zeros(len(me0.vertices))

        variable_bevel = False
        try:
            bevel_weight = get_weight_numpy(ob.vertex_groups[self.vertex_group_bevel], len(me0.vertices))
            variable_bevel = True
        except:
            bevel_weight = np.ones(len(me0.vertices))

        if self.auto_bevel:
            # calc weight density
            bevel_weight = np.ones(len(me0.vertices))*10000
            bevel_weight = np.zeros(len(me0.vertices))
            edges_length = np.array([e.calc_length() for e in bm.edges])
            edges_dw = np.array([max(abs(weight[e.verts[0].index]-weight[e.verts[1].index]),0.000001) for e in bm.edges])
            dens = edges_length/edges_dw
            n_records = np.zeros(len(me0.vertices))
            for i, e in enumerate(bm.edges):
                for v in e.verts:
                    id = v.index
                    #bevel_weight[id] = min(bevel_weight[id], dens[i])
                    bevel_weight[id] += dens[i]
                    n_records[id] += 1
            bevel_weight = bevel_weight/n_records
            bevel_weight = (bevel_weight - min(bevel_weight))/(max(bevel_weight) - min(bevel_weight))
            #bevel_weight = 1-bevel_weight
            variable_bevel = True

        #filtered_edges = bm.edges
        total_verts = np.zeros((0,3))
        total_radii = np.zeros((0,1))
        total_segments = []# np.array([])
        radius = []

        # start iterate contours levels
        vertices, normals = get_vertices_and_normals_numpy(me0)
        filtered_edges = get_edges_id_numpy(me0)


        min_iso = min(self.min_iso, self.max_iso)
        max_iso = max(self.min_iso, self.max_iso)

        # Spiral
        if self.spiralized:
            nx = normals[:,0]
            ny = normals[:,1]
            ang = self.spiral_rotation + weight*pi*n_curves+arctan2(nx,ny)
            weight = sin(ang)/2+0.5
            n_curves = 1

        if n_curves > 1:
            delta_iso = (max_iso-min_iso)/(n_curves-1)

        else:
            delta_iso = None

        faces_weight = [np.array([weight[v] for v in p.vertices]) for p in me0.polygons]
        fw_min = np.array([np.min(fw) for fw in faces_weight])
        fw_max = np.array([np.max(fw) for fw in faces_weight])

        bm_faces = np.array(bm.faces)

        #print("Contour Curves, data loaded: " + str(timeit.default_timer() - start_time) + " sec")
        step_time = timeit.default_timer()
        for c in range(n_curves):
            if delta_iso:
                iso_val = c*delta_iso + min_iso
                if iso_val < 0: iso_val = (min_iso + max_iso)/2
            else:
                iso_val = (min_iso + max_iso)/2

            #if c == 0 and self.auto_bevel:


            # remove passed faces
            bool_mask = iso_val < fw_max
            bm_faces = bm_faces[bool_mask]
            fw_min = fw_min[bool_mask]
            fw_max = fw_max[bool_mask]

            # mask faces
            bool_mask = fw_min < iso_val
            faces_mask = bm_faces[bool_mask]

            count = len(total_verts)

            new_filtered_edges, edges_index, verts, bevel = contour_edges_pattern(self, c, len(total_verts), iso_val, vertices, normals, filtered_edges, weight, pattern_weight, bevel_weight)

            if len(edges_index) > 0:
                if self.auto_bevel and False:
                    bevel = 1-dens[edges_index]
                    bevel = bevel[:,np.newaxis]
                if self.max_bevel_depth != self.min_bevel_depth:
                    min_radius = self.min_bevel_depth / max(0.0001,self.max_bevel_depth)
                    radii = min_radius + bevel*(1 - min_radius)
                else:
                    radii = bevel
            else:
                continue

            if verts[0,0] == None: continue
            else: filtered_edges = new_filtered_edges
            edges_id = {}
            for i, id in enumerate(edges_index): edges_id[id] = i + count

            if len(verts) == 0: continue

            # finding segments
            segments = []
            for f in faces_mask:
                seg = []
                for e in f.edges:
                    try:
                        #seg.append(new_ids[np.where(edges_index == e.index)[0][0]])
                        seg.append(edges_id[e.index])
                        if len(seg) == 2:
                            segments.append(seg)
                            seg = []
                    except: pass

            total_segments = total_segments + segments
            total_verts = np.concatenate((total_verts, verts))
            total_radii = np.concatenate((total_radii, radii))

            if self.min_rad != self.max_rad:
                try:
                    iso_rad = c*(self.max_rad-self.min_rad)/(self.n_curves-1)+self.min_rad
                    if iso_rad < 0: iso_rad = (self.min_rad + self.max_rad)/2
                except:
                    iso_rad = (self.min_rad + self.max_rad)/2
                radius = radius + [iso_rad]*len(verts)
        #print("Contour Curves, points computing: " + str(timeit.default_timer() - step_time) + " sec")
        step_time = timeit.default_timer()

        if len(total_segments) > 0:
            step_time = timeit.default_timer()
            ordered_points = find_curves(total_segments, len(total_verts))

            #print("Contour Curves, point ordered in: " + str(timeit.default_timer() - step_time) + " sec")
            step_time = timeit.default_timer()
            crv = curve_from_pydata(total_verts, total_radii, ordered_points, ob0.name + '_ContourCurves', self.remove_open_curves, merge_distance=self.clean_distance)
            context.view_layer.objects.active = crv
            if variable_bevel: crv.data.bevel_depth = self.max_bevel_depth
            else: crv.data.bevel_depth = self.bevel_depth

            crv.select_set(True)
            ob0.select_set(False)
            crv.matrix_world = ob0.matrix_world
            #print("Contour Curves, curves created in: " + str(timeit.default_timer() - step_time) + " sec")
        else:
            bm.free()
            self.report({'ERROR'}, "There are no values in the chosen range")
            return {'CANCELLED'}
        bm.free()
        print("Contour Curves, total time: " + str(timeit.default_timer() - start_time) + " sec")
        return {'FINISHED'}

class vertex_colors_to_vertex_groups(Operator):
    bl_idname = "object.vertex_colors_to_vertex_groups"
    bl_label = "Vertex Color"
    bl_options = {'REGISTER', 'UNDO'}
    bl_description = ("Convert the active Vertex Color into a Vertex Group")

    red : BoolProperty(
        name="red channel", default=False, description="convert red channel")
    green : BoolProperty(
        name="green channel", default=False,
        description="convert green channel")
    blue : BoolProperty(
        name="blue channel", default=False, description="convert blue channel")
    value : BoolProperty(
        name="value channel", default=True, description="convert value channel")
    invert : BoolProperty(
         name="invert", default=False, description="invert all color channels")

    @classmethod
    def poll(cls, context):
        try:
            return len(context.object.data.vertex_colors) > 0
        except: return False

    def execute(self, context):
        obj = context.active_object
        id = len(obj.vertex_groups)
        id_red = id
        id_green = id
        id_blue = id
        id_value = id

        boolCol = len(obj.data.vertex_colors)
        if(boolCol): col_name = obj.data.vertex_colors.active.name
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.select_all(action='SELECT')

        if(self.red and boolCol):
            bpy.ops.object.vertex_group_add()
            bpy.ops.object.vertex_group_assign()
            id_red = id
            obj.vertex_groups[id_red].name = col_name + '_red'
            id+=1
        if(self.green and boolCol):
            bpy.ops.object.vertex_group_add()
            bpy.ops.object.vertex_group_assign()
            id_green = id
            obj.vertex_groups[id_green].name = col_name + '_green'
            id+=1
        if(self.blue and boolCol):
            bpy.ops.object.vertex_group_add()
            bpy.ops.object.vertex_group_assign()
            id_blue = id
            obj.vertex_groups[id_blue].name = col_name + '_blue'
            id+=1
        if(self.value and boolCol):
            bpy.ops.object.vertex_group_add()
            bpy.ops.object.vertex_group_assign()
            id_value = id
            obj.vertex_groups[id_value].name = col_name + '_value'
            id+=1

        mult = 1
        if(self.invert): mult = -1
        bpy.ops.object.mode_set(mode='OBJECT')
        sub_red = 1 + self.value + self.blue + self.green
        sub_green = 1 + self.value + self.blue
        sub_blue = 1 + self.value
        sub_value = 1

        id = len(obj.vertex_groups)
        if(id_red <= id and id_green <= id and id_blue <= id and id_value <= \
                id and boolCol):
            v_colors = obj.data.vertex_colors.active.data
            i = 0
            for f in obj.data.polygons:
                for v in f.vertices:
                    gr = obj.data.vertices[v].groups
                    if(self.red): gr[min(len(gr)-sub_red, id_red)].weight = \
                        self.invert + mult * v_colors[i].color[0]
                    if(self.green): gr[min(len(gr)-sub_green, id_green)].weight\
                        = self.invert + mult * v_colors[i].color[1]
                    if(self.blue): gr[min(len(gr)-sub_blue, id_blue)].weight = \
                        self.invert + mult * v_colors[i].color[2]
                    if(self.value):
                        r = v_colors[i].color[0]
                        g = v_colors[i].color[1]
                        b = v_colors[i].color[2]
                        gr[min(len(gr)-sub_value, id_value)].weight\
                        = self.invert + mult * (0.2126*r + 0.7152*g + 0.0722*b)
                    i+=1
            bpy.ops.paint.weight_paint_toggle()
        return {'FINISHED'}

class vertex_group_to_vertex_colors(Operator):
    bl_idname = "object.vertex_group_to_vertex_colors"
    bl_label = "Vertex Group"
    bl_options = {'REGISTER', 'UNDO'}
    bl_description = ("Convert the active Vertex Group into a Vertex Color")

    channel : EnumProperty(
        items=[('BLUE', 'Blue Channel', 'Convert to Blue Channel'),
               ('GREEN', 'Green Channel', 'Convert to Green Channel'),
               ('RED', 'Red Channel', 'Convert to Red Channel'),
               ('VALUE', 'Value Channel', 'Convert to Grayscale'),
               ('FALSE_COLORS', 'False Colors', 'Convert to False Colors')],
        name="Convert to", description="Choose how to convert vertex group",
        default="VALUE", options={'LIBRARY_EDITABLE'})

    invert : BoolProperty(
        name="invert", default=False, description="invert color channel")

    @classmethod
    def poll(cls, context):
        return len(context.object.vertex_groups) > 0

    def execute(self, context):
        obj = context.active_object
        me = obj.data
        group_id = obj.vertex_groups.active_index
        if (group_id == -1):
            return {'FINISHED'}

        bpy.ops.object.mode_set(mode='OBJECT')
        group_name = obj.vertex_groups[group_id].name
        me.vertex_colors.new()
        colors_id = obj.data.vertex_colors.active_index

        colors_name = group_name
        if(self.channel == 'FALSE_COLORS'): colors_name += "_false_colors"
        elif(self.channel == 'VALUE'):  colors_name += "_value"
        elif(self.channel == 'RED'):  colors_name += "_red"
        elif(self.channel == 'GREEN'):  colors_name += "_green"
        elif(self.channel == 'BLUE'):  colors_name += "_blue"
        context.object.data.vertex_colors[colors_id].name = colors_name

        v_colors = obj.data.vertex_colors.active.data

        bm = bmesh.new()
        bm.from_mesh(me)
        dvert_lay = bm.verts.layers.deform.active
        weight = bmesh_get_weight_numpy(group_id,dvert_lay,bm.verts)
        if self.invert: weight = 1-weight
        loops_size = get_attribute_numpy(me.polygons, attribute='loop_total', mult=1)
        n_colors = np.sum(loops_size)
        verts = np.ones(n_colors)
        me.polygons.foreach_get('vertices',verts)
        splitted_weight = weight[verts.astype(int)][:,None]
        r = np.zeros(splitted_weight.shape)
        g = np.zeros(splitted_weight.shape)
        b = np.zeros(splitted_weight.shape)
        a = np.ones(splitted_weight.shape)
        if(self.channel == 'FALSE_COLORS'):
            mult = 0.6+0.4*splitted_weight
            mask = splitted_weight < 0.25
            g[mask] = splitted_weight[mask]*4
            b[mask] = np.ones(splitted_weight.shape)[mask]

            mask = np.where(np.logical_and(splitted_weight>=0.25, splitted_weight<0.5))
            g[mask] = np.ones(splitted_weight.shape)[mask]
            b[mask] = (1-(splitted_weight[mask]-0.25)*4)

            mask = np.where(np.logical_and(splitted_weight>=0.5, splitted_weight<0.75))
            r[mask] = (splitted_weight[mask]-0.5)*4
            g[mask] = np.ones(splitted_weight.shape)[mask]

            mask = 0.75 <= splitted_weight
            r[mask] = np.ones(splitted_weight.shape)[mask]
            g[mask] = (1-(splitted_weight[mask]-0.75)*4)
        elif(self.channel == 'VALUE'):
            r = splitted_weight
            g = splitted_weight
            b = splitted_weight
        elif(self.channel == 'RED'):
            r = splitted_weight
        elif(self.channel == 'GREEN'):
            g = splitted_weight
        elif(self.channel == 'BLUE'):
            b = splitted_weight

        colors = np.concatenate((r,g,b,a),axis=1).flatten()
        v_colors.foreach_set('color',colors)

        bpy.ops.paint.vertex_paint_toggle()
        context.object.data.vertex_colors[colors_id].active_render = True
        return {'FINISHED'}

class vertex_group_to_uv(Operator):
    bl_idname = "object.vertex_group_to_uv"
    bl_label = "Vertex Group"
    bl_options = {'REGISTER', 'UNDO'}
    bl_description = ("Combine two Vertex Groups as UV Map Layer")

    vertex_group_u : StringProperty(
        name="U", default='',
        description="Vertex Group used for the U coordinate")
    vertex_group_v : StringProperty(
        name="V", default='',
        description="Vertex Group used for the V coordinate")
    normalize_weight : BoolProperty(
        name="Normalize Weight", default=True,
        description="Normalize weight values")
    invert_u : BoolProperty(
        name="Invert U", default=False, description="Invert U")
    invert_v : BoolProperty(
        name="Invert V", default=False, description="Invert V")

    @classmethod
    def poll(cls, context):
        return len(context.object.vertex_groups) > 0

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

    def draw(self, context):
        ob = context.object
        layout = self.layout
        col = layout.column(align=True)
        row = col.row(align=True)
        row.prop_search(self, 'vertex_group_u', ob, "vertex_groups", text='')
        row.separator()
        row.prop_search(self, 'vertex_group_v', ob, "vertex_groups", text='')
        row = col.row(align=True)
        row.prop(self, "invert_u")
        row.separator()
        row.prop(self, "invert_v")
        row = col.row(align=True)
        row.prop(self, "normalize_weight")

    def execute(self, context):
        ob = context.active_object
        me = ob.data
        n_verts = len(me.vertices)
        vg_keys = ob.vertex_groups.keys()
        bool_u = self.vertex_group_u in vg_keys
        bool_v = self.vertex_group_v in vg_keys
        if bool_u or bool_v:
            bm = bmesh.new()
            bm.from_mesh(me)
            dvert_lay = bm.verts.layers.deform.active
            if bool_u:
                u_index = ob.vertex_groups[self.vertex_group_u].index
                u = bmesh_get_weight_numpy(u_index, dvert_lay, bm.verts)
                if self.invert_u:
                    u = 1-u
                if self.normalize_weight:
                    u = np.interp(u, (u.min(), u.max()), (0, 1))
            else:
                u = np.zeros(n_verts)
            if bool_v:
                v_index = ob.vertex_groups[self.vertex_group_v].index
                v = bmesh_get_weight_numpy(v_index, dvert_lay, bm.verts)
                if self.invert_v:
                    v = 1-v
                if self.normalize_weight:
                    v = np.interp(v, (v.min(), v.max()), (0, 1))
            else:
                v = np.zeros(n_verts)
        else:
            u = v = np.zeros(n_verts)

        uv_layer = me.uv_layers.new(name='Weight_to_UV')
        loops_size = get_attribute_numpy(me.polygons, attribute='loop_total', mult=1)
        n_data = np.sum(loops_size)
        v_id = np.ones(n_data)
        me.polygons.foreach_get('vertices',v_id)
        v_id = v_id.astype(int)
        split_u = u[v_id,None]
        split_v = v[v_id,None]
        uv = np.concatenate((split_u,split_v),axis=1).flatten()
        uv_layer.data.foreach_set('uv',uv)
        me.uv_layers.update()
        return {'FINISHED'}

class curvature_to_vertex_groups(Operator):
    bl_idname = "object.curvature_to_vertex_groups"
    bl_label = "Curvature"
    bl_options = {'REGISTER', 'UNDO'}
    bl_description = ("Generate a Vertex Group based on the curvature of the"
                      "mesh. Is based on Dirty Vertex Color")

    invert : BoolProperty(
        name="invert", default=False, description="invert values")

    blur_strength : FloatProperty(
      name="Blur Strength", default=1, min=0.001,
      max=1, description="Blur strength per iteration")

    blur_iterations : IntProperty(
      name="Blur Iterations", default=1, min=0,
      max=40, description="Number of times to blur the values")

    min_angle : FloatProperty(
      name="Min Angle", default=0, min=0,
      max=pi/2, subtype='ANGLE', description="Minimum angle")

    max_angle : FloatProperty(
      name="Max Angle", default=pi, min=pi/2,
      max=pi, subtype='ANGLE', description="Maximum angle")

    invert : BoolProperty(
        name="Invert", default=False,
        description="Invert the curvature map")

    def execute(self, context):
        bpy.ops.object.mode_set(mode='OBJECT')
        vertex_colors = context.active_object.data.vertex_colors
        vertex_colors.new()
        vertex_colors[-1].active = True
        vertex_colors[-1].active_render = True
        vertex_colors[-1].name = "Curvature"
        for c in vertex_colors[-1].data: c.color = (1,1,1,1)
        bpy.ops.object.mode_set(mode='VERTEX_PAINT')
        bpy.ops.paint.vertex_color_dirt(
            blur_strength=self.blur_strength,
            blur_iterations=self.blur_iterations, clean_angle=self.max_angle,
            dirt_angle=self.min_angle)
        bpy.ops.object.vertex_colors_to_vertex_groups(invert=self.invert)
        vertex_colors.remove(vertex_colors.active)
        return {'FINISHED'}

class face_area_to_vertex_groups(Operator):
    bl_idname = "object.face_area_to_vertex_groups"
    bl_label = "Area"
    bl_options = {'REGISTER', 'UNDO'}
    bl_description = ("Generate a Vertex Group based on the area of individual"
                      "faces")

    invert : BoolProperty(
        name="invert", default=False, description="invert values")
    bounds : EnumProperty(
        items=(('MANUAL', "Manual Bounds", ""),
            ('AUTOMATIC', "Automatic Bounds", "")),
        default='AUTOMATIC', name="Bounds")

    min_area : FloatProperty(
        name="Min", default=0.01, soft_min=0, soft_max=1,
        description="Faces with 0 weight")

    max_area : FloatProperty(
        name="Max", default=0.1, soft_min=0, soft_max=1,
        description="Faces with 1 weight")

    def draw(self, context):
        layout = self.layout
        layout.label(text="Bounds")
        layout.prop(self, "bounds", text="")
        if self.bounds == 'MANUAL':
            layout.prop(self, "min_area")
            layout.prop(self, "max_area")

    def execute(self, context):
        try: ob = context.object
        except:
            self.report({'ERROR'}, "Please select an Object")
            return {'CANCELLED'}
        ob.vertex_groups.new(name="Faces Area")

        areas = [[] for v in ob.data.vertices]

        for p in ob.data.polygons:
            for v in p.vertices:
                areas[v].append(p.area)

        for i in range(len(areas)):
            areas[i] = mean(areas[i])
        if self.bounds == 'MANUAL':
            min_area = self.min_area
            max_area = self.max_area
        elif self.bounds == 'AUTOMATIC':
            min_area = min(areas)
            max_area = max(areas)
        elif self.bounds == 'COMPRESSION':
            min_area = 1
            max_area = min(areas)
        elif self.bounds == 'TENSION':
            min_area = 1
            max_area = max(areas)
        delta_area = max_area - min_area
        if delta_area == 0:
            delta_area = 0.0001
            if self.bounds == 'MANUAL':
                delta_area = 0.0001
            else:
                self.report({'ERROR'}, "The faces have the same areas")
                #return {'CANCELLED'}
        for i in range(len(areas)):
            weight = (areas[i] - min_area)/delta_area
            ob.vertex_groups[-1].add([i], weight, 'REPLACE')
        ob.vertex_groups.update()
        ob.data.update()
        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
        return {'FINISHED'}

class random_weight(Operator):
    bl_idname = "object.random_weight"
    bl_label = "Random"
    bl_options = {'REGISTER', 'UNDO'}
    bl_description = ("Generate a random Vertex Group")

    min_val : FloatProperty(
        name="Min", default=0, soft_min=0, soft_max=1,
        description="Minimum Value")

    max_val : FloatProperty(
        name="Max", default=1, soft_min=0, soft_max=1,
        description="Maximum Value")

    #def draw(self, context):
    #    layout = self.layout
    #    layout.prop(self, "min_area")
    #    layout.prop(self, "max_area")
    @classmethod
    def poll(cls, context):
        return len(context.object.vertex_groups) > 0

    def execute(self, context):
        try: ob = context.object
        except:
            self.report({'ERROR'}, "Please select an Object")
            return {'CANCELLED'}
        #ob.vertex_groups.new(name="Random")
        n_verts = len(ob.data.vertices)
        weight = np.random.uniform(low=self.min_val, high=self.max_val, size=(n_verts,))
        np.clip(weight, 0, 1, out=weight)

        group_id = ob.vertex_groups.active_index
        for i in range(n_verts):
            ob.vertex_groups[group_id].add([i], weight[i], 'REPLACE')
        ob.vertex_groups.update()
        ob.data.update()
        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
        return {'FINISHED'}


class harmonic_weight(Operator):
    bl_idname = "object.harmonic_weight"
    bl_label = "Harmonic"
    bl_options = {'REGISTER', 'UNDO'}
    bl_description = ("Create an harmonic variation of the active Vertex Group")

    freq : FloatProperty(
        name="Frequency", default=20, soft_min=0,
        soft_max=100, description="Wave frequency")

    amp : FloatProperty(
        name="Amplitude", default=1, soft_min=0,
        soft_max=10, description="Wave amplitude")

    midlevel : FloatProperty(
        name="Midlevel", default=0, min=-1,
        max=1, description="Midlevel")

    add : FloatProperty(
        name="Add", default=0, min=-1,
        max=1, description="Add to the Weight")

    mult : FloatProperty(
        name="Multiply", default=0, min=0,
        max=1, description="Multiply for he Weight")

    @classmethod
    def poll(cls, context):
        return len(context.object.vertex_groups) > 0

    def execute(self, context):
        ob = context.active_object
        if len(ob.vertex_groups) > 0:
            group_id = ob.vertex_groups.active_index
            ob.vertex_groups.new(name="Harmonic")
            for i in range(len(ob.data.vertices)):
                try: val = ob.vertex_groups[group_id].weight(i)
                except: val = 0
                weight = self.amp*(math.sin(val*self.freq) - self.midlevel)/2 + 0.5 + self.add*val*(1-(1-val)*self.mult)
                ob.vertex_groups[-1].add([i], weight, 'REPLACE')
            ob.data.update()
        else:
            self.report({'ERROR'}, "Active object doesn't have vertex groups")
            return {'CANCELLED'}
        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
        return {'FINISHED'}


class tissue_weight_distance(Operator):
    bl_idname = "object.tissue_weight_distance"
    bl_label = "Weight Distance"
    bl_options = {'REGISTER', 'UNDO'}
    bl_description = ("Create a weight map according to the distance from the "
                    "selected vertices along the mesh surface")

    mode : EnumProperty(
        items=(('GEOD', "Geodesic Distance", ""),
            ('EUCL', "Euclidean Distance", ""),
            ('TOPO', "Topology Distance", "")),
        default='GEOD', name="Distance Method")

    normalize : BoolProperty(
        name="Normalize", default=True,
        description="Automatically remap the distance values from 0 to 1")

    min_value : FloatProperty(
        name="Min", default=0, min=0,
        soft_max=100, description="Minimum Distance")

    max_value : FloatProperty(
        name="Max", default=10, min=0,
        soft_max=100, description="Max Distance")

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

    def fill_neighbors(self,verts,weight):
        neigh = {}
        for v0 in verts:
            for f in v0.link_faces:
                for v1 in f.verts:
                    if self.mode == 'GEOD':
                        dist = weight[v0.index] + (v0.co-v1.co).length
                    elif self.mode == 'TOPO':
                        dist = weight[v0.index] + 1.0
                    w1 = weight[v1.index]
                    if w1 == None or w1 > dist:
                        weight[v1.index] = dist
                        neigh[v1] = 0
        if len(neigh) == 0: return weight
        else: return self.fill_neighbors(neigh.keys(), weight)

    def execute(self, context):
        ob = context.object
        old_mode = ob.mode
        if old_mode != 'OBJECT':
            bpy.ops.object.mode_set(mode='OBJECT')

        me = ob.data

        # store weight values
        weight = [None]*len(me.vertices)

        if self.mode != 'EUCL':
            bm = bmesh.new()
            bm.from_mesh(me)
            bm.verts.ensure_lookup_table()
            bm.edges.ensure_lookup_table()
            bm.faces.ensure_lookup_table()
            selected = [v for v in bm.verts if v.select]
            if len(selected) == 0:
                bpy.ops.object.mode_set(mode=old_mode)
                message = "Please, select one or more vertices"
                self.report({'ERROR'}, message)
                return {'CANCELLED'}
            for v in selected: weight[v.index] = 0
            weight = self.fill_neighbors(selected, weight)
            bm.free()
        else:
            selected = [v for v in me.vertices if v.select]
            kd = KDTree(len(selected))
            for i, v in enumerate(selected):
                kd.insert(v.co, i)
            kd.balance()
            for i,v in enumerate(me.vertices):
                co, index, dist = kd.find(v.co)
                weight[i] = dist


        for i in range(len(weight)):
            if weight[i] == None: weight[i] = 0
        weight = np.array(weight)
        max_dist = np.max(weight)
        if self.normalize:
            if max_dist > 0:
                weight /= max_dist
        else:
            delta_value = self.max_value - self.min_value
            if delta_value == 0: delta_value = 0.0000001
            weight = (weight-self.min_value)/delta_value

        if self.mode == 'TOPO':
            vg = ob.vertex_groups.new(name='Distance: {:d}'.format(int(max_dist)))
        else:
            vg = ob.vertex_groups.new(name='Distance: {:.4f}'.format(max_dist))
        for i, w in enumerate(weight):
            vg.add([i], w, 'REPLACE')
        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')
        return {'FINISHED'}

class TISSUE_PT_color(Panel):
    bl_label = "Tissue Tools"
    bl_category = "Tissue"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    #bl_options = {'DEFAULT_CLOSED'}
    bl_context = "vertexpaint"

    def draw(self, context):
        layout = self.layout
        col = layout.column(align=True)
        col.operator("object.vertex_colors_to_vertex_groups",
            icon="GROUP_VERTEX", text="Convert to Weight")

class TISSUE_PT_weight(Panel):
    bl_label = "Tissue Tools"
    bl_category = "Tissue"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    #bl_options = {'DEFAULT_CLOSED'}
    bl_context = "weightpaint"

    def draw(self, context):
        layout = self.layout
        col = layout.column(align=True)
        #if context.object.type == 'MESH' and context.mode == 'OBJECT':
            #col.label(text="Transform:")
            #col.separator()
        #elif bpy.context.mode == 'PAINT_WEIGHT':
        col.label(text="Weight Generate:")
        #col.operator(
        #    "object.vertex_colors_to_vertex_groups", icon="GROUP_VCOL")
        col.operator("object.face_area_to_vertex_groups", icon="FACESEL")
        col.operator("object.curvature_to_vertex_groups", icon="SMOOTHCURVE")
        col.operator("object.tissue_weight_distance", icon="TRACKING")
        row = col.row(align=True)
        try: row.operator("object.weight_formula", icon="CON_TRANSFORM")
        except: row.operator("object.weight_formula")#, icon="CON_TRANSFORM")
        row.operator("object.update_weight_formula", icon="FILE_REFRESH", text='')#, icon="CON_TRANSFORM")
        #col.label(text="Weight Processing:")
        col.separator()

        # TO BE FIXED
        col.operator("object.weight_laplacian", icon="SMOOTHCURVE")

        col.label(text="Weight Edit:")
        col.operator("object.harmonic_weight", icon="IPO_ELASTIC")
        col.operator("object.random_weight", icon="RNDCURVE")
        col.separator()
        col.label(text="Deformation Analysis:")
        col.operator("object.edges_deformation", icon="DRIVER_DISTANCE")#FULLSCREEN_ENTER")
        col.operator("object.edges_bending", icon="DRIVER_ROTATIONAL_DIFFERENCE")#"MOD_SIMPLEDEFORM")
        col.separator()
        col.label(text="Weight Curves:")
        #col.operator("object.weight_contour_curves", icon="MOD_CURVE")
        col.operator("object.tissue_weight_streamlines", icon="ANIM")
        col.operator("object.tissue_weight_contour_curves_pattern", icon="FORCE_TURBULENCE")
        col.separator()
        col.operator("object.weight_contour_displace", icon="MOD_DISPLACE")
        col.operator("object.weight_contour_mask", icon="MOD_MASK")
        col.separator()
        col.label(text="Simulations:")
        #col.operator("object.reaction_diffusion", icon="MOD_OCEAN")
        col.operator("object.start_reaction_diffusion",
                    icon="EXPERIMENTAL",
                    text="Reaction-Diffusion")
        col.separator()
        col.label(text="Materials:")
        col.operator("object.random_materials", icon='COLOR')
        col.operator("object.weight_to_materials", icon='GROUP_VERTEX')
        col.separator()
        col.label(text="Weight Convert:")
        col.operator("object.vertex_group_to_vertex_colors", icon="GROUP_VCOL",
            text="Convert to Colors")
        col.operator("object.vertex_group_to_uv", icon="UV",
            text="Convert to UV")

        #col.prop(context.object, "reaction_diffusion_run", icon="PLAY", text="Run Simulation")
        ####col.prop(context.object, "reaction_diffusion_run")
        #col.separator()
        #col.label(text="Vertex Color from:")
        #col.operator("object.vertex_group_to_vertex_colors", icon="GROUP_VERTEX")




class start_reaction_diffusion(Operator):
    bl_idname = "object.start_reaction_diffusion"
    bl_label = "Start Reaction Diffusion"
    bl_description = ("Run a Reaction-Diffusion based on existing Vertex Groups: A and B")
    bl_options = {'REGISTER', 'UNDO'}

    run : BoolProperty(
        name="Run Reaction-Diffusion", default=True, description="Compute a new iteration on frame changes")

    time_steps : IntProperty(
        name="Steps", default=10, min=0, soft_max=50,
        description="Number of Steps")

    dt : FloatProperty(
        name="dt", default=1, min=0, soft_max=0.2,
        description="Time Step")

    diff_a : FloatProperty(
        name="Diff A", default=0.18, min=0, soft_max=2,
        description="Diffusion A")

    diff_b : FloatProperty(
        name="Diff B", default=0.09, min=0, soft_max=2,
        description="Diffusion B")

    f : FloatProperty(
        name="f", default=0.055, min=0, soft_min=0.01, soft_max=0.06, max=0.1, precision=4,
        description="Feed Rate")

    k : FloatProperty(
        name="k", default=0.062, min=0, soft_min=0.035, soft_max=0.065, max=0.1, precision=4,
        description="Kill Rate")

    @classmethod
    def poll(cls, context):
        return context.object.type == 'MESH' and context.mode != 'EDIT_MESH'

    def execute(self, context):
        reaction_diffusion_add_handler(self, context)
        set_animatable_fix_handler(self, context)

        ob = context.object

        ob.reaction_diffusion_settings.run = self.run
        ob.reaction_diffusion_settings.dt = self.dt
        ob.reaction_diffusion_settings.time_steps = self.time_steps
        ob.reaction_diffusion_settings.f = self.f
        ob.reaction_diffusion_settings.k = self.k
        ob.reaction_diffusion_settings.diff_a = self.diff_a
        ob.reaction_diffusion_settings.diff_b = self.diff_b


        # check vertex group A
        try:
            vg = ob.vertex_groups['A']
        except:
            ob.vertex_groups.new(name='A')
        # check vertex group B
        try:
            vg = ob.vertex_groups['B']
        except:
            ob.vertex_groups.new(name='B')

        for v in ob.data.vertices:
            ob.vertex_groups['A'].add([v.index], 1, 'REPLACE')
            ob.vertex_groups['B'].add([v.index], 0, 'REPLACE')

        ob.vertex_groups.update()
        ob.data.update()
        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')

        return {'FINISHED'}

class reset_reaction_diffusion_weight(Operator):
    bl_idname = "object.reset_reaction_diffusion_weight"
    bl_label = "Reset Reaction Diffusion Weight"
    bl_description = ("Set A and B weight to default values")
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return context.object.type == 'MESH' and context.mode != 'EDIT_MESH'

    def execute(self, context):
        reaction_diffusion_add_handler(self, context)
        set_animatable_fix_handler(self, context)

        ob = context.object

        # check vertex group A
        try:
            vg = ob.vertex_groups['A']
        except:
            ob.vertex_groups.new(name='A')
        # check vertex group B
        try:
            vg = ob.vertex_groups['B']
        except:
            ob.vertex_groups.new(name='B')

        for v in ob.data.vertices:
            ob.vertex_groups['A'].add([v.index], 1, 'REPLACE')
            ob.vertex_groups['B'].add([v.index], 0, 'REPLACE')

        ob.vertex_groups.update()
        ob.data.update()
        bpy.ops.object.mode_set(mode='WEIGHT_PAINT')

        return {'FINISHED'}

class bake_reaction_diffusion(Operator):
    bl_idname = "object.bake_reaction_diffusion"
    bl_label = "Bake Data"
    bl_description = ("Bake the Reaction-Diffusion to the cache directory")
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return context.object.type == 'MESH' and context.mode != 'EDIT_MESH'

    def execute(self, context):
        ob = context.object
        props = ob.reaction_diffusion_settings
        if props.fast_bake:
            bool_run = props.run
            props.run = False
            context.scene.frame_current = props.cache_frame_start
            fast_bake_def(ob, frame_start=props.cache_frame_start, frame_end=props.cache_frame_end)
            #create_fast_bake_def(ob, frame_start=props.cache_frame_start, frame_end=props.cache_frame_end)
            context.scene.frame_current = props.cache_frame_end
            props.run = bool_run
        else:
            for i in range(props.cache_frame_start, props.cache_frame_end):
                context.scene.frame_current = i
                reaction_diffusion_def(ob, bake=True)
        props.bool_cache = True

        return {'FINISHED'}

class reaction_diffusion_free_data(Operator):
    bl_idname = "object.reaction_diffusion_free_data"
    bl_label = "Free Data"
    bl_description = ("Free Reaction-Diffusion data")
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        return context.object.type == 'MESH'

    def execute(self, context):
        ob = context.object
        props = ob.reaction_diffusion_settings
        props.bool_cache = False

        folder = Path(props.cache_dir)
        for i in range(props.cache_frame_start, props.cache_frame_end):
            data_a = folder / "a_{:04d}".format(i)
            if os.path.exists(data_a):
                os.remove(data_a)
            data_a = folder / "b_{:04d}".format(i)
            if os.path.exists(data_a):
                os.remove(data_a)
        return {'FINISHED'}

from bpy.app.handlers import persistent

def reaction_diffusion_scene(scene, bake=False):
    for ob in scene.objects:
        if ob.reaction_diffusion_settings.run:
            reaction_diffusion_def(ob)

def reaction_diffusion_def(ob, bake=False):

    scene = bpy.context.scene
    start = time.time()
    if type(ob) == bpy.types.Scene: return None
    props = ob.reaction_diffusion_settings

    if bake or props.bool_cache:
        if props.cache_dir == '':
            letters = string.ascii_letters
            random_name = ''.join(rnd.choice(letters) for i in range(6))
            if bpy.context.blend_data.filepath == '':
                folder = Path(bpy.context.preferences.filepaths.temporary_directory)
                folder = folder / 'reaction_diffusion_cache' / random_name
            else:
                folder = '//' + Path(bpy.context.blend_data.filepath).stem
                folder = Path(bpy.path.abspath(folder)) / 'reaction_diffusion_cache' / random_name
            folder.mkdir(parents=True, exist_ok=True)
            props.cache_dir = str(folder)
        else:
            folder = Path(props.cache_dir)

    me = ob.data
    n_edges = len(me.edges)
    n_verts = len(me.vertices)
    a = np.zeros(n_verts)
    b = np.zeros(n_verts)

    print("{:6d} Reaction-Diffusion: {}".format(scene.frame_current, ob.name))

    if not props.bool_cache:

        if props.bool_mod:
            # hide deforming modifiers
            mod_visibility = []
            for m in ob.modifiers:
                mod_visibility.append(m.show_viewport)
                if not mod_preserve_shape(m): m.show_viewport = False

            # evaluated mesh
            dg = bpy.context.evaluated_depsgraph_get()
            ob_eval = ob.evaluated_get(dg)
            me = bpy.data.meshes.new_from_object(ob_eval, preserve_all_data_layers=True, depsgraph=dg)

            # set original visibility
            for v, m in zip(mod_visibility, ob.modifiers):
                m.show_viewport = v
            ob.modifiers.update()

        bm = bmesh.new()   # create an empty BMesh
        bm.from_mesh(me)   # fill it in from a Mesh
        dvert_lay = bm.verts.layers.deform.active

        dt = props.dt
        time_steps = props.time_steps
        f = props.f
        k = props.k
        diff_a = props.diff_a
        diff_b = props.diff_b
        scale = props.diff_mult

        brush_mult = props.brush_mult

        # store weight values
        if 'dB' in ob.vertex_groups: db = np.zeros(n_verts)
        if 'grad' in ob.vertex_groups: grad = np.zeros(n_verts)

        if props.vertex_group_diff_a != '': diff_a = np.zeros(n_verts)
        if props.vertex_group_diff_b != '': diff_b = np.zeros(n_verts)
        if props.vertex_group_scale != '': scale = np.zeros(n_verts)
        if props.vertex_group_f != '': f = np.zeros(n_verts)
        if props.vertex_group_k != '': k = np.zeros(n_verts)
        if props.vertex_group_brush != '': brush = np.zeros(n_verts)
        else: brush = 0

        group_index_a = ob.vertex_groups["A"].index
        group_index_b = ob.vertex_groups["B"].index
        a = bmesh_get_weight_numpy(group_index_a, dvert_lay, bm.verts)
        b = bmesh_get_weight_numpy(group_index_b, dvert_lay, bm.verts)

        if props.vertex_group_diff_a != '':
            group_index = ob.vertex_groups[props.vertex_group_diff_a].index
            diff_a = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
            if props.invert_vertex_group_diff_a:
                vg_bounds = (props.min_diff_a, props.max_diff_a)
            else:
                vg_bounds = (props.max_diff_a, props.min_diff_a)
            diff_a = np.interp(diff_a, (0,1), vg_bounds)

        if props.vertex_group_diff_b != '':
            group_index = ob.vertex_groups[props.vertex_group_diff_b].index
            diff_b = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
            if props.invert_vertex_group_diff_b:
                vg_bounds = (props.max_diff_b, props.min_diff_b)
            else:
                vg_bounds = (props.min_diff_b, props.max_diff_b)
            diff_b = np.interp(diff_b, (0,1), vg_bounds)

        if props.vertex_group_scale != '':
            group_index = ob.vertex_groups[props.vertex_group_scale].index
            scale = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
            if props.invert_vertex_group_scale:
                vg_bounds = (props.max_scale, props.min_scale)
            else:
                vg_bounds = (props.min_scale, props.max_scale)
            scale = np.interp(scale, (0,1), vg_bounds)

        if props.vertex_group_f != '':
            group_index = ob.vertex_groups[props.vertex_group_f].index
            f = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
            if props.invert_vertex_group_f:
                vg_bounds = (props.max_f, props.min_f)
            else:
                vg_bounds = (props.min_f, props.max_f)
            f = np.interp(f, (0,1), vg_bounds, )

        if props.vertex_group_k != '':
            group_index = ob.vertex_groups[props.vertex_group_k].index
            k = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
            if props.invert_vertex_group_k:
                vg_bounds = (props.max_k, props.min_k)
            else:
                vg_bounds = (props.min_k, props.max_k)
            k = np.interp(k, (0,1), vg_bounds)

        if props.vertex_group_brush != '':
            group_index = ob.vertex_groups[props.vertex_group_brush].index
            brush = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
            brush *= brush_mult


        #timeElapsed = time.time() - start
        #print('RD - Read Vertex Groups:',timeElapsed)
        #start = time.time()

        diff_a *= scale
        diff_b *= scale

        edge_verts = [0]*n_edges*2
        me.edges.foreach_get("vertices", edge_verts)
        edge_verts = np.array(edge_verts)

        if 'gradient' in ob.vertex_groups.keys() and False:
            group_index = ob.vertex_groups['gradient'].index
            gradient = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)

            arr = (np.arange(n_edges)*2).astype(int)
            id0 = edge_verts[arr]
            id1 = edge_verts[arr+1]

            #gradient = np.abs(gradient[id0] - gradient[id1])
            gradient = gradient[id1] - gradient[id0]
            gradient /= np.max(gradient)
            sign = np.sign(gradient)
            sign[sign==0] = 1
            gradient = (0.05*abs(gradient) + 0.95)*sign
            #gradient *= (1-abs(gradient)
            #gradient = 0.2*(1-gradient) + 0.95

        #gradient = get_uv_edge_vectors(me)
        #uv_dir = Vector((0.5,0.5,0)).normalized()
        #gradient = np.array([abs(g.dot(uv_dir.normalized())) for g in gradient])
        #gradient = (gradient + 0.5)/2
        #gradient = np.array([max(0,g.dot(uv_dir.normalized())) for g in gradient])

        timeElapsed = time.time() - start
        print('       Preparation Time:',timeElapsed)
        start = time.time()

        try:
            _f = f if type(f) is np.ndarray else np.array((f,))
            _k = k if type(k) is np.ndarray else np.array((k,))
            _diff_a = diff_a if type(diff_a) is np.ndarray else np.array((diff_a,))
            _diff_b = diff_b if type(diff_b) is np.ndarray else np.array((diff_b,))
            _brush = brush if type(brush) is np.ndarray else np.array((brush,))

            #a, b = numba_reaction_diffusion_anisotropic(n_verts, n_edges, edge_verts, a, b, _brush, _diff_a, _diff_b, _f, _k, dt, time_steps, gradient)
            a, b = numba_reaction_diffusion(n_verts, n_edges, edge_verts, a, b, _brush, _diff_a, _diff_b, _f, _k, dt, time_steps)
        except:
            print('Not using Numba! The simulation could be slow.')
            arr = np.arange(n_edges)*2
            id0 = edge_verts[arr]     # first vertex indices for each edge
            id1 = edge_verts[arr+1]   # second vertex indices for each edge
            for i in range(time_steps):
                b += brush
                lap_a = np.zeros(n_verts)
                lap_b = np.zeros(n_verts)
                lap_a0 =  a[id1] -  a[id0]   # laplacian increment for first vertex of each edge
                lap_b0 =  b[id1] -  b[id0]   # laplacian increment for first vertex of each edge

                np.add.at(lap_a, id0, lap_a0)
                np.add.at(lap_b, id0, lap_b0)
                np.add.at(lap_a, id1, -lap_a0)
                np.add.at(lap_b, id1, -lap_b0)

                ab2 = a*b**2
                a += eval("(diff_a*lap_a - ab2 + f*(1-a))*dt")
                b += eval("(diff_b*lap_b + ab2 - (k+f)*b)*dt")
                #a += (diff_a*lap_a - ab2 + f*(1-a))*dt
                #b += (diff_b*lap_b + ab2 - (k+f)*b)*dt

                a = nan_to_num(a)
                b = nan_to_num(b)

        timeElapsed = time.time() - start
        print('       Simulation Time:',timeElapsed)

    if bake:
        if not(os.path.exists(folder)):
            os.mkdir(folder)
        file_name = folder / "a_{:04d}".format(scene.frame_current)
        a.tofile(file_name)
        file_name = folder / "b_{:04d}".format(scene.frame_current)
        b.tofile(file_name)
    elif props.bool_cache:
        try:
            file_name = folder / "a_{:04d}".format(scene.frame_current)
            a = np.fromfile(file_name)
            file_name = folder / "b_{:04d}".format(scene.frame_current)
            b = np.fromfile(file_name)
        except:
            print('       Cannot read cache.')
            return

    if props.update_weight_a or props.update_weight_b:
        start = time.time()
        if props.update_weight_a:
            if 'A' in ob.vertex_groups.keys():
                vg_a = ob.vertex_groups['A']
            else:
                vg_a = ob.vertex_groups.new(name='A')
        else:
            vg_a = None
        if props.update_weight_b:
            if 'B' in ob.vertex_groups.keys():
                vg_b = ob.vertex_groups['B']
            else:
                vg_b = ob.vertex_groups.new(name='B')
        else:
            vg_b = None
        if vg_a == vg_b == None:
            pass
        else:
            if ob.mode == 'WEIGHT_PAINT':# or props.bool_cache:
                # slower, but prevent crashes
                for i in range(n_verts):
                    if vg_a: vg_a.add([i], a[i], 'REPLACE')
                    if vg_b: vg_b.add([i], b[i], 'REPLACE')
            else:
                if props.bool_mod or props.bool_cache:
                    #bm.free()               # release old bmesh
                    bm = bmesh.new()        # create an empty BMesh
                    bm.from_mesh(ob.data)   # fill it in from a Mesh
                    dvert_lay = bm.verts.layers.deform.active
                # faster, but can cause crashes while painting weight
                if vg_a: index_a = vg_a.index
                if vg_b: index_b = vg_b.index
                for i, v in enumerate(bm.verts):
                    dvert = v[dvert_lay]
                    if vg_a: dvert[index_a] = a[i]
                    if vg_b: dvert[index_b] = b[i]
                bm.to_mesh(ob.data)
                bm.free()
        print('       Writing Vertex Groups Time:',time.time() - start)
    if props.normalize:
        min_a = np.min(a)
        max_a = np.max(a)
        min_b = np.min(b)
        max_b = np.max(b)
        a = (a - min_a)/(max_a - min_a)
        b = (b - min_b)/(max_b - min_b)
    split_a = None
    split_b = None
    splitted = False
    if props.update_colors:#_a or props.update_colors_b:
        start = time.time()
        loops_size = get_attribute_numpy(me.polygons, attribute='loop_total', mult=1)
        n_colors = np.sum(loops_size)
        v_id = np.ones(n_colors)
        me.polygons.foreach_get('vertices',v_id)
        v_id = v_id.astype(int)
        #v_id = np.array([v for p in ob.data.polygons for v in p.vertices])
        '''
        if props.update_colors_b:
            if 'B' in ob.data.vertex_colors.keys():
                vc = ob.data.vertex_colors['B']
            else:
                vc = ob.data.vertex_colors.new(name='B')
            c_val = b[v_id]
            c_val = np.repeat(c_val, 4, axis=0)
            vc.data.foreach_set('color',c_val)

        if props.update_colors_a:
            if 'A' in ob.data.vertex_colors.keys():
                vc = ob.data.vertex_colors['A']
            else:
                vc = ob.data.vertex_colors.new(name='A')
            c_val = a[v_id]
            c_val = np.repeat(c_val, 4, axis=0)
            vc.data.foreach_set('color',c_val)
        '''
        split_a = a[v_id,None]
        split_b = b[v_id,None]
        splitted = True
        ones = np.ones((n_colors,1))
        #rgba = np.concatenate((split_a,split_b,-split_b+split_a,ones),axis=1).flatten()
        rgba = np.concatenate((split_a,split_b,ones,ones),axis=1).flatten()
        if 'AB' in ob.data.vertex_colors.keys():
            vc = ob.data.vertex_colors['AB']
        else:
            vc = ob.data.vertex_colors.new(name='AB')
        vc.data.foreach_set('color',rgba)
        ob.data.vertex_colors.update()

        print('       Writing Vertex Colors Time:',time.time() - start)
    if props.update_uv:
        start = time.time()
        if 'AB' in me.uv_layers.keys():
            uv_layer = me.uv_layers['AB']
        else:
            uv_layer = me.uv_layers.new(name='AB')
        if not splitted:
            loops_size = get_attribute_numpy(me.polygons, attribute='loop_total', mult=1)
            n_data = np.sum(loops_size)
            v_id = np.ones(n_data)
            me.polygons.foreach_get('vertices',v_id)
            v_id = v_id.astype(int)
            split_a = a[v_id,None]
            split_b = b[v_id,None]
        uv = np.concatenate((split_a,split_b),axis=1).flatten()
        uv_layer.data.foreach_set('uv',uv)
        me.uv_layers.update()
        print('       Writing UV Map Time:',time.time() - start)

    for ps in ob.particle_systems:
        if ps.vertex_group_density == 'B' or ps.vertex_group_density == 'A':
            ps.invert_vertex_group_density = not ps.invert_vertex_group_density
            ps.invert_vertex_group_density = not ps.invert_vertex_group_density

    if props.bool_mod and not props.bool_cache: bpy.data.meshes.remove(me)

def fast_bake_def(ob, frame_start=1, frame_end=250):
    scene = bpy.context.scene
    start = time.time()
    if type(ob) == bpy.types.Scene: return None
    props = ob.reaction_diffusion_settings

    # Define cache folder
    if props.cache_dir == '':
        letters = string.ascii_letters
        random_name = ''.join(rnd.choice(letters) for i in range(6))
        if bpy.context.blend_data.filepath == '':
            folder = Path(bpy.context.preferences.filepaths.temporary_directory)
            folder = folder / 'reaction_diffusion_cache' / random_name
        else:
            folder = '//' + Path(bpy.context.blend_data.filepath).stem
            folder = Path(bpy.path.abspath(folder)) / 'reaction_diffusion_cache' / random_name
        folder.mkdir(parents=True, exist_ok=True)
        props.cache_dir = str(folder)
    else:
        folder = Path(props.cache_dir)

    if props.bool_mod:
        # hide deforming modifiers
        mod_visibility = []
        for m in ob.modifiers:
            mod_visibility.append(m.show_viewport)
            if not mod_preserve_shape(m): m.show_viewport = False

        # evaluated mesh
        dg = bpy.context.evaluated_depsgraph_get()
        ob_eval = ob.evaluated_get(dg)
        me = bpy.data.meshes.new_from_object(ob_eval, preserve_all_data_layers=True, depsgraph=dg)

        # set original visibility
        for v, m in zip(mod_visibility, ob.modifiers):
            m.show_viewport = v
        ob.modifiers.update()
    else:
        me = ob.data

    bm = bmesh.new()   # create an empty BMesh
    bm.from_mesh(me)   # fill it in from a Mesh
    dvert_lay = bm.verts.layers.deform.active
    n_edges = len(me.edges)
    n_verts = len(me.vertices)
    a = np.zeros(n_verts)
    b = np.zeros(n_verts)
    group_index_a = ob.vertex_groups["A"].index
    group_index_b = ob.vertex_groups["B"].index

    dt = props.dt
    time_steps = props.time_steps
    f = props.f
    k = props.k
    diff_a = props.diff_a
    diff_b = props.diff_b
    scale = props.diff_mult

    brush_mult = props.brush_mult

    # store weight values
    if 'dB' in ob.vertex_groups: db = np.zeros(n_verts)
    if 'grad' in ob.vertex_groups: grad = np.zeros(n_verts)

    if props.vertex_group_diff_a != '': diff_a = np.zeros(n_verts)
    if props.vertex_group_diff_b != '': diff_b = np.zeros(n_verts)
    if props.vertex_group_scale != '': scale = np.zeros(n_verts)
    if props.vertex_group_f != '': f = np.zeros(n_verts)
    if props.vertex_group_k != '': k = np.zeros(n_verts)
    if props.vertex_group_brush != '': brush = np.zeros(n_verts)
    else: brush = 0

    a = bmesh_get_weight_numpy(group_index_a, dvert_lay, bm.verts)
    b = bmesh_get_weight_numpy(group_index_b, dvert_lay, bm.verts)

    if props.vertex_group_diff_a != '':
        group_index = ob.vertex_groups[props.vertex_group_diff_a].index
        diff_a = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
        if props.invert_vertex_group_diff_a:
            vg_bounds = (props.min_diff_a, props.max_diff_a)
        else:
            vg_bounds = (props.max_diff_a, props.min_diff_a)
        diff_a = np.interp(diff_a, (0,1), vg_bounds)

    if props.vertex_group_diff_b != '':
        group_index = ob.vertex_groups[props.vertex_group_diff_b].index
        diff_b = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
        if props.invert_vertex_group_diff_b:
            vg_bounds = (props.max_diff_b, props.min_diff_b)
        else:
            vg_bounds = (props.min_diff_b, props.max_diff_b)
        diff_b = np.interp(diff_b, (0,1), vg_bounds)

    if props.vertex_group_scale != '':
        group_index = ob.vertex_groups[props.vertex_group_scale].index
        scale = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
        if props.invert_vertex_group_scale:
            vg_bounds = (props.max_scale, props.min_scale)
        else:
            vg_bounds = (props.min_scale, props.max_scale)
        scale = np.interp(scale, (0,1), vg_bounds)

    if props.vertex_group_f != '':
        group_index = ob.vertex_groups[props.vertex_group_f].index
        f = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
        if props.invert_vertex_group_f:
            vg_bounds = (props.max_f, props.min_f)
        else:
            vg_bounds = (props.min_f, props.max_f)
        f = np.interp(f, (0,1), vg_bounds, )

    if props.vertex_group_k != '':
        group_index = ob.vertex_groups[props.vertex_group_k].index
        k = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
        if props.invert_vertex_group_k:
            vg_bounds = (props.max_k, props.min_k)
        else:
            vg_bounds = (props.min_k, props.max_k)
        k = np.interp(k, (0,1), vg_bounds)

    if props.vertex_group_brush != '':
        group_index = ob.vertex_groups[props.vertex_group_brush].index
        brush = bmesh_get_weight_numpy(group_index, dvert_lay, bm.verts)
        brush *= brush_mult

    diff_a *= scale
    diff_b *= scale

    edge_verts = [0]*n_edges*2
    me.edges.foreach_get("vertices", edge_verts)

    gradient = get_uv_edge_vectors(me)
    uv_dir = Vector((0.5,0.5,0))
    #gradient = [abs(g.dot(uv_dir)) for g in gradient]
    gradient = [max(0,g.dot(uv_dir)) for g in gradient]

    timeElapsed = time.time() - start
    print('       Preparation Time:',timeElapsed)
    start = time.time()

    try:
        edge_verts = np.array(edge_verts)
        _f = f if type(f) is np.ndarray else np.array((f,))
        _k = k if type(k) is np.ndarray else np.array((k,))
        _diff_a = diff_a if type(diff_a) is np.ndarray else np.array((diff_a,))
        _diff_b = diff_b if type(diff_b) is np.ndarray else np.array((diff_b,))
        _brush = brush if type(brush) is np.ndarray else np.array((brush,))

        run_rd = False
        for j in range(props.cache_frame_start, props.cache_frame_end+1):
            start2 = time.time()
            print("{:6d} Reaction-Diffusion: {}".format(j, ob.name))
            if run_rd:
                b += _brush
                a, b = numba_reaction_diffusion(n_verts, n_edges, edge_verts, a, b, _brush, _diff_a, _diff_b, _f, _k, dt, time_steps)
            else:
                run_rd = True

            if not(os.path.exists(folder)):
                os.mkdir(folder)
            file_name = folder / "a_{:04d}".format(j)
            a.tofile(file_name)
            file_name = folder / "b_{:04d}".format(j)
            b.tofile(file_name)

            timeElapsed = time.time() - start2
            print('       Simulation Time:',timeElapsed)

    except:
        print('Not using Numba! The simulation could be slow.')
        edge_verts = np.array(edge_verts)
        arr = np.arange(n_edges)*2
        id0 = edge_verts[arr]     # first vertex indices for each edge
        id1 = edge_verts[arr+1]   # second vertex indices for each edge
        for j in range(props.cache_frame_start, props.cache_frame_end):
            for i in range(time_steps):
                b += brush
                lap_a = np.zeros(n_verts)
                lap_b = np.zeros(n_verts)
                lap_a0 =  a[id1] -  a[id0]   # laplacian increment for first vertex of each edge
                lap_b0 =  b[id1] -  b[id0]   # laplacian increment for first vertex of each edge

                np.add.at(lap_a, id0, lap_a0)
                np.add.at(lap_b, id0, lap_b0)
                np.add.at(lap_a, id1, -lap_a0)
                np.add.at(lap_b, id1, -lap_b0)

                ab2 = a*b**2
                a += eval("(diff_a*lap_a - ab2 + f*(1-a))*dt")
                b += eval("(diff_b*lap_b + ab2 - (k+f)*b)*dt")

                a = nan_to_num(a)
                b = nan_to_num(b)

            if not(os.path.exists(folder)):
                os.mkdir(folder)
            file_name = folder / "a_{:04d}".format(j)
            a.tofile(file_name)
            file_name = folder / "b_{:04d}".format(j)
            b.tofile(file_name)

    if ob.mode == 'WEIGHT_PAINT':
        # slower, but prevent crashes
        vg_a = ob.vertex_groups['A']
        vg_b = ob.vertex_groups['B']
        for i in range(n_verts):
            vg_a.add([i], a[i], 'REPLACE')
            vg_b.add([i], b[i], 'REPLACE')
    else:
        if props.bool_mod:
            bm.free()               # release old bmesh
            bm = bmesh.new()        # create an empty BMesh
            bm.from_mesh(ob.data)   # fill it in from a Mesh
            dvert_lay = bm.verts.layers.deform.active
        # faster, but can cause crashes while painting weight
        for i, v in enumerate(bm.verts):
            dvert = v[dvert_lay]
            dvert[group_index_a] = a[i]
            dvert[group_index_b] = b[i]
        bm.to_mesh(ob.data)

    # Update Vertex Colors
    if 'A' in ob.data.vertex_colors or 'B' in ob.data.vertex_colors:
        v_id = np.array([v for p in ob.data.polygons for v in p.vertices])

        if 'B' in ob.data.vertex_colors:
            c_val = b[v_id]
            c_val = np.repeat(c_val, 4, axis=0)
            vc = ob.data.vertex_colors['B']
            vc.data.foreach_set('color',c_val.tolist())

        if 'A' in ob.data.vertex_colors:
            c_val = a[v_id]
            c_val = np.repeat(c_val, 4, axis=0)
            vc = ob.data.vertex_colors['A']
            vc.data.foreach_set('color',c_val.tolist())

    for ps in ob.particle_systems:
        if ps.vertex_group_density == 'B' or ps.vertex_group_density == 'A':
            ps.invert_vertex_group_density = not ps.invert_vertex_group_density
            ps.invert_vertex_group_density = not ps.invert_vertex_group_density

    if props.bool_mod: bpy.data.meshes.remove(me)
    bm.free()
    timeElapsed = time.time() - start
    print('       Closing Time:',timeElapsed)

def create_fast_bake_def(ob, frame_start=1, frame_end=250):
    scene = bpy.context.scene
    start = time.time()
    if type(ob) == bpy.types.Scene: return None
    props = ob.reaction_diffusion_settings

    dt = props.dt
    time_steps = props.time_steps
    scale = props.diff_mult

    if props.cache_dir == '':
        letters = string.ascii_letters
        random_name = ''.join(rnd.choice(letters) for i in range(6))
        if bpy.context.blend_data.filepath == '':
            folder = Path(bpy.context.preferences.filepaths.temporary_directory)
            folder = folder / 'reaction_diffusion_cache' / random_name
        else:
            folder = '//' + Path(bpy.context.blend_data.filepath).stem
            folder = Path(bpy.path.abspath(folder)) / 'reaction_diffusion_cache' / random_name
        folder.mkdir(parents=True, exist_ok=True)
        props.cache_dir = str(folder)
    else:
        folder = Path(props.cache_dir)

    if props.bool_mod:
        # hide deforming modifiers
        mod_visibility = []
        for m in ob.modifiers:
            mod_visibility.append(m.show_viewport)
            if not mod_preserve_shape(m): m.show_viewport = False

        # evaluated mesh
        dg = bpy.context.evaluated_depsgraph_get()
        ob_eval = ob.evaluated_get(dg)
        me = bpy.data.meshes.new_from_object(ob_eval, preserve_all_data_layers=True, depsgraph=dg)

        # set original visibility
        for v, m in zip(mod_visibility, ob.modifiers):
            m.show_viewport = v
        ob.modifiers.update()
    else:
        me = ob.data

    bm = bmesh.new()   # create an empty BMesh
    bm.from_mesh(me)   # fill it in from a Mesh
    verts = get_vertices_numpy(me)
    dvert_lay = bm.verts.layers.deform.active
    n_edges = len(me.edges)
    n_verts = len(me.vertices)
    group_index_x = ob.vertex_groups["x"].index
    group_index_y = ob.vertex_groups["y"].index
    group_index_module = ob.vertex_groups["module"].index
    group_index_values = ob.vertex_groups["values"].index

    if not props.bool_cache:
        time_steps = props.time_steps

        # store weight values
        if 'dB' in ob.vertex_groups: db = np.zeros(n_verts)
        if 'grad' in ob.vertex_groups: grad = np.zeros(n_verts)
        vec_x = np.zeros(n_verts)
        vec_y = np.zeros(n_verts)
        vec_module = np.zeros(n_verts)
        values = np.zeros(n_verts)

        vec_x = bmesh_get_weight_numpy(group_index_x, dvert_lay, bm.verts)
        vec_y = bmesh_get_weight_numpy(group_index_y, dvert_lay, bm.verts)
        vec_module = bmesh_get_weight_numpy(group_index_module, dvert_lay, bm.verts)
        values = bmesh_get_weight_numpy(group_index_values, dvert_lay, bm.verts)
        field = np.concatenate((vec_x[:,None],vec_y[:,None],vec_y[:,None]*0),axis=1)
        field = field*2-1
        field[:,2] = 0
        edge_verts = get_edges_numpy(me)

        id0 = edge_verts[:,0]
        id1 = edge_verts[:,1]
        vert0 = verts[id0]
        vert1 = verts[id1]
        vec = vert1-vert0
        edge_field = (field[id0] + field[id1])/2    # average vector associated to the edge
        print(vert0.shape)
        print(field.shape)
        print(edge_field.shape)
        # normalize vectors
        vec /= np.linalg.norm(vec,axis=1)[:,None]
        edge_field /= np.linalg.norm(edge_field,axis=1)[:,None]
        edge_flow = np.einsum('...j,...j', vec, edge_field)
        #sign = (edge_flow>0).astype(int)
        #values[edge_verts[sign]] += values[edge_verts[1-sign]]*
        #values[verts0] += values[verts1]*edge_flow

        timeElapsed = time.time() - start
        print('       Preparation Time:',timeElapsed)
        start = time.time()

        # Preserve energy
        mult = np.zeros(values.shape)
        #mult[id0] -= edge_flow
        #mult[id1] += edge_flow
        np.add.at(mult,id0,-edge_flow)
        np.add.at(mult,id1,edge_flow)
        print("mult")
        mult = scale/mult
        print(mult)
        print(np.sum(mult))


        #try:
        print(vec)
        print(edge_flow)
        print(edge_flow)

        bool_run = False
        for j in range(props.cache_frame_start, props.cache_frame_end+1):
            start2 = time.time()
            print("{:6d} Reaction-Diffusion: {}".format(j, ob.name))
            if bool_run:
                print(values)
                #for i in range(1):
                values = integrate_field(n_edges,id0,id1,values,edge_flow,mult,time_steps)
                #values0 = values
                #np.add.at(values, id0, values0[id1]*edge_flow*mult[id1])
                #np.add.at(values, id1, -values0[id0]*edge_flow*mult[id0])
                #np.add.at(values, id0, values0[id1]*edge_flow*mult)
                #np.add.at(values, id1, -values0[id0]*edge_flow*mult)
                #values[id1] += values0[id0]*edge_flow/mult[id1]*dt
                #values[id0] -= values0[id1]*edge_flow/mult[id0]*dt
                #values[id1] = edge_flow
                #values[id1] += edge_flow
                #a, b = numba_reaction_diffusion(n_verts, n_edges, edge_verts, a, b, _brush, _diff_a, _diff_b, _f, _k, dt, time_steps)

                '''
                lap_a = np.zeros(n_verts)
                lap_b = np.zeros(n_verts)
                lap_a0 =  a[id1] -  a[id0]   # laplacian increment for first vertex of each edge
                lap_b0 =  b[id1] -  b[id0]   # laplacian increment for first vertex of each edge

                np.add.at(lap_a, id0, lap_a0)
                np.add.at(lap_b, id0, lap_b0)
                np.add.at(lap_a, id1, -lap_a0)
                np.add.at(lap_b, id1, -lap_b0)
                '''
            else:
                bool_run = True

            if not(os.path.exists(folder)):
                os.mkdir(folder)
            file_name = folder / "a_{:04d}".format(j)
            values.tofile(file_name)
            file_name = folder / "b_{:04d}".format(j)
            values.tofile(file_name)


            timeElapsed = time.time() - start2
            print('       Simulation Time:',timeElapsed)

    if props.bool_mod: bpy.data.meshes.remove(me)
    bm.free()
    timeElapsed = time.time() - start
    print('       Closing Time:',timeElapsed)





class TISSUE_PT_reaction_diffusion(Panel):
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "data"
    bl_label = "Tissue Reaction-Diffusion"
    bl_options = {'DEFAULT_CLOSED'}

    @classmethod
    def poll(cls, context):
        return 'A' and 'B' in context.object.vertex_groups

    def draw(self, context):
        reaction_diffusion_add_handler(self, context)

        ob = context.object
        props = ob.reaction_diffusion_settings
        layout = self.layout
        col = layout.column(align=True)
        row = col.row(align=True)
        if not ("A" and "B" in ob.vertex_groups):
            row.operator("object.start_reaction_diffusion",
                        icon="EXPERIMENTAL",
                        text="Reaction-Diffusion")
        else:
            row.operator("object.start_reaction_diffusion",
                        icon="EXPERIMENTAL",
                        text="Reset Reaction-Diffusion")
            row = col.row(align=True)
            row.prop(props, "run", text="Run Reaction-Diffusion")
            col = layout.column(align=True)
            row = col.row(align=True)
            row.prop(props, "time_steps")
            row.prop(props, "dt")
            row.enabled = not props.bool_cache
            col.separator()
            row = col.row(align=True)
            col1 = row.column(align=True)
            col1.prop(props, "diff_a")
            col1.enabled = props.vertex_group_diff_a == '' and not props.bool_cache
            col1 = row.column(align=True)
            col1.prop(props, "diff_b")
            col1.enabled = props.vertex_group_diff_b == '' and not props.bool_cache
            row = col.row(align=True)
            row.prop(props, "diff_mult")
            row.enabled = props.vertex_group_scale == '' and not props.bool_cache
            #col.separator()
            row = col.row(align=True)
            col1 = row.column(align=True)
            col1.prop(props, "f")
            col1.enabled = props.vertex_group_f == '' and not props.bool_cache
            col1 = row.column(align=True)
            col1.prop(props, "k")
            col1.enabled = props.vertex_group_k == '' and not props.bool_cache
            col.separator()
            col.label(text='Cache:')
            #col.prop(props, "bool_cache")
            col.prop(props, "cache_dir", text='')
            col.separator()
            row = col.row(align=True)
            row.prop(props, "cache_frame_start")
            row.prop(props, "cache_frame_end")
            col.separator()
            if props.bool_cache:
                col.operator("object.reaction_diffusion_free_data")
            else:
                row = col.row(align=True)
                row.operator("object.bake_reaction_diffusion")
                file = bpy.context.blend_data.filepath
                temp = bpy.context.preferences.filepaths.temporary_directory
                if file == temp == props.cache_dir == '':
                    row.enabled = False
                    col.label(text="Cannot use cache", icon='ERROR')
                    col.label(text='please save the Blender or set a Cache directory')
                col.prop(props, "fast_bake")

            col.separator()
            col.label(text='Output attributes:')
            row = col.row(align=True)
            col2 = row.column(align=True)
            row2 = col2.row(align=True)
            row2.prop(props, "update_weight_a", icon='GROUP_VERTEX', text='A')
            row2.prop(props, "update_weight_b", icon='GROUP_VERTEX', text='B')
            col2.enabled = props.bool_cache
            row.separator()
            #row.prop(props, "update_colors_a", icon='GROUP_VCOL', text='A')
            #row.prop(props, "update_colors_b", icon='GROUP_VCOL', text='B')
            row.prop(props, "update_colors", icon='GROUP_VCOL', text='AB')
            row.separator()
            row.prop(props, "update_uv", icon='GROUP_UVS', text='AB')
            col.prop(props,'normalize')

            #col.prop_search(props, 'vertex_group_diff_a', ob, "vertex_groups", text='Diff A')
            #col.prop_search(props, 'vertex_group_diff_b', ob, "vertex_groups", text='Diff B')
            #col.prop_search(props, 'vertex_group_scale', ob, "vertex_groups", text='Scale')
            #col.prop_search(props, 'vertex_group_f', ob, "vertex_groups", text='f')
            #col.prop_search(props, 'vertex_group_k', ob, "vertex_groups", text='k')


class TISSUE_PT_reaction_diffusion_weight(Panel):
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = "data"
    bl_parent_id = "TISSUE_PT_reaction_diffusion"
    bl_label = "Vertex Groups"
    bl_options = {'DEFAULT_CLOSED'}

    @classmethod
    def poll(cls, context):
        return 'A' and 'B' in context.object.vertex_groups

    def draw(self, context):
        ob = context.object
        props = ob.reaction_diffusion_settings
        layout = self.layout
        #layout.use_property_split = True
        col = layout.column(align=True)
        col.prop(props, "bool_mod")
        if props.bool_mod and props.fast_bake:
            col.label(text="When Fast Bake is on, the modifiers", icon='ERROR')
            col.label(text="          are used only for the first frame")
        col.separator()
        insert_weight_parameter(col, ob, 'brush', text='Brush:')
        insert_weight_parameter(col, ob, 'diff_a', text='Diff A:')
        insert_weight_parameter(col, ob, 'diff_b', text='Diff B:')
        insert_weight_parameter(col, ob, 'scale', text='Scale:')
        insert_weight_parameter(col, ob, 'f', text='f:')
        insert_weight_parameter(col, ob, 'k', text='k:')
        col.enabled = not props.bool_cache

def insert_weight_parameter(col, ob, name, text=''):
    props = ob.reaction_diffusion_settings
    split = col.split(factor=0.25, align=True)
    col2 = split.column(align=True)
    col2.label(text=text)
    col2 = split.column(align=True)
    row2 = col2.row(align=True)
    row2.prop_search(props, 'vertex_group_' + name, ob, "vertex_groups", text='')
    if name != 'brush':
        row2.prop(props, "invert_vertex_group_" + name, text="", toggle=True, icon='ARROW_LEFTRIGHT')
    if 'vertex_group_' + name in props:
        if props['vertex_group_' + name] != '':
            if name == 'brush':
                col2.prop(props, "brush_mult")
            else:
                row2 = col2.row(align=True)
                row2.prop(props, "min_" + name, text="Min")
                row2 = col2.row(align=True)
                row2.prop(props, "max_" + name, text="Max")
    col.separator()

def contour_edges_pattern(operator, c, verts_count, iso_val, vertices, normals, filtered_edges, weight, pattern_weight, bevel_weight):
    # vertices indexes
    id0 = filtered_edges[:,0]
    id1 = filtered_edges[:,1]
    # vertices weight
    w0 = weight[id0]
    w1 = weight[id1]
    # weight condition
    bool_w0 = w0 < iso_val
    bool_w1 = w1 < iso_val

    # mask all edges that have one weight value below the iso value
    mask_new_verts = np.logical_xor(bool_w0, bool_w1)
    if not mask_new_verts.any():
        return np.array([[None]]), {}, np.array([[None]]), np.array([[None]])

    id0 = id0[mask_new_verts]
    id1 = id1[mask_new_verts]
    # filter arrays
    v0 = vertices[id0]
    v1 = vertices[id1]
    n0 = normals[id0]
    n1 = normals[id1]
    w0 = w0[mask_new_verts]
    w1 = w1[mask_new_verts]
    pattern0 = pattern_weight[id0]
    pattern1 = pattern_weight[id1]
    try:
        bevel0 = bevel_weight[id0]
        bevel1 = bevel_weight[id1]
    except: pass

    ### Spiral
    #edge_nor = (n0+n1)/2
    #shift = np.arctan2(edge_nor[:,0], edge_nor[:,1])/2/pi*delta_iso

    #param = (iso_val + shift - w0)/(w1-w0)
    param = (iso_val - w0)/(w1-w0)
    # pattern displace
    #mult = 1 if c%2 == 0 else -1
    if c%(operator.in_steps + operator.out_steps) < operator.in_steps:
        mult = operator.in_displace
    else:
        mult = operator.out_displace
    pattern_value = pattern0 + (pattern1-pattern0)*param
    try:
        bevel_value = bevel0 + (bevel1-bevel0)*param
        bevel_value = np.expand_dims(bevel_value,axis=1)
    except: bevel_value = None
    disp = pattern_value * mult

    param = np.expand_dims(param,axis=1)
    disp = np.expand_dims(disp,axis=1)
    verts = v0 + (v1-v0)*param
    norm = n0 + (n1-n0)*param
    if operator.limit_z: disp *= 1-abs(np.expand_dims(norm[:,2], axis=1))
    verts = verts + norm*disp
    #verts = verts[np.flip(np.argsort(shift))]
    #verts = verts[np.argsort(shift)]

    # indexes of edges with new vertices
    edges_index = filtered_edges[mask_new_verts][:,2]

    # remove all edges completely below the iso value
    #mask_edges = np.logical_not(np.logical_and(bool_w0, bool_w1))
    #filtered_edges = filtered_edges[mask_edges]
    return filtered_edges, edges_index, verts, bevel_value

def contour_bmesh(me, bm, weight, iso_val):
    bm.verts.ensure_lookup_table()
    bm.edges.ensure_lookup_table()
    bm.faces.ensure_lookup_table()

    # store weight values

    vertices = get_vertices_numpy(me)
    faces_mask = np.array(bm.faces)
    filtered_edges = get_edges_id_numpy(me)
    n_verts = len(bm.verts)

    #############################

    # vertices indexes
    id0 = filtered_edges[:,0]
    id1 = filtered_edges[:,1]
    # vertices weight
    w0 = weight[id0]
    w1 = weight[id1]
    # weight condition
    bool_w0 = w0 < iso_val
    bool_w1 = w1 < iso_val

    # mask all edges that have one weight value below the iso value
    mask_new_verts = np.logical_xor(bool_w0, bool_w1)
    if not mask_new_verts.any(): return np.array([[None]]), {}, np.array([[None]])

    id0 = id0[mask_new_verts]
    id1 = id1[mask_new_verts]
    # filter arrays
    v0 = vertices[id0]
    v1 = vertices[id1]
    w0 = w0[mask_new_verts]
    w1 = w1[mask_new_verts]
    param = (iso_val-w0)/(w1-w0)
    param = np.expand_dims(param,axis=1)
    verts = v0 + (v1-v0)*param

    # indexes of edges with new vertices
    #edges_index = filtered_edges[mask_new_verts][:,2]

    edges_id = {}
    for i, e in enumerate(filtered_edges):
        #edges_id[id] = i + n_verts
        edges_id['{}_{}'.format(e[0],e[1])] = i + n_verts
        edges_id['{}_{}'.format(e[1],e[0])] = i + n_verts


    '''
    for e in filtered_edges:
        id0 = e.verts[0].index
        id1 = e.verts[1].index
        w0 = weight[id0]
        w1 = weight[id1]

        if w0 == w1: continue
        elif w0 > iso_val and w1 > iso_val:
            continue
        elif w0 < iso_val and w1 < iso_val: continue
        elif w0 == iso_val or w1 == iso_val: continue
        else:
            v0 = me0.vertices[id0].co
            v1 = me0.vertices[id1].co
            v = v0.lerp(v1, (iso_val-w0)/(w1-w0))
            delete_edges.append(e)
            verts.append(v)
            edges_id[str(id0)+"_"+str(id1)] = count
            edges_id[str(id1)+"_"+str(id0)] = count
            count += 1
    '''

    splitted_faces = []

    switch = False
    # splitting faces
    for f in faces_mask:
        # create sub-faces slots. Once a new vertex is reached it will
        # change slot, storing the next vertices for a new face.
        build_faces = [[],[]]
        #switch = False
        verts0 = list(me.polygons[f.index].vertices)
        verts1 = list(verts0)
        verts1.append(verts1.pop(0)) # shift list
        for id0, id1 in zip(verts0, verts1):

            # add first vertex to active slot
            build_faces[switch].append(id0)

            # try to split edge
            try:
                # check if the edge must be splitted
                new_vert = edges_id['{}_{}'.format(id0,id1)]
                # add new vertex
                build_faces[switch].append(new_vert)
                # if there is an open face on the other slot
                if len(build_faces[not switch]) > 0:
                    # store actual face
                    splitted_faces.append(build_faces[switch])
                    # reset actual faces and switch
                    build_faces[switch] = []
                    # change face slot
                switch = not switch
                # continue previous face
                build_faces[switch].append(new_vert)
            except: pass
        if len(build_faces[not switch]) == 2:
            build_faces[not switch].append(id0)
        if len(build_faces[not switch]) > 2:
            splitted_faces.append(build_faces[not switch])
        # add last face
        splitted_faces.append(build_faces[switch])

    # adding new vertices use fast local method access
    _new_vert = bm.verts.new
    for v in verts: _new_vert(v)
    bm.verts.ensure_lookup_table()

    # deleting old edges/faces
    bm.edges.ensure_lookup_table()
    remove_edges = [bm.edges[i] for i in filtered_edges[:,2]]
    #for e in remove_edges: bm.edges.remove(e)
    #for e in delete_edges: bm.edges.remove(e)

    bm.verts.ensure_lookup_table()
    # adding new faces use fast local method access
    _new_face = bm.faces.new
    missed_faces = []
    for f in splitted_faces:
        try:
            face_verts = [bm.verts[i] for i in f]
            _new_face(face_verts)
        except:
            missed_faces.append(f)

    #me = bpy.data.meshes.new('_tissue_tmp_')
    bm.to_mesh(me)
    weight = np.concatenate((weight, np.ones(len(verts))*iso_val))

    return me, bm, weight




class tissue_weight_streamlines(Operator):
    bl_idname = "object.tissue_weight_streamlines"
    bl_label = "Streamlines Curves"
    bl_description = ("")
    bl_options = {'REGISTER', 'UNDO'}

    mode : EnumProperty(
        items=(
            ('VERTS', "Verts", "Follow vertices"),
            ('EDGES', "Edges", "Follow Edges")
            ),
        default='VERTS',
        name="Streamlines path mode"
        )

    interpolation : EnumProperty(
        items=(
            ('POLY', "Poly", "Generate Polylines"),
            ('NURBS', "NURBS", "Generate Nurbs curves")
            ),
        default='POLY',
        name="Interpolation mode"
        )

    use_modifiers : BoolProperty(
        name="Use Modifiers", default=True,
        description="Apply all the modifiers")

    use_selected : BoolProperty(
        name="Use Selected Vertices", default=False,
        description="Use selected vertices as Seed")

    same_weight : BoolProperty(
        name="Same Weight", default=True,
        description="Continue the streamlines when the weight is the same")

    min_iso : FloatProperty(
        name="Min Value", default=0., soft_min=0, soft_max=1,
        description="Minimum weight value")
    max_iso : FloatProperty(
        name="Max Value", default=1, soft_min=0, soft_max=1,
        description="Maximum weight value")

    rand_seed : IntProperty(
        name="Seed", default=0, min=0, soft_max=10,
        description="Random Seed")
    n_curves : IntProperty(
        name="Curves", default=50, soft_min=1, soft_max=100000,
        description="Number of Curves")
    min_rad = 1
    max_rad = 1

    pos_steps : IntProperty(
        name="High Steps", default=50, min=0, soft_max=100,
        description="Number of steps in the direction of high weight")
    neg_steps : IntProperty(
        name="Low Steps", default=50, min=0, soft_max=100,
        description="Number of steps in the direction of low weight")

    bevel_depth : FloatProperty(
        name="Bevel Depth", default=0, min=0, soft_max=1,
        description="")
    min_bevel_depth : FloatProperty(
        name="Min Bevel Depth", default=0.1, min=0, soft_max=1,
        description="")
    max_bevel_depth : FloatProperty(
        name="Max Bevel Depth", default=1, min=0, soft_max=1,
        description="")

    rand_dir : FloatProperty(
        name="Randomize", default=0, min=0, max=1,
        description="Randomize streamlines directions (Slower)")

    vertex_group_seeds : StringProperty(
        name="Displace", default='',
        description="Vertex Group used for pattern displace")

    vertex_group_bevel : StringProperty(
        name="Bevel", default='',
        description="Variable Bevel depth")

    object_name : StringProperty(
        name="Active Object", default='',
        description="")

    try: vg_name = bpy.context.object.vertex_groups.active.name
    except: vg_name = ''

    vertex_group_streamlines : StringProperty(
        name="Flow", default=vg_name,
        description="Vertex Group used for streamlines")

    @classmethod
    def poll(cls, context):
        ob = context.object
        return ob and len(ob.vertex_groups) > 0 or ob.type == 'CURVE'

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

    def draw(self, context):
        if not context.object.type == 'CURVE':
            self.object_name = context.object.name
        ob = bpy.data.objects[self.object_name]
        if self.vertex_group_streamlines not in [vg.name for vg in ob.vertex_groups]:
            self.vertex_group_streamlines = ob.vertex_groups.active.name
        layout = self.layout
        col = layout.column(align=True)
        row = col.row(align=True)
        row.prop(self, 'mode', expand=True,
            slider=True, toggle=False, icon_only=False, event=False,
            full_event=False, emboss=True, index=-1)
        col.prop(self, "use_modifiers")
        col.label(text="Streamlines Curves:")
        row = col.row(align=True)
        row.prop(self, 'interpolation', expand=True,
            slider=True, toggle=False, icon_only=False, event=False,
            full_event=False, emboss=True, index=-1)
        col.separator()
        col.prop_search(self, 'vertex_group_streamlines', ob, "vertex_groups", text='')
        if not (self.use_selected or context.mode == 'EDIT_MESH'):
            row = col.row(align=True)
            row.prop(self,'n_curves')
            #row.enabled = context.mode != 'EDIT_MESH'
            row = col.row(align=True)
            row.prop(self,'rand_seed')
            #row.enabled = context.mode != 'EDIT_MESH'
        row = col.row(align=True)
        row.prop(self,'neg_steps')
        row.prop(self,'pos_steps')
        #row = col.row(align=True)
        #row.prop(self,'min_iso')
        #row.prop(self,'max_iso')
        col.prop(self, "same_weight")
        col.separator()
        col.label(text='Curves Bevel:')
        col.prop_search(self, 'vertex_group_bevel', ob, "vertex_groups", text='')
        if self.vertex_group_bevel != '':
            row = col.row(align=True)
            row.prop(self,'min_bevel_depth')
            row.prop(self,'max_bevel_depth')
        else:
            col.prop(self,'bevel_depth')
        col.separator()
        col.prop(self, "rand_dir")

    def execute(self, context):
        start_time = timeit.default_timer()
        try:
            check = context.object.vertex_groups[0]
        except:
            self.report({'ERROR'}, "The object doesn't have Vertex Groups")
            return {'CANCELLED'}
        ob = bpy.data.objects[self.object_name]
        ob.select_set(False)


        seeds = []

        if bpy.context.mode == 'EDIT_MESH':
            self.use_selected = True
            bpy.ops.object.mode_set(mode='OBJECT')
            #ob = bpy.context.object
            #me = simple_to_mesh(ob)
        ob = convert_object_to_mesh(ob, apply_modifiers=self.use_modifiers)
        #dg = context.evaluated_depsgraph_get()
        #ob = ob.evaluated_get(dg)
        me = ob.data

        if self.use_selected:
            # generate new bmesh
            bm = bmesh.new()
            bm.from_mesh(me)
            print(len(me.vertices))
            #for v in me.vertices:
            #    if v.select: seeds.append(v.index)
            for v in bm.verts:
                if v.select: seeds.append(v.index)
            bm.free()
        n_verts = len(me.vertices)
        n_edges = len(me.edges)
        n_faces = len(me.polygons)

        # store weight values
        try:
            weight = get_weight_numpy(ob.vertex_groups[self.vertex_group_streamlines], n_verts)
        except:
            bpy.data.objects.remove(ob)
            self.report({'ERROR'}, "Please select a Vertex Group for streamlines")
            return {'CANCELLED'}

        variable_bevel = False
        bevel_weight = None
        bevel_depth = self.bevel_depth
        try:
            if self.min_bevel_depth == self.max_bevel_depth:
                #bevel_weight = np.ones((n_verts))
                bevel_depth = self.min_bevel_depth
            else:
                b0 = min(self.min_bevel_depth, self.max_bevel_depth)
                b1 = max(self.min_bevel_depth, self.max_bevel_depth)
                bevel_weight = get_weight_numpy(ob.vertex_groups[self.vertex_group_bevel], n_verts)
                if self.min_bevel_depth > self.max_bevel_depth:
                    bevel_weight = 1-bevel_weight
                bevel_weight = b0/b1 + bevel_weight*((b1-b0)/b1)
                bevel_depth = b1
            variable_bevel = True
        except:
            pass#bevel_weight = np.ones((n_verts))


        if not seeds:
            np.random.seed(self.rand_seed)
            seeds = np.random.randint(n_verts, size=self.n_curves)

        #weight = np.array(get_weight(ob.vertex_groups.active, n_verts))

        curves_pts = []
        curves_weight = []

        neigh = [[] for i in range(n_verts)]
        if self.mode == 'EDGES':
            # store neighbors
            for e in me.edges:
                ev = e.vertices
                neigh[ev[0]].append(ev[1])
                neigh[ev[1]].append(ev[0])

        elif self.mode == 'VERTS':
            # store neighbors
            for p in me.polygons:
                face_verts = [v for v in p.vertices]
                n_face_verts = len(face_verts)
                for i in range(n_face_verts):
                    fv = face_verts.copy()
                    neigh[fv.pop(i)] += fv

        neigh_weight = [weight[n].tolist() for n in neigh]

        # evaluate direction
        next_vert = [-1]*n_verts

        if self.rand_dir > 0:
            for i in range(n_verts):
                n = neigh[i]
                nw = neigh_weight[i]
                sorted_nw = neigh_weight[i].copy()
                sorted_nw.sort()
                for w in sorted_nw:
                    neigh[i] = [n[nw.index(w)] for w in sorted_nw]
        else:
            if self.pos_steps > 0:
                for i in range(n_verts):
                    n = neigh[i]
                    nw = neigh_weight[i]
                    max_w = max(nw)
                    if self.same_weight:
                        if max_w >= weight[i]:
                            next_vert[i] = n[nw.index(max(nw))]
                    else:
                        if max_w > weight[i]:
                            next_vert[i] = n[nw.index(max(nw))]

            if self.neg_steps > 0:
                prev_vert = [-1]*n_verts
                for i in range(n_verts):
                    n = neigh[i]
                    nw = neigh_weight[i]
                    min_w = min(nw)
                    if self.same_weight:
                        if min_w <= weight[i]:
                            prev_vert[i] = n[nw.index(min(nw))]
                    else:
                        if min_w < weight[i]:
                            prev_vert[i] = n[nw.index(min(nw))]

        co = [0]*3*n_verts
        me.vertices.foreach_get('co', co)
        co = np.array(co).reshape((-1,3))

        # create streamlines
        curves = []
        for i in seeds:
            next_pts = [i]
            for j in range(self.pos_steps):
                if self.rand_dir > 0:
                    n = neigh[next_pts[-1]]
                    next = n[int(len(n) * (1-random.random() * self.rand_dir))]
                else:
                    next = next_vert[next_pts[-1]]
                if next > 0:
                    if next not in next_pts: next_pts.append(next)
                else: break

            prev_pts = [i]
            for j in range(self.neg_steps):
                if self.rand_dir > 0:
                    n = neigh[prev_pts[-1]]
                    prev = n[int(len(n) * random.random() * self.rand_dir)]
                else:
                    prev = prev_vert[prev_pts[-1]]
                if prev > 0:
                    if prev not in prev_pts:
                        prev_pts.append(prev)
                else: break

            next_pts = np.array(next_pts).astype('int')
            prev_pts = np.flip(prev_pts[1:]).astype('int')
            all_pts = np.concatenate((prev_pts, next_pts))
            if len(all_pts) > 1:
                curves.append(all_pts)
        crv = nurbs_from_vertices(curves, co, bevel_weight, ob.name + '_Streamlines', True, self.interpolation)
        crv.data.bevel_depth = bevel_depth
        crv.matrix_world = ob.matrix_world
        bpy.data.objects.remove(ob)

        print("Streamlines Curves, total time: " + str(timeit.default_timer() - start_time) + " sec")
        return {'FINISHED'}
