OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / operators / meshtools / curve_extrude.py

292 lines · python ref:
import bpy
import gpu
import math
import bmesh
from math import radians
from bmesh.types import BMFace
from mathutils import Vector, Matrix, Euler
from bpy.props import IntProperty, FloatProperty, BoolProperty
from ... utility import addon
from ...ui_framework.operator_ui import Master
from ...utils.bmesh import get_edges_center, is_an_edge_selected
from ...utils.curve import copy_curve, convert_curve_to_mesh
from ...utility.object import apply_transform_bpy_version

class HOPS_OT_Curve_Extrude(bpy.types.Operator):
    bl_idname = "hops.curve_extrude"
    bl_label = "Curve Extrude"
    bl_description = "Extrude face along a curve"
    bl_options = {"REGISTER", "UNDO", "BLOCKING"}

    adaptive_segments: IntProperty(name="Adaptive Segments", default=15, min=2, max=100)
    stop_location: IntProperty(name="Stop Location", default=100, min=0, max=100)
    smooth_repeat: IntProperty(name="Smooth Repeat", default=0, min=0, max=50)
    smoothing_factor: FloatProperty(name="Smooth Factor", default=.5, min=0, max=1)
    apply_object_transform: BoolProperty(name="Apply Object Transform", default=False)

    called_ui = False

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        HOPS_OT_Curve_Extrude.called_ui = False

    @classmethod
    def poll(cls, context):

        mesh, curve, junk_obj = False, False, False
        mesh_obj = None
        for obj in context.selected_editable_objects:
            if obj.type == "MESH":
                mesh_obj = obj
                mesh = True
            elif obj.type == "CURVE":
                curve = True
            else:
                junk_obj = True
        if mesh and curve and not junk_obj:
            return True
        return False


    def execute(self, context):

        start_state = context.active_object.mode
        if start_state == 'OBJECT':
            bpy.ops.object.mode_set(mode='EDIT')

        cancel_due_to_bad_selection = False
        if is_an_edge_selected(context, context.active_object) == False:
            cancel_due_to_bad_selection = True

        if start_state == 'OBJECT':
            bpy.ops.object.mode_set(mode='OBJECT')

        # Operator UI
        if not HOPS_OT_Curve_Extrude.called_ui:

            HOPS_OT_Curve_Extrude.called_ui = True

            ui = Master()
            draw_data= []
            if not cancel_due_to_bad_selection:
                draw_data = [
                    ["Curve Extrude"],
                    ["Smooth Factor", self.smoothing_factor],
                    ["Smooth Repeat", self.smooth_repeat],
                    ["Stop Location", self.stop_location],
                    ["Adaptive Segments", self.adaptive_segments]]
            else:
                draw_data = [
                    ["Curve Extrude"],
                    ["Select a Face(s) or Edge(s)", "ERROR"]]

            ui.receive_draw_data(draw_data=draw_data)
            ui.draw(draw_bg=addon.preference().ui.Hops_operator_draw_bg, draw_border=addon.preference().ui.Hops_operator_draw_border)

        if cancel_due_to_bad_selection:
            return {'CANCELLED'}

        # Get Mesh and Curve
        self.mesh, self.curve = None, None
        self.mesh_obj = None
        for obj in context.editable_objects:
            if obj.type == "MESH":
                if context.active_object.name == obj.name:
                    self.mesh = obj
                    self.mesh_obj = obj
            if obj.type == "CURVE":
                self.curve = obj
        if self.mesh == None or self.curve == None:
            return {'CANCELLED'}

        if self.apply_object_transform:
            apply_transform_bpy_version()

        self.extrude_along_curve(context=context)

        # Set the mesh and curve selection back
        self.curve.select_set(True)
        self.mesh.select_set(True)

        return {"FINISHED"}


    def extrude_along_curve(self, context):

        # Copy original curve
        original_segments = self.curve.data.resolution_u
        mesh_curve = copy_curve(context, self.curve)

        # Set curve segs
        mesh_curve.data.resolution_u = self.adaptive_segments

        # Convert curve to mesh
        m_curve = convert_curve_to_mesh(context, mesh_curve, self.smooth_repeat, self.smoothing_factor)
        self.curve.data.resolution_u = original_segments

        # Get the points along the mesh curve
        points = get_points_from_mesh_curve(context, self.curve, m_curve, context.object)

        # Extrude
        extrude(context, points, self.stop_location, self.adaptive_segments)

        # Remove the mesh curve
        bpy.data.objects.remove(mesh_curve, do_unlink=True)


