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

# Repeats extrusion + rotation + scale for one or more faces
# Original code by liero
# Update by Jimmy Hazevoet 03/2017 for Blender 2.79
# normal rotation, probability, scaled offset, object coords, initial and per step noise


bl_info = {
    "name": "MExtrude Plus1",
    "author": "liero, Jimmy Hazevoet",
    "version": (1, 3, 0),
    "blender": (2, 77, 0),
    "location": "View3D > Tool Shelf",
    "description": "Repeat extrusions from faces to create organic shapes",
    "warning": "",
    "doc_url": "",
    "category": "Mesh",
}


import bpy
import bmesh
import random
from bpy.types import Operator
from random import gauss
from math import radians
from mathutils import (
        Euler, Vector,
        )
from bpy.props import (
        FloatProperty,
        IntProperty,
        BoolProperty,
        )


def gloc(self, r):
    return Vector((self.offx, self.offy, self.offz))


def vloc(self, r):
    random.seed(self.ran + r)
    return self.off * (1 + gauss(0, self.var1 / 3))


def nrot(self, n):
    return Euler((radians(self.nrotx) * n[0],
                  radians(self.nroty) * n[1],
                  radians(self.nrotz) * n[2]), 'XYZ')


def vrot(self, r):
    random.seed(self.ran + r)
    return Euler((radians(self.rotx) + gauss(0, self.var2 / 3),
                  radians(self.roty) + gauss(0, self.var2 / 3),
                  radians(self.rotz) + gauss(0, self.var2 / 3)), 'XYZ')


def vsca(self, r):
    random.seed(self.ran + r)
    return self.sca * (1 + gauss(0, self.var3 / 3))


