bl_info = {
    "name": "Simple panels",
    "blender": (2, 80, 0),
    "category": "Object",
}

if "bpy" in locals():
    import importlib
    if "edge_walker" in locals():
        importlib.reload(edge_walker)

import bpy

from . import (
    edge_walker
)
import bmesh
import random
import time
from functools import reduce

class SimplePanels(bpy.types.Operator):
    """Simple panel generator"""      # Use this as a tooltip for menu items and buttons.
    bl_idname = "object.simple_panels"        # Unique identifier for buttons and menu items to reference.
    bl_label = "Simple panels"         # Display name in the interface.
    bl_options = {'REGISTER', 'UNDO'}  # Enable undo for the operator.
    seed: bpy.props.IntProperty(name="Seed", default=1, min=1, max=999)
    forward_chance: bpy.props.FloatProperty(
        name="Forward walk", default=0.65, min=0, max=1)
    panel_line_bevel_offset: bpy.props.FloatProperty(
        name="Line width", default=0.01, min=0)
    inset_thickness_factor: bpy.props.FloatProperty(
        name="Line extrude width", default=0, min=0, max=1, subtype="FACTOR")
    inset_depth: bpy.props.FloatProperty(
        name="Line depth", default=0.1)
    bevel_panel_corners: bpy.props.BoolProperty(
        name="Bevel panel corners", default=False)
    bevel_panel_corners_chance: bpy.props.FloatProperty(
        name="Bevel corner chance", default=0.75, min=0, max=1)
    bevel_clamp_overlap: bpy.props.BoolProperty(
        name="Bevel clamp overlap", default=False)

    @classmethod
    def poll(cls, context):
        return context.edit_object is not None

    def invoke(self, context, event):
        self.panel_line_bevel_offset = self.__calc_default_panel_line_bevel_offset(context)
        self.inset_depth = self.panel_line_bevel_offset
        return self.execute(context)

    def cancel(self, context):
        self.report({'INFO'}, 'Cancel')

    # execute() is called when running the operator.
    def execute(self, context):

        self.report(
            {'INFO'}, 'Fwd chance: %.2f;  bevel corners: %s; seed: %d' %
            (
                self.forward_chance,
                self.bevel_panel_corners,
                self.seed
            )
        )
        
        perf_total_start = time.time()
        self.__perf_metric_cut_corners_s = 0;
        self.__perf_metric_walk_s = 0;
        self.__perf_metric_next_vert_pick_s = 0;
        self.__perf_metric_cut_lines_s = 0;
        random.seed(self.seed)
        mesh = context.edit_object.data
        bm = bmesh.from_edit_mesh(mesh)
        bm.verts.ensure_lookup_table()
        bm.edges.ensure_lookup_table()

        start_edge = random.choice(bm.edges)
        start_vert = random.choice(start_edge.verts)

        walker = edge_walker.EdgeWalker(bm)
        walker.start(start_edge, start_vert)

        self.__walk(walker)

        if self.bevel_panel_corners:
            perf_cut_corners_start = time.time()
            self.__cut_corners(bm, walker.traversed_edges)
            self.__perf_metric_cut_corners_s = time.time() - perf_cut_corners_start

        # TODO: for now just select the edges
        for e in walker.traversed_edges:
            e.select = True
        bm.select_flush_mode()

        self.__cut_lines()

        bmesh.update_edit_mesh(mesh)

        perf_total_s = time.time() - perf_total_start
        self.report(
            {'INFO'},
            "Traversed edges: %d; has open vert: %s. perf_cut_corners=%d%%; perf_walk=%d%%; perf_cut=%d%%; perf_pick=%d%%" %
            (
                len(walker.traversed_edges),
                walker.first_open_vert() != None,
                self.__perf_metric_cut_corners_s / perf_total_s * 100,
                self.__perf_metric_walk_s / perf_total_s * 100,
                self.__perf_metric_cut_lines_s / perf_total_s * 100,
                self.__perf_metric_next_vert_pick_s / perf_total_s * 100
            )
        )

        # Lets Blender know the operator finished successfully.
        return {'FINISHED'}

    def __walk(self, walker):
        while True:
            perf_walk_start = time.time()
            while walker.is_valid() and not walker.ends_at_traversed_vertex():
                if random.random() < self.forward_chance:
                    walker.forward()
                else:
                    walker.turn()
            self.__perf_metric_walk_s += time.time() - perf_walk_start
            perf_next_vert_start = time.time()
            (next_iteration_vert, next_iteration_edge) = self.__next_vert_and_edge(walker)
            next_iteration_edge = walker.random_non_traversed_edge_from_vertex(next_iteration_vert)
            self.__perf_metric_next_vert_pick_s += time.time() - perf_next_vert_start
            if next_iteration_edge == None or next_iteration_vert == None:
                if random.random() < 0.1:
                    return
            # if there is a continuation, continue walk
            walker.start(next_iteration_edge, next_iteration_vert)

    def __next_vert_and_edge(self, walker):
        # Try to pick starting vertex and edge for next walk.
        # If current path ends on itself, one of its corners is always a next path start.
        # Open edges (random edge from vertex with only 1 traversed edge) are always processed.
        # The rest depends on random with the following priority:
        # - vertices with 2 connected traversed edges at corners - TODO
        # - vertices with 2 connected traversed edges
        # - non-traversed vertices
        # For picked vert try to pick a non-traversed edge starting from it

        # Walked back to current path. This often looks ugly and "incomplete",
        # in such case corner verts of current path should be prioritised for next iteration
        current_path_loop_corners = walker.current_path_loop_until_current_vertex(corners_only = True)
        if current_path_loop_corners != None and len(current_path_loop_corners) > 0:
            next_iteration_vert = random.choice(current_path_loop_corners)
            next_iteration_edge = walker.random_non_traversed_edge_from_vertex(
                next_iteration_vert)
            if next_iteration_edge != None:
                return (next_iteration_vert, next_iteration_edge)

        next_iteration_vert = walker.first_open_vert()
        next_iteration_edge = walker.random_non_traversed_edge_from_vertex(
            next_iteration_vert)
        if next_iteration_edge != None:
            return (next_iteration_vert, next_iteration_edge)
        # try random pick: vertices with 2 connected traversed edges
        if random.random() < 0.5:
            next_iteration_vert = walker.random_bi_connected_vert()
            next_iteration_edge = walker.random_non_traversed_edge_from_vertex(
                next_iteration_vert)
            return (next_iteration_vert, next_iteration_edge)
        if random.random() < 0.5:
            next_iteration_vert = walker.random_non_traversed_vert()
            next_iteration_edge = walker.random_non_traversed_edge_from_vertex(
                next_iteration_vert)
            return (next_iteration_vert, next_iteration_edge)
        return (None, None)


    def __calc_default_panel_line_bevel_offset(self, context):
        mesh = context.edit_object.data
        bm = bmesh.from_edit_mesh(mesh)
        # 5% of average edge length
        return reduce(lambda a, b: a + b, map(lambda e: e.calc_length(), bm.edges)) / len(bm.edges) * 0.05


    def __cut_corners(self, bm, traversed_edges):
        affected_verts = set()
        result = list()
        for face in bm.faces:
            if len(face.edges) != 4:
                continue
            traversed_cnt = sum(1 for e in face.edges if e in traversed_edges)
            if traversed_cnt != 2:
                continue
            for i in range(0, 4):
                cur_edge = face.edges[i]
                next_edge = face.edges[(i + 1) % 4]
                if cur_edge in traversed_edges and next_edge in traversed_edges:
                    vert = shared_vert(cur_edge, next_edge)
                    if vert in affected_verts:
                        continue
                    # need to connect to only 2 traversed edges (effectively being the edges of current face)
                    # can't be boundary, thus must have 4 link loops
                    traversed_link_edges = list(
                        e for e in vert.link_edges if e in traversed_edges)
                    if len(traversed_link_edges) == 2 and len(vert.link_loops) == 4:
                        cur_other_vert = cur_edge.other_vert(vert)
                        next_other_vert = next_edge.other_vert(vert)
                        if cur_other_vert in affected_verts or next_other_vert in affected_verts:
                            continue
                        if random.random() < self.bevel_panel_corners_chance:
                            vert.co = (cur_other_vert.co +
                                    next_other_vert.co) * 0.5
                            affected_verts.add(vert)
                            affected_verts.add(cur_other_vert)
                            affected_verts.add(next_other_vert)
                            #print("vert for cut:", vert)
        return result

    def __cut_lines(self):
        perf_start_s = time.time()
        # TODO: if I continue to use these ops I need to clear selection before exec
        # TODO: maybe use bmesh.ops.bevel+inset_region?
        bpy.ops.mesh.bevel(
            offset_type='OFFSET',
            offset=self.panel_line_bevel_offset,
            segments=1,
            profile=0.5,
            clamp_overlap=self.bevel_clamp_overlap,
            loop_slide=False,
            material=-1
        )

        # TODO: this currently extrudes the faces which have all edges selected,
        # including those which are not a result of bevel operator (but this is ok if edit mode is FACES)
        bpy.ops.mesh.inset(
            use_boundary=True,
            use_even_offset=True,
            use_relative_offset=False,
            use_edge_rail=False,
            # TODO: instead of thickness it will be better to scale edges in panel insets
            thickness=self.panel_line_bevel_offset*self.inset_thickness_factor,
            depth=-self.inset_depth,
            use_outset=False,
            use_select_inset=False,
            use_individual=False,
            use_interpolate=True,
            release_confirm=False
        )
        self.__perf_metric_cut_lines_s = time.time() - perf_start_s


