# SPDX-License-Identifier: GPL-2.0-or-later


bl_info = {
    "name": "SpiroFit, Bounce Spline, and Catenary",
    "author": "Antonio Osprite, Liero, Atom, Jimmy Hazevoet",
    "version": (0, 2, 2),
    "blender": (2, 80, 0),
    "location": "Add > Curve > Knots",
    "description": "SpiroFit, BounceSpline and Catenary adds "
                   "splines to selected mesh or objects",
    "warning": "",
    "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/extra_objects.html",
    "category": "Object",
}

import bpy
from bpy.types import (
        Operator,
        Panel,
        )
from bpy.props import (
        BoolProperty,
        EnumProperty,
        FloatProperty,
        IntProperty,
        StringProperty,
        )
from mathutils import (
        Matrix,
        Vector,
        )
from math import (
        sin, cos,
        pi, sqrt,
        pow, radians
        )
import random as r


# ------------------------------------------------------------
# "Build a spiral that fit the active object"
# Spirofit, original blender 2.45 script by: Antonio Osprite
# http://www.kino3d.com/forum/viewtopic.php?t=5374
# ------------------------------------------------------------
def distance(v1, v2):
    d = (Vector(v1) - Vector(v2)).length
    return d


def spiral_point(step, radius, z_coord, spires, waves, wave_iscale, rndm):
    x = radius * cos(spires * step) + (r.random() - 0.5) * rndm
    y = radius * sin(spires * step) + (r.random() - 0.5) * rndm
    z = z_coord + (cos(waves * step * pi) * wave_iscale) + (r.random() - 0.5) * rndm
    return [x, y, z]


def spirofit_spline(obj,
            spire_resolution=4,
            spires=4,
            offset=0.0,
            waves=0,
            wave_iscale=0.0,
            rndm_spire=0.0,
            direction=False,
            map_method='RAYCAST'
            ):

    points = []
    bb = obj.bound_box
    bb_xmin = min([v[0] for v in bb])
    bb_ymin = min([v[1] for v in bb])
    bb_zmin = min([v[2] for v in bb])
    bb_xmax = max([v[0] for v in bb])
    bb_ymax = max([v[1] for v in bb])
    bb_zmax = max([v[2] for v in bb])

    radius = distance([bb_xmax, bb_ymax, bb_zmin], [bb_xmin, bb_ymin, bb_zmin]) / 2.0
    height = bb_zmax - bb_zmin
    cx = (bb_xmax + bb_xmin) / 2.0
    cy = (bb_ymax + bb_ymin) / 2.0
    steps = spires * spire_resolution

    for i in range(steps + 1):
        t = bb_zmin + (2 * pi / steps) * i
        z = bb_zmin + (float(height) / steps) * i
        if direction:
            t = -t
        cp = spiral_point(t, radius, z, spires, waves, wave_iscale, rndm_spire)

        if map_method == 'RAYCAST':
            success, hit, nor, index = obj.ray_cast(Vector(cp), (Vector([cx, cy, z]) - Vector(cp)))
            if success:
                points.append((hit + offset * nor))

        elif map_method == 'CLOSESTPOINT':
            success, hit, nor, index = obj.closest_point_on_mesh(cp)
            if success:
                points.append((hit + offset * nor))

    return points


