OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / operators / modifiers / curve.py

288 lines · python ref:
import bpy
from math import radians
from mathutils import Vector, Matrix
from ... utility import addon


class Object():
    def __init__(self, obj:bpy.types.Object) -> None:
        self.name = obj.name

        loc, rot, sca = obj.matrix_world.decompose()

        eval = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())

        self.matrix_scaless = Matrix.Translation(loc) @ rot.to_matrix().to_4x4()
        scale = Matrix.Diagonal((*sca, 1))
        self.bounds = [scale @ Vector(v) for v in eval.bound_box]
        self.min_corner, self.max_corner = coordinates_to_diagonal(self.bounds)
        self.center = (self.min_corner + self.max_corner) /2


class Curve():
    _name = 'HOPS Curve'
    _axis_map = {
        'POS_X' : 0, # X+ direction
        'POS_Y' : 90, # Y+ direction
        'POS_Z' : 180, # Z+ direction
        'NEG_X' : -90, #  X- direction
        'NEG_Y' : 180, # Y- direction
        'NEG_Z' : -90, # -90 tilt Z- direction
    }

    def __init__(self, context, deform_axis='POS_Z', stretch=True) -> None:
        self.modifiers = []
        self.deform_axis = deform_axis
        self.type = 'NURBS'

        self.data = bpy.data.curves.new(self._name, 'CURVE')
        self.data.use_path = False
        self.data.use_stretch = stretch
        self.data.use_deform_bounds = True
        self.data.dimensions = '3D'

        self.object = bpy.data.objects.new(self._name, self.data)
        self.object.show_in_front = True

        context.collection.objects.link(self.object)

    def remove(self) -> None:
        bpy.data.objects.remove(self.object)
        bpy.data.curves.remove(self.data)

        for mod in self.modifies:
            mod.id_data.modifiers.remove(self.modifier)

        self.modifies.clear()

    def add_modifier(self, object):
        modifier = object.modifiers.new(type='CURVE', name=self._name)
        modifier.name = self._name
        modifier.object = self.object
        modifier.deform_axis = self.deform_axis

        self.modifiers.append(modifier)

    def create_spline(self, points: list, type='NURBS', resolution_u=36, order_u=3):
        self.type = type

        self.data.splines.clear()
        spline = self.data.splines.new(type)
        spline.use_endpoint_u = True
        count = len(points)
        tilt = radians(self._axis_map[self.deform_axis])
        spline.resolution_u = resolution_u

        if type == 'BEZIER':
            spline.bezier_points.add(count - 1)
            handle_offset = ((points[-1] - points[0]) / count) * 0.5

            for point, vec in zip(spline.bezier_points, points):
                point.co = vec
                point.radius = 1
                point.tilt = tilt

                point.handle_left_type = 'ALIGNED'
                point.handle_right_type = 'ALIGNED'
                point.handle_right = vec + handle_offset
                point.handle_left = vec - handle_offset

        else:
            spline.points.add(count - 1)
            spline.order_u = order_u

            for point, vec in zip(spline.points, points):
                point.co[0] = vec[0]
                point.co[1] = vec[1]
                point.co[2] = vec[2]
                point.co[3] = 1
                point.radius = 1
                point.tilt = tilt


DESC = '''Adds a curve + curve mod to shape

F9 for adjustment

'''