class MExtrude(Operator):
    bl_idname = "object.mextrude"
    bl_label = "Multi Extrude"
    bl_description = ("Extrude selected Faces with Rotation,\n"
                      "Scaling, Variation, Randomization")
    bl_options = {"REGISTER", "UNDO", "PRESET"}

    off: FloatProperty(
            name="Offset",
            soft_min=0.001, soft_max=10,
            min=-100, max=100,
            default=1.0,
            description="Translation"
            )
    offx: FloatProperty(
            name="Loc X",
            soft_min=-10.0, soft_max=10.0,
            min=-100.0, max=100.0,
            default=0.0,
            description="Global Translation X"
            )
    offy: FloatProperty(
            name="Loc Y",
            soft_min=-10.0, soft_max=10.0,
            min=-100.0, max=100.0,
            default=0.0,
            description="Global Translation Y"
            )
    offz: FloatProperty(
            name="Loc Z",
            soft_min=-10.0, soft_max=10.0,
            min=-100.0, max=100.0,
            default=0.0,
            description="Global Translation Z"
            )
    rotx: FloatProperty(
            name="Rot X",
            min=-85, max=85,
            soft_min=-30, soft_max=30,
            default=0,
            description="X Rotation"
            )
    roty: FloatProperty(
            name="Rot Y",
            min=-85, max=85,
            soft_min=-30,
            soft_max=30,
            default=0,
            description="Y Rotation"
            )
    rotz: FloatProperty(
            name="Rot Z",
            min=-85, max=85,
            soft_min=-30, soft_max=30,
            default=-0,
            description="Z Rotation"
            )
    nrotx: FloatProperty(
            name="N Rot X",
            min=-85, max=85,
            soft_min=-30, soft_max=30,
            default=0,
            description="Normal X Rotation"
            )
    nroty: FloatProperty(
            name="N Rot Y",
            min=-85, max=85,
            soft_min=-30, soft_max=30,
            default=0,
            description="Normal Y Rotation"
            )
    nrotz: FloatProperty(
            name="N Rot Z",
            min=-85, max=85,
            soft_min=-30, soft_max=30,
            default=-0,
            description="Normal Z Rotation"
            )
    sca: FloatProperty(
            name="Scale",
            min=0.01, max=10,
            soft_min=0.5, soft_max=1.5,
            default=1.0,
            description="Scaling of the selected faces after extrusion"
            )
    var1: FloatProperty(
            name="Offset Var", min=-10, max=10,
            soft_min=-1, soft_max=1,
            default=0,
            description="Offset variation"
            )
    var2: FloatProperty(
            name="Rotation Var",
            min=-10, max=10,
            soft_min=-1, soft_max=1,
            default=0,
            description="Rotation variation"
            )
    var3: FloatProperty(
            name="Scale Noise",
            min=-10, max=10,
            soft_min=-1, soft_max=1,
            default=0,
            description="Scaling noise"
            )
    var4: IntProperty(
            name="Probability",
            min=0, max=100,
            default=100,
            description="Probability, chance of extruding a face"
            )
    num: IntProperty(
            name="Repeat",
            min=1, max=500,
            soft_max=100,
            default=5,
            description="Repetitions"
            )
    ran: IntProperty(
            name="Seed",
            min=-9999, max=9999,
            default=0,
            description="Seed to feed random values"
            )
    opt1: BoolProperty(
            name="Polygon coordinates",
            default=True,
            description="Polygon coordinates, Object coordinates"
            )
    opt2: BoolProperty(
            name="Proportional offset",
            default=False,
            description="Scale * Offset"
            )
    opt3: BoolProperty(
            name="Per step rotation noise",
            default=False,
            description="Per step rotation noise, Initial rotation noise"
            )
    opt4: BoolProperty(
            name="Per step scale noise",
            default=False,
            description="Per step scale noise, Initial scale noise"
            )

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

    def draw(self, context):
        layout = self.layout
        col = layout.column(align=True)
        col.label(text="Transformations:")
        col.prop(self, "off", slider=True)
        col.prop(self, "offx", slider=True)
        col.prop(self, "offy", slider=True)
        col.prop(self, "offz", slider=True)

        col = layout.column(align=True)
        col.prop(self, "rotx", slider=True)
        col.prop(self, "roty", slider=True)
        col.prop(self, "rotz", slider=True)
        col.prop(self, "nrotx", slider=True)
        col.prop(self, "nroty", slider=True)
        col.prop(self, "nrotz", slider=True)
        col = layout.column(align=True)
        col.prop(self, "sca", slider=True)

        col = layout.column(align=True)
        col.label(text="Variation settings:")
        col.prop(self, "var1", slider=True)
        col.prop(self, "var2", slider=True)
        col.prop(self, "var3", slider=True)
        col.prop(self, "var4", slider=True)
        col.prop(self, "ran")
        col = layout.column(align=False)
        col.prop(self, 'num')

        col = layout.column(align=True)
        col.label(text="Options:")
        col.prop(self, "opt1")
        col.prop(self, "opt2")
        col.prop(self, "opt3")
        col.prop(self, "opt4")

    def execute(self, context):
        obj = bpy.context.object
        om = obj.mode
        bpy.context.tool_settings.mesh_select_mode = [False, False, True]
        origin = Vector([0.0, 0.0, 0.0])

        # bmesh operations
        bpy.ops.object.mode_set()
        bm = bmesh.new()
        bm.from_mesh(obj.data)
        sel = [f for f in bm.faces if f.select]

        after = []

        # faces loop
        for i, of in enumerate(sel):
            nro = nrot(self, of.normal)
            off = vloc(self, i)
            loc = gloc(self, i)
            of.normal_update()

            # initial rotation noise
            if self.opt3 is False:
                rot = vrot(self, i)
            # initial scale noise
            if self.opt4 is False:
                s = vsca(self, i)

            # extrusion loop
            for r in range(self.num):
                # random probability % for extrusions
                if self.var4 > int(random.random() * 100):
                    nf = of.copy()
                    nf.normal_update()
                    no = nf.normal.copy()

                    # face/obj coördinates
                    if self.opt1 is True:
                        ce = nf.calc_center_bounds()
                    else:
                        ce = origin

                    # per step rotation noise
                    if self.opt3 is True:
                        rot = vrot(self, i + r)
                    # per step scale noise
                    if self.opt4 is True:
                        s = vsca(self, i + r)

                    # proportional, scale * offset
                    if self.opt2 is True:
                        off = s * off

                    for v in nf.verts:
                        v.co -= ce
                        v.co.rotate(nro)
                        v.co.rotate(rot)
                        v.co += ce + loc + no * off
                        v.co = v.co.lerp(ce, 1 - s)

                    # extrude code from TrumanBlending
                    for a, b in zip(of.loops, nf.loops):
                        sf = bm.faces.new((a.vert, a.link_loop_next.vert,
                                           b.link_loop_next.vert, b.vert))
                        sf.normal_update()
                    bm.faces.remove(of)
                    of = nf

            after.append(of)

        for v in bm.verts:
            v.select = False
        for e in bm.edges:
            e.select = False

        for f in after:
            if f not in sel:
                f.select = True
            else:
                f.select = False

        bm.to_mesh(obj.data)
        obj.data.update()

        # restore user settings
        bpy.ops.object.mode_set(mode=om)

        if not len(sel):
            self.report({"WARNING"},
                        "No suitable Face selection found. Operation cancelled")
            return {'CANCELLED'}

        return {'FINISHED'}


def register():
    bpy.utils.register_module(__name__)


def unregister():
    bpy.utils.unregister_module(__name__)


if __name__ == '__main__':
    register()