class SpiroFitSpline(Operator):
    bl_idname = "object.add_spirofit_spline"
    bl_label = "SpiroFit"
    bl_description = "Wrap selected mesh in a spiral"
    bl_options = {'REGISTER', 'UNDO', 'PRESET'}

    map_method : EnumProperty(
            name="Mapping",
            default='RAYCAST',
            description="Mapping method",
            items=[('RAYCAST', 'Ray cast', 'Ray casting'),
                   ('CLOSESTPOINT', 'Closest point', 'Closest point on mesh')]
            )
    direction : BoolProperty(
            name="Direction",
            description="Spire direction",
            default=False
            )
    spire_resolution : IntProperty(
            name="Spire Resolution",
            default=8,
            min=3,
            max=1024,
            soft_max=128,
            description="Number of steps for one turn"
            )
    spires : IntProperty(
            name="Spires",
            default=4,
            min=1,
            max=1024,
            soft_max=128,
            description="Number of turns"
            )
    offset : FloatProperty(
            name="Offset",
            default=0.0,
            precision=3,
            description="Use normal direction to offset spline"
            )
    waves : IntProperty(
            name="Wave",
            default=0,
            min=0,
            description="Wave amount"
            )
    wave_iscale : FloatProperty(
            name="Wave Intensity",
            default=0.0,
            min=0.0,
            precision=3,
            description="Wave intensity scale"
            )
    rndm_spire : FloatProperty(
            name="Randomise",
            default=0.0,
            min=0.0,
            precision=3,
            description="Randomise spire"
            )
    spline_name : StringProperty(
            name="Name",
            default="SpiroFit"
            )
    spline_type : EnumProperty(
            name="Spline",
            default='BEZIER',
            description="Spline type",
            items=[('POLY', 'Poly', 'Poly spline'),
                   ('BEZIER', 'Bezier', 'Bezier spline')]
            )
    resolution_u : IntProperty(
            name="Resolution U",
            default=12,
            min=0,
            max=64,
            description="Curve resolution u"
            )
    bevel : FloatProperty(
            name="Bevel Radius",
            default=0.0,
            min=0.0,
            precision=3,
            description="Bevel depth"
            )
    bevel_res : IntProperty(
            name="Bevel Resolution",
            default=0,
            min=0,
            max=32,
            description="Bevel resolution"
            )
    extrude : FloatProperty(
            name="Extrude",
            default=0.0,
            min=0.0,
            precision=3,
            description="Extrude amount"
            )
    twist_mode : EnumProperty(
            name="Twisting",
            default='MINIMUM',
            description="Twist method, type of tilt calculation",
            items=[('Z_UP', "Z-Up", 'Z Up'),
                   ('MINIMUM', "Minimum", 'Minimum'),
                   ('TANGENT', "Tangent", 'Tangent')]
            )
    twist_smooth : FloatProperty(
            name="Smooth",
            default=0.0,
            min=0.0,
            precision=3,
            description="Twist smoothing amount for tangents"
            )
    tilt : FloatProperty(
            name="Tilt",
            default=0.0,
            precision=3,
            description="Spline handle tilt"
            )
    random_radius : FloatProperty(
            name="Randomise",
            default=0.0,
            min=0.0,
            precision=3,
            description="Randomise radius of spline controlpoints"
            )
    random_seed : IntProperty(
            name="Random Seed",
            default=1,
            min=0,
            description="Random seed number"
            )
    origin_to_start : BoolProperty(
            name="Origin at Start",
            description="Set origin at first point of spline",
            default=False
            )
    refresh : BoolProperty(
            name="Refresh",
            description="Refresh spline",
            default=False
            )
    auto_refresh : BoolProperty(
            name="Auto",
            description="Auto refresh spline",
            default=True
            )

    def draw(self, context):
        layout = self.layout
        col = layout.column(align=True)
        row = col.row(align=True)

        if self.auto_refresh is False:
            self.refresh = False
        elif self.auto_refresh is True:
            self.refresh = True

        row.prop(self, "auto_refresh", toggle=True, icon="AUTO", icon_only=True)
        row.prop(self, "refresh", toggle=True, icon="FILE_REFRESH", icon_only=True)
        row.operator("object.add_spirofit_spline", text="Add")
        row.prop(self, "origin_to_start", toggle=True, icon="CURVE_DATA", icon_only=True)

        col = layout.column(align=True)
        col.prop(self, "spline_name")
        col.separator()
        col.prop(self, "map_method")
        col.separator()
        col.prop(self, "spire_resolution")
        row = col.row(align=True).split(factor=0.9, align=True)
        row.prop(self, "spires")
        row.prop(self, "direction", toggle=True, text="", icon='ARROW_LEFTRIGHT')
        col.prop(self, "offset")
        col.prop(self, "waves")
        col.prop(self, "wave_iscale")
        col.prop(self, "rndm_spire")
        col.prop(self, "random_seed")
        draw_spline_settings(self)

    @classmethod
    def poll(self, context):
        ob = context.active_object
        return ((ob is not None) and
                (context.mode == 'OBJECT'))

    def invoke(self, context, event):
        self.refresh = True
        return self.execute(context)

    def execute(self, context):
        if not self.refresh:
            return {'PASS_THROUGH'}

        obj = context.active_object
        if obj.type != 'MESH':
            self.report({'WARNING'},
                        "Active Object is not a Mesh. Operation Cancelled")
            return {'CANCELLED'}

        bpy.ops.object.select_all(action='DESELECT')

        r.seed(self.random_seed)

        points = spirofit_spline(
                obj,
                self.spire_resolution,
                self.spires,
                self.offset,
                self.waves,
                self.wave_iscale,
                self.rndm_spire,
                self.direction,
                self.map_method
                )

        add_curve_object(
                points,
                obj.matrix_world,
                self.spline_name,
                self.spline_type,
                self.resolution_u,
                self.bevel,
                self.bevel_res,
                self.extrude,
                self.random_radius,
                self.twist_mode,
                self.twist_smooth,
                self.tilt
                )

        if self.origin_to_start is True:
            move_origin_to_start()

        if self.auto_refresh is False:
            self.refresh = False

        return {'FINISHED'}