# TODO: move away
def shared_vert(e1, e2):
    for v1 in e1.verts:
        for v2 in e2.verts:
            if v1 == v2:
                return v1
    return None


def menu_func(self, context):
    self.layout.operator(SimplePanels.bl_idname)


def register():
    bpy.utils.register_class(SimplePanels)
    # Adds the new operator to an existing menu.
    bpy.types.VIEW3D_MT_edit_mesh.append(menu_func)


def unregister():
    bpy.utils.unregister_class(SimplePanels)
    bpy.types.VIEW3D_MT_edit_mesh.remove(menu_func)


# This allows you to run the script directly from Blender's Text editor
# to test the add-on without having to install it.
# Solutions for duplicate menu items: https://blender.stackexchange.com/questions/3394/adding-custom-menu-run-script-button-causes-duplicate
if __name__ == "__main__":
    register()

bl_info = {
    "name": "Simple panels",
    "blender": (2, 80, 0),
    "category": "Object",
}

if "bpy" in locals():
    import importlib
    if "edge_walker" in locals():
        importlib.reload(edge_walker)

import bpy

from . import (
    edge_walker
)
import bmesh
import random
import time
from functools import reduce

class SimplePanels(bpy.types.Operator):
    """Simple panel generator"""      # Use this as a tooltip for menu items and buttons.
    bl_idname = "object.simple_panels"        # Unique identifier for buttons and menu items to reference.
    bl_label = "Simple panels"         # Display name in the interface.
    bl_options = {'REGISTER', 'UNDO'}  # Enable undo for the operator.
    seed: bpy.props.IntProperty(name="Seed", default=1, min=1, max=999)
    forward_chance: bpy.props.FloatProperty(
        name="Forward walk", default=0.65, min=0, max=1)
    panel_line_bevel_offset: bpy.props.FloatProperty(
        name="Line width", default=0.01, min=0)
    inset_thickness_factor: bpy.props.FloatProperty(
        name="Line extrude width", default=0, min=0, max=1, subtype="FACTOR")
    inset_depth: bpy.props.FloatProperty(
        name="Line depth", default=0.1)
    bevel_panel_corners: bpy.props.BoolProperty(
        name="Bevel panel corners", default=False)
    bevel_panel_corners_chance: bpy.props.FloatProperty(
        name="Bevel corner chance", default=0.75, min=0, max=1)
    bevel_clamp_overlap: bpy.props.BoolProperty(
        name="Bevel clamp overlap", default=False)

    @classmethod
    def poll(cls, context):
        return context.edit_object is not None

    def invoke(self, context, event):
        self.panel_line_bevel_offset = self.__calc_default_panel_line_bevel_offset(context)
        self.inset_depth = self.panel_line_bevel_offset
        return self.execute(context)

    def cancel(self, context):
        self.report({'INFO'}, 'Cancel')

    # execute() is called when running the operator.
    def execute(self, context):

        self.report(
            {'INFO'}, 'Fwd chance: %.2f;  bevel corners: %s; seed: %d' %
            (
                self.forward_chance,
                self.bevel_panel_corners,
                self.seed
            )
        )
        
        perf_total_start = time.time()
        self.__perf_metric_cut_corners_s = 0;
        self.__perf_metric_walk_s = 0;
        self.__perf_metric_next_vert_pick_s = 0;
        self.__perf_metric_cut_lines_s = 0;
        random.seed(self.seed)
        mesh = context.edit_object.data
        bm = bmesh.from_edit_mesh(mesh)
        bm.verts.ensure_lookup_table()
        bm.edges.ensure_lookup_table()

        start_edge = random.choice(bm.edges)
        start_vert = random.choice(start_edge.verts)

        walker = edge_walker.EdgeWalker(bm)
        walker.start(start_edge, start_vert)

        self.__walk(walker)

        if self.bevel_panel_corners:
            perf_cut_corners_start = time.time()
            self.__cut_corners(bm, walker.traversed_edges)
            self.__perf_metric_cut_corners_s = time.time() - perf_cut_corners_start

        # TODO: for now just select the edges
        for e in walker.traversed_edges:
            e.select = True
        bm.select_flush_mode()

        self.__cut_lines()

        bmesh.update_edit_mesh(mesh)

        perf_total_s = time.time() - perf_total_start
        self.report(
            {'INFO'},
            "Traversed edges: %d; has open vert: %s. perf_cut_corners=%d%%; perf_walk=%d%%; perf_cut=%d%%; perf_pick=%d%%" %
            (
                len(walker.traversed_edges),
                walker.first_open_vert() != None,
                self.__perf_metric_cut_corners_s / perf_total_s * 100,
                self.__perf_metric_walk_s / perf_total_s * 100,
                self.__perf_metric_cut_lines_s / perf_total_s * 100,
                self.__perf_metric_next_vert_pick_s / perf_total_s * 100
            )
        )

        # Lets Blender know the operator finished successfully.
        return {'FINISHED'}

    def __walk(self, walker):
        while True:
            perf_walk_start = time.time()
            while walker.is_valid() and not walker.ends_at_traversed_vertex():
                if random.random() < self.forward_chance:
                    walker.forward()
                else:
                    walker.turn()
            self.__perf_metric_walk_s += time.time() - perf_walk_start
            perf_next_vert_start = time.time()
            (next_iteration_vert, next_iteration_edge) = self.__next_vert_and_edge(walker)
            next_iteration_edge = walker.random_non_traversed_edge_from_vertex(next_iteration_vert)
            self.__perf_metric_next_vert_pick_s += time.time() - perf_next_vert_start
            if next_iteration_edge == None or next_iteration_vert == None:
                if random.random() < 0.1:
                    return
            # if there is a continuation, continue walk
            walker.start(next_iteration_edge, next_iteration_vert)

    def __next_vert_and_edge(self, walker):
        # Try to pick starting vertex and edge for next walk.
        # If current path ends on itself, one of its corners is always a next path start.
        # Open edges (random edge from vertex with only 1 traversed edge) are always processed.
        # The rest depends on random with the following priority:
        # - vertices with 2 connected traversed edges at corners - TODO
        # - vertices with 2 connected traversed edges
        # - non-traversed vertices
        # For picked vert try to pick a non-traversed edge starting from it

        # Walked back to current path. This often looks ugly and "incomplete",
        # in such case corner verts of current path should be prioritised for next iteration
        current_path_loop_corners = walker.current_path_loop_until_current_vertex(corners_only = True)
        if current_path_loop_corners != None and len(current_path_loop_corners) > 0:
            next_iteration_vert = random.choice(current_path_loop_corners)
            next_iteration_edge = walker.random_non_traversed_edge_from_vertex(
                next_iteration_vert)
            if next_iteration_edge != None:
                return (next_iteration_vert, next_iteration_edge)

        next_iteration_vert = walker.first_open_vert()
        next_iteration_edge = walker.random_non_traversed_edge_from_vertex(
            next_iteration_vert)
        if next_iteration_edge != None:
            return (next_iteration_vert, next_iteration_edge)
        # try random pick: vertices with 2 connected traversed edges
        if random.random() < 0.5:
            next_iteration_vert = walker.random_bi_connected_vert()
            next_iteration_edge = walker.random_non_traversed_edge_from_vertex(
                next_iteration_vert)
            return (next_iteration_vert, next_iteration_edge)
        if random.random() < 0.5:
            next_iteration_vert = walker.random_non_traversed_vert()
            next_iteration_edge = walker.random_non_traversed_edge_from_vertex(
                next_iteration_vert)
            return (next_iteration_vert, next_iteration_edge)
        return (None, None)


    def __calc_default_panel_line_bevel_offset(self, context):
        mesh = context.edit_object.data
        bm = bmesh.from_edit_mesh(mesh)
        # 5% of average edge length
        return reduce(lambda a, b: a + b, map(lambda e: e.calc_length(), bm.edges)) / len(bm.edges) * 0.05


    def __cut_corners(self, bm, traversed_edges):
        affected_verts = set()
        result = list()
        for face in bm.faces:
            if len(face.edges) != 4:
                continue
            traversed_cnt = sum(1 for e in face.edges if e in traversed_edges)
            if traversed_cnt != 2:
                continue
            for i in range(0, 4):
                cur_edge = face.edges[i]
                next_edge = face.edges[(i + 1) % 4]
                if cur_edge in traversed_edges and next_edge in traversed_edges:
                    vert = shared_vert(cur_edge, next_edge)
                    if vert in affected_verts:
                        continue
                    # need to connect to only 2 traversed edges (effectively being the edges of current face)
                    # can't be boundary, thus must have 4 link loops
                    traversed_link_edges = list(
                        e for e in vert.link_edges if e in traversed_edges)
                    if len(traversed_link_edges) == 2 and len(vert.link_loops) == 4:
                        cur_other_vert = cur_edge.other_vert(vert)
                        next_other_vert = next_edge.other_vert(vert)
                        if cur_other_vert in affected_verts or next_other_vert in affected_verts:
                            continue
                        if random.random() < self.bevel_panel_corners_chance:
                            vert.co = (cur_other_vert.co +
                                    next_other_vert.co) * 0.5
                            affected_verts.add(vert)
                            affected_verts.add(cur_other_vert)
                            affected_verts.add(next_other_vert)
                            #print("vert for cut:", vert)
        return result

    def __cut_lines(self):
        perf_start_s = time.time()
        # TODO: if I continue to use these ops I need to clear selection before exec
        # TODO: maybe use bmesh.ops.bevel+inset_region?
        bpy.ops.mesh.bevel(
            offset_type='OFFSET',
            offset=self.panel_line_bevel_offset,
            segments=1,
            profile=0.5,
            clamp_overlap=self.bevel_clamp_overlap,
            loop_slide=False,
            material=-1
        )

        # TODO: this currently extrudes the faces which have all edges selected,
        # including those which are not a result of bevel operator (but this is ok if edit mode is FACES)
        bpy.ops.mesh.inset(
            use_boundary=True,
            use_even_offset=True,
            use_relative_offset=False,
            use_edge_rail=False,
            # TODO: instead of thickness it will be better to scale edges in panel insets
            thickness=self.panel_line_bevel_offset*self.inset_thickness_factor,
            depth=-self.inset_depth,
            use_outset=False,
            use_select_inset=False,
            use_individual=False,
            use_interpolate=True,
            release_confirm=False
        )
        self.__perf_metric_cut_lines_s = time.time() - perf_start_s