def extrude(context, points, stop_location, adaptive_segments):

    obj = context.object
    mesh = obj.data
    bm = bmesh.from_edit_mesh(mesh)
    stop_location = int(len(points) * (stop_location * .01))
    index = 0

    for point in points:
        if stop_location == index:
            break

        # Extrude edges
        if bm.faces.active == None:
            edge_extrude(bm, point, points, obj, index)

        # Extrude faces
        else:
            face_extrude(bm, point, points, obj, index)

        index += 1

    bmesh.update_edit_mesh(mesh)


def face_extrude(bm, point, points, obj, index):

    bm.normal_update()
    active_face = bm.faces.active
    face_normal = active_face.normal
    face_normal = obj.matrix_world @ face_normal

    face_center_loc = get_edges_center(bm)
    face_center_loc = obj.matrix_world @ face_center_loc

    face_to_point_vec = point - face_center_loc

    if index == 0:
        if int(face_to_point_vec.magnitude) ==  0:
            return
        if face_center_loc == point:
            return

    # Extrude faces
    faces = [f for f in bm.faces if f.select]
    ret_geom = bmesh.ops.extrude_face_region(bm, geom=faces)

    for face in faces:
        face.select = False

    for face in ret_geom["geom"]:
        if isinstance(face, bmesh.types.BMFace):
            face.select = True

    # Flip extruded geo
    bmesh.ops.reverse_faces(bm, faces=faces)

    # Offset vector
    offset_vec = point - face_to_point_vec

    # Translate
    verts = [v for v in ret_geom["geom"] if isinstance(v, bmesh.types.BMVert)]
    ret_geom = bmesh.ops.translate(bm, vec=face_to_point_vec, space=obj.matrix_world, verts=verts)

    # Rotate
    if index + 1 < len(points):
        rot = face_normal.rotation_difference(points[index + 1] - point).to_matrix()
        bmesh.ops.rotate(bm, cent=point, matrix=rot, verts=verts, space=obj.matrix_world)

    # Delete
    bmesh.ops.delete(bm, geom=faces, context='FACES_KEEP_BOUNDARY')


def edge_extrude(bm, point, points, obj, index):

    edge_center_loc = get_edges_center(bm)
    edge_center_loc = obj.matrix_world @ edge_center_loc

    bm.normal_update()
    edge_center_to_point = point - edge_center_loc

    if index == 0:
        if int(edge_center_to_point.magnitude) == 0:
            return
        if edge_center_loc == point:
            return

    # Extrude edges
    edges = [e for e in bm.edges if e.select]
    ret_geom = bmesh.ops.extrude_edge_only(bm, edges=edges, use_normal_flip=True)

    # Recalc the extruded normals
    faces = []
    for face in ret_geom["geom"]:
        if isinstance(face, bmesh.types.BMFace):
            faces.append(face)

    bmesh.ops.recalc_face_normals(bm, faces=faces)

    for edge in edges:
        edge.select = False

    for edge in ret_geom["geom"]:
        if isinstance(edge, bmesh.types.BMEdge):
            edge.select = True

    # Offset vector
    offset_vec = point - edge_center_to_point

    # Translate
    verts = [v for v in ret_geom["geom"] if isinstance(v, bmesh.types.BMVert)]
    ret_geom = bmesh.ops.translate(bm, vec=edge_center_to_point, space=obj.matrix_world, verts=verts)

    # Rotate
    if index + 1 < len(points):
        rot = edge_center_to_point.rotation_difference(points[index + 1] - point).to_matrix()
        bmesh.ops.rotate(bm, cent=point, matrix=rot, verts=verts, space=obj.matrix_world)


def get_points_from_mesh_curve(context, original_curve, mesh_curve, obj):
    '''Get the points from the curve mesh, this also makes sure to send back the correct order.'''

    # Get the face center location
    bm = bmesh.from_edit_mesh(obj.data)
    face = bm.faces.active

    face_center_loc = Vector((0,0,0))

    # If there is no active face
    if face == None:
        face_center_loc = get_edges_center(bm)
        face_center_loc = obj.matrix_world @ face_center_loc

    else:
        face_center_loc = face.calc_center_median()
        face_center_loc = obj.matrix_world @ face_center_loc

    points = []
    verts = mesh_curve.data.vertices

    # Generate the points with the tail end closer to the face center
    first_coord = original_curve.matrix_world @ verts[0].co
    last_coord = original_curve.matrix_world @ verts[-1].co
    reverse = False if (first_coord - face_center_loc).magnitude < (last_coord - face_center_loc).magnitude else True

    if reverse:
        for vert in reversed(verts):
            points.append(original_curve.matrix_world @ vert.co)
    else:
        for vert in verts:
            points.append(original_curve.matrix_world @ vert.co)

    return points