# ------------------------------------------------------------
# Bounce spline / Fiber mesh
# Original script by Liero and Atom
# https://blenderartists.org/forum/showthread.php?331750-Fiber-Mesh-Emulation
# ------------------------------------------------------------
def noise(var=1):
    rand = Vector((r.gauss(0, 1), r.gauss(0, 1), r.gauss(0, 1)))
    vec = rand.normalized() * var
    return vec


def bounce_spline(obj,
            number=1000,
            ang_noise=0.25,
            offset=0.0,
            extra=50,
            active_face=False
            ):

    dist, points = 1000, []
    poly = obj.data.polygons

    if active_face:
        try:
            n = poly.active
        except:
            print("No active face selected")
            pass
    else:
        n = r.randint(0, len(poly) - 1)

    end = poly[n].normal.copy() * -1
    start = poly[n].center
    points.append(start + offset * end)

    for i in range(number):
        for ray in range(extra + 1):
            end += noise(ang_noise)
            try:
                hit, nor, index = obj.ray_cast(start, end * dist)[-3:]
            except:
                index = -1
            if index != -1:
                start = hit - nor / 10000
                end = end.reflect(nor).normalized()
                points.append(hit + offset * nor)
                break
        if index == -1:
            return points
    return points


class BounceSpline(Operator):
    bl_idname = "object.add_bounce_spline"
    bl_label = "Bounce Spline"
    bl_description = "Fill selected mesh with a spline"
    bl_options = {'REGISTER', 'UNDO', 'PRESET'}

    bounce_number : IntProperty(
            name="Bounces",
            default=1000,
            min=1,
            max=100000,
            soft_max=10000,
            description="Number of bounces"
            )
    ang_noise : FloatProperty(
            name="Angular Noise",
            default=0.25,
            min=0.0,
            precision=3,
            description="Add some noise to ray direction"
            )
    offset : FloatProperty(
            name="Offset",
            default=0.0,
            precision=3,
            description="Use normal direction to offset spline"
            )
    extra : IntProperty(
            name="Extra",
            default=50,
            min=0,
            max=1000,
            description="Number of extra tries if it fails to hit mesh"
            )
    active_face : BoolProperty(
            name="Active Face",
            default=False,
            description="Starts from active face or a random one"
            )
    spline_name : StringProperty(
            name="Name",
            default="BounceSpline"
            )
    spline_type : EnumProperty(
            name="Spline",
            default='BEZIER',
            description="Spline type",
            items=[('POLY', "Poly", "Poly spline"),
                   ('BEZIER', "Bezier", "Bezier spline")]
            )
    resolution_u : IntProperty(
            name="Resolution U",
            default=12,
            min=0,
            max=64,
            description="Curve resolution u"
            )
    bevel : FloatProperty(
            name="Bevel Radius",
            default=0.0,
            min=0.0,
            precision=3,
            description="Bevel depth"
            )
    bevel_res : IntProperty(
            name="Bevel Resolution",
            default=0,
            min=0,
            max=32,
            description="Bevel resolution"
            )
    extrude : FloatProperty(
            name="Extrude",
            default=0.0,
            min=0.0,
            precision=3,
            description="Extrude amount"
            )
    twist_mode : EnumProperty(
            name="Twisting",
            default='MINIMUM',
            description="Twist method, type of tilt calculation",
            items=[('Z_UP', "Z-Up", 'Z Up'),
                   ('MINIMUM', "Minimum", 'Minimum'),
                   ('TANGENT', "Tangent", 'Tangent')]
            )
    twist_smooth : FloatProperty(
            name="Smooth",
            default=0.0,
            min=0.0,
            precision=3,
            description="Twist smoothing amount for tangents"
            )
    tilt : FloatProperty(
            name="Tilt",
            default=0.0,
            precision=3,
            description="Spline handle tilt"
            )
    random_radius : FloatProperty(
            name="Randomise",
            default=0.0,
            min=0.0,
            precision=3,
            description="Randomise radius of spline controlpoints"
            )
    random_seed : IntProperty(
            name="Random Seed",
            default=1,
            min=0,
            description="Random seed number"
            )
    origin_to_start : BoolProperty(
            name="Origin at Start",
            description="Set origin at first point of spline",
            default=False
            )
    refresh : BoolProperty(
            name="Refresh",
            description="Refresh spline",
            default=False
            )
    auto_refresh : BoolProperty(
            name="Auto",
            description="Auto refresh spline",
            default=True
            )

    def draw(self, context):
        layout = self.layout
        col = layout.column(align=True)
        row = col.row(align=True)
        if self.auto_refresh is False:
            self.refresh = False
        elif self.auto_refresh is True:
            self.refresh = True

        row.prop(self, "auto_refresh", toggle=True, icon="AUTO", icon_only=True)
        row.prop(self, "refresh", toggle=True, icon="FILE_REFRESH", icon_only=True)
        row.operator("object.add_bounce_spline", text="Add")
        row.prop(self, "origin_to_start", toggle=True, icon="CURVE_DATA", icon_only=True)

        col = layout.column(align=True)
        col.prop(self, "spline_name")
        col.separator()
        col.prop(self, "bounce_number")
        row = col.row(align=True).split(factor=0.9, align=True)
        row.prop(self, "ang_noise")
        row.prop(self, "active_face", toggle=True, text="", icon="SNAP_FACE")
        col.prop(self, "offset")
        col.prop(self, "extra")
        col.prop(self, "random_seed")
        draw_spline_settings(self)

    @classmethod
    def poll(self, context):
        ob = context.active_object
        return ((ob is not None) and
                (context.mode == 'OBJECT'))

    def invoke(self, context, event):
        self.refresh = True
        return self.execute(context)

    def execute(self, context):
        if not self.refresh:
            return {'PASS_THROUGH'}

        obj = context.active_object
        if obj.type != 'MESH':
            return {'CANCELLED'}

        bpy.ops.object.select_all(action='DESELECT')

        r.seed(self.random_seed)

        points = bounce_spline(
                obj,
                self.bounce_number,
                self.ang_noise,
                self.offset,
                self.extra,
                self.active_face
                )

        add_curve_object(
                points,
                obj.matrix_world,
                self.spline_name,
                self.spline_type,
                self.resolution_u,
                self.bevel,
                self.bevel_res,
                self.extrude,
                self.random_radius,
                self.twist_mode,
                self.twist_smooth,
                self.tilt
                )

        if self.origin_to_start is True:
            move_origin_to_start()

        if self.auto_refresh is False:
            self.refresh = False

        return {'FINISHED'}