# TODO: move away
def shared_vert(e1, e2):
    for v1 in e1.verts:
        for v2 in e2.verts:
            if v1 == v2:
                return v1
    return None


def menu_func(self, context):
    self.layout.operator(SimplePanels.bl_idname)


def register():
    bpy.utils.register_class(SimplePanels)
    # Adds the new operator to an existing menu.
    bpy.types.VIEW3D_MT_edit_mesh.append(menu_func)


def unregister():
    bpy.utils.unregister_class(SimplePanels)
    bpy.types.VIEW3D_MT_edit_mesh.remove(menu_func)


# This allows you to run the script directly from Blender's Text editor
# to test the add-on without having to install it.
# Solutions for duplicate menu items: https://blender.stackexchange.com/questions/3394/adding-custom-menu-run-script-button-causes-duplicate
if __name__ == "__main__":
    register()

Simple panel generator # Use this as a tooltip for menu items and buttons. # Unique identifier for buttons and menu items to reference. # Display name in the interface. # Enable undo for the operator. # execute() is called when running the operator. # TODO: for now just select the edges # Lets Blender know the operator finished successfully. # if there is a continuation, continue walk # Try to pick starting vertex and edge for next walk. # If current path ends on itself, one of its corners is always a next path start. # Open edges (random edge from vertex with only 1 traversed edge) are always processed. # The rest depends on random with the following priority: # - vertices with 2 connected traversed edges at corners - TODO # - vertices with 2 connected traversed edges # - non-traversed vertices # For picked vert try to pick a non-traversed edge starting from it # Walked back to current path. This often looks ugly and "incomplete", # in such case corner verts of current path should be prioritised for next iteration # try random pick: vertices with 2 connected traversed edges # 5% of average edge length # need to connect to only 2 traversed edges (effectively being the edges of current face) # can't be boundary, thus must have 4 link loops #print("vert for cut:", vert) # TODO: if I continue to use these ops I need to clear selection before exec # TODO: maybe use bmesh.ops.bevel+inset_region? # TODO: this currently extrudes the faces which have all edges selected, # including those which are not a result of bevel operator (but this is ok if edit mode is FACES) # TODO: instead of thickness it will be better to scale edges in panel insets # TODO: move away # Adds the new operator to an existing menu. # This allows you to run the script directly from Blender's Text editor # to test the add-on without having to install it. # Solutions for duplicate menu items: https://blender.stackexchange.com/questions/3394/adding-custom-menu-run-script-button-causes-duplicate