class HOPS_OT_MOD_Curve(bpy.types.Operator):
    bl_idname = 'hops.mod_curve'
    bl_label = 'Add a Curve modifier with Curve object'
    bl_options = {'REGISTER', 'UNDO', 'BLOCKING'}
    bl_description = DESC

    deform_axis: bpy.props.EnumProperty(
        name='Axis',
        description='Deform Axis',
        items=[
            ('POS_X', '+X', 'Deform +X axis'),
            ('POS_Y', '+Y', 'Deform +Y axis'),
            ('POS_Z', '+Z', 'Deform +Z axis'),
            ('NEG_X', '-X', 'Deform -X axis'),
            ('NEG_Y', '-Y', 'Deform -Y axis'),
            ('NEG_Z', '-Z', 'Deform -Z axis'),
            ],
        default='POS_Z')

    subdivisions: bpy.props.IntProperty(
        name='Subdivisions',
        description='Number of times the curve is subdivided',
        min=0,
        default=1
    )

    type: bpy.props.EnumProperty(
        name='Type',
        description='Spline type',
        items=[
            ('BEZIER', 'BEZIER', 'BEZIER'),
            ('NURBS', 'NURBS', 'NURBS'),
            ],
        default='BEZIER'
    )

    resolution: bpy.props.IntProperty(
        name='Resolution U',
        description='Curve subdivision per segment',
        min=1,
        default=36,
    )

    order: bpy.props.IntProperty(
        name='Order U',
        description='Order for NURBS curve in U direction. Higher number = smoother curve',
        min=2,
        max=6,
        default=3,
    )

    stretch: bpy.props.BoolProperty(
        name='Stretch',
        description='Stretch Object(s) to fit curve',
        default=True,
    )

    combined: bpy.props.BoolProperty(
        name='Combined',
        description='Create single curve to deform selection',
        default=False
    )

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

    def invoke(self, context, event):
        self.notify = lambda val, sub='': bpy.ops.hops.display_notification(info=val, subtext=sub) if addon.preference().ui.Hops_extra_info else lambda val, sub=None: None
        self.selection = [Object(o) for o in context.selected_objects if o.type == 'MESH']

        if not self.selection:
            msg = 'CANCELLED'
            sub = 'No selected Meshes'
            self.notify(val=msg, sub=sub)

            return {'CANCELLED'}
        return self.execute(context)

    def execute(self, context):
        if self.combined:
            bounds = [o.matrix_scaless @ v for o in self.selection for v in o.bounds]
            _min, _max = coordinates_to_diagonal(bounds)
            center = (_min + _max) / 2

            _min -= center
            _max -= center

            curve = Curve(context, stretch=self.stretch, deform_axis=self.deform_axis)
            axis = self.deform_axis[-1]
            index = 'XYZ'.index(axis)
            points = [_min, _max] if self.deform_axis.startswith('POS') else [_max, _min]

            for vec in points:
                for i in range (3):
                    if i != index:
                        vec[i] = 0

            curve.create_spline(subdivide_line(*points, times=self.subdivisions), type=self.type, resolution_u=self.resolution, order_u=self.order)
            curve.object.matrix_world.translation = center

            for Obj in self.selection:
                obj = bpy.data.objects[Obj.name]

                curve.add_modifier(obj)
        else:
            for Obj in self.selection:
                obj = bpy.data.objects[Obj.name]
                center = Obj.center
                _min = Obj.min_corner - Obj.center
                _max = Obj.max_corner - Obj.center

                curve = Curve(context, stretch=self.stretch, deform_axis=self.deform_axis)
                axis = self.deform_axis[-1]
                index = 'XYZ'.index(axis)
                points = [_min, _max] if self.deform_axis.startswith('POS') else [_max, _min]

                for vec in points:
                    for i in range (3):
                        if i != index:
                            vec[i] = 0.0

                curve.create_spline(subdivide_line(*points, times=self.subdivisions), type=self.type, resolution_u=self.resolution, order_u=self.order)
                curve.add_modifier(obj)
                matrix = Obj.matrix_scaless.copy()
                matrix.translation = Obj.matrix_scaless @ center

                curve.object.matrix_world = matrix

                curve.object.select_set(True)
                context.view_layer.objects.active = curve.object
                obj.select_set(False)

        msg = F'Curve Modifier {self.deform_axis[-1]}'
        subtext = f'{self.type.capitalize()} Divisions: {self.subdivisions} / Resolution: {self.resolution} / Stretch: {self.stretch}'#. Combined: {self.combined}.'
        if self.type == 'NURBS':
            subtext = f'Order: {self.order}. {subtext}'

        self.notify(val=msg, sub=subtext)

        return {'FINISHED'}

    def draw(self, context):
        self.layout.prop(self, 'type')
        self.layout.prop(self, 'deform_axis')
        self.layout.prop(self, 'subdivisions')
        self.layout.prop(self, 'resolution')
        if self.type == 'NURBS':
            self.layout.prop(self, 'order')
        self.layout.prop(self, 'stretch')
        self.layout.prop(self, 'combined')



def coordinates_to_diagonal(coords) -> Vector:
    mins = []
    maxs = []
    length = len(coords[0])

    for i in range (length):
        var = [vec[i] for vec in coords]
        mins.append(min(var))
        maxs.append(max(var))

    return Vector(mins), Vector(maxs)

def subdivide_line(p1: Vector, p2: Vector, times=0) -> list:
    direction = p2 - p1

    divisor = 1 + times
    increment = direction * (1 / divisor)

    result = []

    for i in range(divisor + 1):
        vec = p1 + (increment * i)
        result.append(vec)

    return result