# ------------------------------------------------------------
# Hang Catenary curve between two selected objects
# ------------------------------------------------------------
def catenary_curve(
            start=[-2, 0, 2],
            end=[2, 0, 2],
            steps=24,
            a=2.0
            ):

    points = []
    lx = end[0] - start[0]
    ly = end[1] - start[1]
    lr = sqrt(pow(lx, 2) + pow(ly, 2))
    lv = lr / 2 - (end[2] - start[2]) * a / lr
    zv = start[2] - pow(lv, 2) / (2 * a)
    slx = lx / steps
    sly = ly / steps
    slr = lr / steps
    i = 0
    while i <= steps:
        x = start[0] + i * slx
        y = start[1] + i * sly
        z = zv + pow((i * slr) - lv, 2) / (2 * a)
        points.append([x, y, z])
        i += 1
    return points


class CatenaryCurve(Operator):
    bl_idname = "object.add_catenary_curve"
    bl_label = "Catenary"
    bl_description = "Hang a curve between two selected objects"
    bl_options = {'REGISTER', 'UNDO', 'PRESET'}

    steps : IntProperty(
            name="Steps",
            description="Resolution of the curve",
            default=24,
            min=2,
            max=1024,
            )
    var_a : FloatProperty(
            name="a",
            description="Catenary variable a",
            precision=3,
            default=2.0,
            min=0.01,
            max=100.0
            )
    spline_name : StringProperty(
            name="Name",
            default="Catenary"
            )
    spline_type : EnumProperty(
            name="Spline",
            default='BEZIER',
            description="Spline type",
            items=[('POLY', "Poly", "Poly spline"),
                   ('BEZIER', "Bezier", "Bezier spline")]
            )
    resolution_u : IntProperty(
            name="Resolution U",
            default=12,
            min=0,
            max=64,
            description="Curve resolution u"
            )
    bevel : FloatProperty(
            name="Bevel Radius",
            default=0.0,
            min=0.0,
            precision=3,
            description="Bevel depth"
            )
    bevel_res : IntProperty(
            name="Bevel Resolution",
            default=0,
            min=0,
            max=32,
            description="Bevel resolution"
            )
    extrude : FloatProperty(
            name="Extrude",
            default=0.0,
            min=0.0,
            precision=3,
            description="Extrude amount"
            )
    twist_mode : EnumProperty(
            name="Twisting",
            default='MINIMUM',
            description="Twist method, type of tilt calculation",
            items=[('Z_UP', "Z-Up", 'Z Up'),
                   ('MINIMUM', "Minimum", "Minimum"),
                   ('TANGENT', "Tangent", "Tangent")]
            )
    twist_smooth : FloatProperty(
            name="Smooth",
            default=0.0,
            min=0.0,
            precision=3,
            description="Twist smoothing amount for tangents"
            )
    tilt : FloatProperty(
            name="Tilt",
            default=0.0,
            precision=3,
            description="Spline handle tilt"
            )
    random_radius : FloatProperty(
            name="Randomise",
            default=0.0,
            min=0.0,
            precision=3,
            description="Randomise radius of spline controlpoints"
            )
    random_seed : IntProperty(
            name="Random Seed",
            default=1,
            min=0,
            description="Random seed number"
            )
    origin_to_start : BoolProperty(
            name="Origin at Start",
            description="Set origin at first point of spline",
            default=False
            )
    refresh : BoolProperty(
            name="Refresh",
            description="Refresh spline",
            default=False
            )
    auto_refresh : BoolProperty(
            name="Auto",
            description="Auto refresh spline",
            default=True
            )

    def draw(self, context):
        layout = self.layout
        col = layout.column(align=True)
        row = col.row(align=True)

        if self.auto_refresh is False:
            self.refresh = False
        elif self.auto_refresh is True:
            self.refresh = True

        row.prop(self, "auto_refresh", toggle=True, icon="AUTO", icon_only=True)
        row.prop(self, "refresh", toggle=True, icon="FILE_REFRESH", icon_only=True)
        row.operator("object.add_catenary_curve", text="Add")
        row.prop(self, "origin_to_start", toggle=True, icon="CURVE_DATA", icon_only=True)

        col = layout.column(align=True)
        col.prop(self, "spline_name")
        col.separator()
        col.prop(self, "steps")
        col.prop(self, "var_a")

        draw_spline_settings(self)
        col = layout.column(align=True)
        col.prop(self, "random_seed")

    @classmethod
    def poll(self, context):
        ob = context.active_object
        return ob is not None

    def invoke(self, context, event):
        self.refresh = True
        return self.execute(context)

    def execute(self, context):
        if not self.refresh:
            return {'PASS_THROUGH'}

        try:
            #ob1 = bpy.context.active_object
            #ob1.select = False
            ob1 = bpy.context.selected_objects[0]
            ob2 = bpy.context.selected_objects[1]

            start = ob1.location
            end = ob2.location
            if (start[0] == end[0]) and (start[1] == end[1]):
                self.report({"WARNING"},
                            "Objects have the same X, Y location. Operation Cancelled")

                return {'CANCELLED'}
        except:
            self.report({"WARNING"},
                        "Catenary could not be completed. Operation Cancelled")
            return {'CANCELLED'}

        bpy.ops.object.select_all(action='DESELECT')

        r.seed(self.random_seed)

        points = catenary_curve(
                start,
                end,
                self.steps,
                self.var_a
                )
        add_curve_object(
                points,
                Matrix(),
                self.spline_name,
                self.spline_type,
                self.resolution_u,
                self.bevel,
                self.bevel_res,
                self.extrude,
                self.random_radius,
                self.twist_mode,
                self.twist_smooth,
                self.tilt
                )

        if self.origin_to_start is True:
            move_origin_to_start()
        else:
            bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')

        if self.auto_refresh is False:
            self.refresh = False

        return {'FINISHED'}


# ------------------------------------------------------------
# Generate curve object from given points
# ------------------------------------------------------------
def add_curve_object(
            verts,
            matrix,
            spline_name="Spline",
            spline_type='BEZIER',
            resolution_u=12,
            bevel=0.0,
            bevel_resolution=0,
            extrude=0.0,
            spline_radius=0.0,
            twist_mode='MINIMUM',
            twist_smooth=0.0,
            tilt=0.0
            ):

    scene = bpy.context.scene
    vl = bpy.context.view_layer
    curve = bpy.data.curves.new(spline_name, 'CURVE')
    curve.dimensions = '3D'
    spline = curve.splines.new(spline_type)
    cur = bpy.data.objects.new(spline_name, curve)
    spline.radius_interpolation = 'BSPLINE'
    spline.tilt_interpolation = 'BSPLINE'

    if spline_type == 'BEZIER':
        spline.bezier_points.add(int(len(verts) - 1))
        for i in range(len(verts)):
            spline.bezier_points[i].co = verts[i]
            spline.bezier_points[i].handle_right_type = 'AUTO'
            spline.bezier_points[i].handle_left_type = 'AUTO'
            spline.bezier_points[i].radius += spline_radius * r.random()
            spline.bezier_points[i].tilt = radians(tilt)
    else:
        spline.points.add(int(len(verts) - 1))
        for i in range(len(verts)):
            spline.points[i].co = verts[i][0], verts[i][1], verts[i][2], 1

    scene.collection.objects.link(cur)
    cur.data.resolution_u = resolution_u
    cur.data.fill_mode = 'FULL'
    cur.data.bevel_depth = bevel
    cur.data.bevel_resolution = bevel_resolution
    cur.data.extrude = extrude
    cur.data.twist_mode = twist_mode
    cur.data.twist_smooth = twist_smooth
    cur.matrix_world = matrix
    cur.select_set(True)
    vl.objects.active = cur
    return


def move_origin_to_start():
    active = bpy.context.active_object
    spline = active.data.splines[0]

    if spline.type == 'BEZIER':
        start = active.matrix_world @ spline.bezier_points[0].co
    else:
        start = active.matrix_world @ spline.points[0].co
        start = start[:-1]

    cursor = bpy.context.scene.cursor.location.copy()
    bpy.context.scene.cursor.location = start
    bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
    bpy.context.scene.cursor.location = cursor


def draw_spline_settings(self):
    layout = self.layout
    col = layout.column(align=True)

    col.prop(self, "spline_type")
    col.separator()
    col.prop(self, "resolution_u")
    col.prop(self, "bevel")
    col.prop(self, "bevel_res")
    col.prop(self, "extrude")

    if self.spline_type == 'BEZIER':
        col.prop(self, "random_radius")
    col.separator()
    col.prop(self, "twist_mode")
    col.separator()

    if self.twist_mode == 'TANGENT':
        col.prop(self, "twist_smooth")

    if self.spline_type == 'BEZIER':
        col.prop(self, "tilt")


# ------------------------------------------------------------
# Register
# ------------------------------------------------------------
def register():
    bpy.utils.register_class(SpiroFitSpline)
    bpy.utils.register_class(BounceSpline)
    bpy.utils.register_class(CatenaryCurve)


def unregister():
    bpy.utils.unregister_class(SpiroFitSpline)
    bpy.utils.unregister_class(BounceSpline)
    bpy.utils.unregister_class(CatenaryCurve)


if __name__ == "__main__":
    register()
