Skip to content
Snippets Groups Projects
mesh_looptools.py 167 KiB
Newer Older
                n = len(splines[i]) - 1
            elif n < 0:
                n = 0
            
            if interpolation == 'cubic':
                ax, bx, cx, dx, tx = splines[i][n][0]
                x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
                ay, by, cy, dy, ty = splines[i][n][1]
                y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
                az, bz, cz, dz, tz = splines[i][n][2]
                z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
                change.append([p, mathutils.Vector([x,y,z])])
            else: # interpolation == 'linear'
                a, d, t, u = splines[i][n]
                if u == 0:
                    u = 1e-8
                change.append([p, ((m-t)/u)*d + a])
    for c in change:
        move.append([c[0], (bm_mod.verts[c[0]].co + c[1]) / 2])
    
    return(move)


##########################################
####### Space functions ##################
##########################################

# calculate relative positions compared to first knot
def space_calculate_t(bm_mod, knots):
    tknots = []
    loc_prev = False
    len_total = 0
    for k in knots:
        loc = mathutils.Vector(bm_mod.verts[k].co[:])
        if not loc_prev:
            loc_prev = loc
        len_total += (loc - loc_prev).length
        tknots.append(len_total)
        loc_prev = loc
    amount = len(knots)
    t_per_segment = len_total / (amount - 1)
    tpoints = [i * t_per_segment for i in range(amount)]
    
    return(tknots, tpoints)


# change the location of the points to their place on the spline
def space_calculate_verts(bm_mod, interpolation, tknots, tpoints, points,
splines):
    move = []
    for p in points:
        m = tpoints[points.index(p)]
        if m in tknots:
            n = tknots.index(m)
        else:
            t = tknots[:]
            t.append(m)
            t.sort()
            n = t.index(m) - 1
        if n > len(splines) - 1:
            n = len(splines) - 1
        elif n < 0:
            n = 0
        
        if interpolation == 'cubic':
            ax, bx, cx, dx, tx = splines[n][0]
            x = ax + bx*(m-tx) + cx*(m-tx)**2 + dx*(m-tx)**3
            ay, by, cy, dy, ty = splines[n][1]
            y = ay + by*(m-ty) + cy*(m-ty)**2 + dy*(m-ty)**3
            az, bz, cz, dz, tz = splines[n][2]
            z = az + bz*(m-tz) + cz*(m-tz)**2 + dz*(m-tz)**3
            move.append([p, mathutils.Vector([x,y,z])])
        else: # interpolation == 'linear'
            a, d, t, u = splines[n]
            move.append([p, ((m-t)/u)*d + a])
    
    return(move)


##########################################
####### Operators ########################
##########################################

# bridge operator
class Bridge(bpy.types.Operator):
    bl_idname = 'mesh.looptools_bridge'
    bl_label = "Bridge / Loft"
    bl_description = "Bridge two, or loft several, loops of vertices"
    bl_options = {'REGISTER', 'UNDO'}
    
    cubic_strength = bpy.props.FloatProperty(name = "Strength",
        description = "Higher strength results in more fluid curves",
        default = 1.0,
        soft_min = -3.0,
        soft_max = 3.0)
    interpolation = bpy.props.EnumProperty(name = "Interpolation mode",
        items = (('cubic', "Cubic", "Gives curved results"),
            ('linear', "Linear", "Basic, fast, straight interpolation")),
        description = "Interpolation mode: algorithm used when creating "\
            "segments",
        default = 'cubic')
    loft = bpy.props.BoolProperty(name = "Loft",
        description = "Loft multiple loops, instead of considering them as "\
            "a multi-input for bridging",
        default = False)
    loft_loop = bpy.props.BoolProperty(name = "Loop",
        description = "Connect the first and the last loop with each other",
        default = False)
    min_width = bpy.props.IntProperty(name = "Minimum width",
        description = "Segments with an edge smaller than this are merged "\
            "(compared to base edge)",
        default = 0,
        min = 0,
        max = 100,
        subtype = 'PERCENTAGE')
    mode = bpy.props.EnumProperty(name = "Mode",
        items = (('basic', "Basic", "Fast algorithm"), ('shortest',
            "Shortest edge", "Slower algorithm with better vertex matching")),
        description = "Algorithm used for bridging",
        default = 'shortest')
    remove_faces = bpy.props.BoolProperty(name = "Remove faces",
        description = "Remove faces that are internal after bridging",
        default = True)
    reverse = bpy.props.BoolProperty(name = "Reverse",
        description = "Manually override the direction in which the loops "\
                      "are bridged. Only use if the tool gives the wrong " \
                      "result",
        default = False)
    segments = bpy.props.IntProperty(name = "Segments",
        description = "Number of segments used to bridge the gap "\
            "(0 = automatic)",
        default = 1,
        min = 0,
        soft_max = 20)
    twist = bpy.props.IntProperty(name = "Twist",
        description = "Twist what vertices are connected to each other",
        default = 0)
    
    @classmethod
    def poll(cls, context):
        ob = context.active_object
        return (ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
    def draw(self, context):
        layout = self.layout
        #layout.prop(self, "mode") # no cases yet where 'basic' mode is needed
        
        # top row
        col_top = layout.column(align=True)
        row = col_top.row(align=True)
        col_left = row.column(align=True)
        col_right = row.column(align=True)
        col_right.active = self.segments != 1
        col_left.prop(self, "segments")
        col_right.prop(self, "min_width", text="")
        # bottom row
        bottom_left = col_left.row()
        bottom_left.active = self.segments != 1
        bottom_left.prop(self, "interpolation", text="")
        bottom_right = col_right.row()
        bottom_right.active = self.interpolation == 'cubic'
        bottom_right.prop(self, "cubic_strength")
        # boolean properties
        col_top.prop(self, "remove_faces")
        if self.loft:
            col_top.prop(self, "loft_loop")
        
        # override properties
        col_top.separator()
        row = layout.row(align = True)
        row.prop(self, "twist")
        row.prop(self, "reverse")
    
    def invoke(self, context, event):
        # load custom settings
        context.window_manager.looptools.bridge_loft = self.loft
        settings_load(self)
        return self.execute(context)
    
    def execute(self, context):
        # initialise
        global_undo, object, bm = initialise()
        edge_faces, edgekey_to_edge, old_selected_faces, smooth = \
            bridge_initialise(bm, self.interpolation)
        settings_write(self)
        
        # check cache to see if we can save time
        input_method = bridge_input_method(self.loft, self.loft_loop)
        cached, single_loops, loops, derived, mapping = cache_read("Bridge",
            object, bm, input_method, False)
        if not cached:
            # get loops
            loops = bridge_get_input(bm)
            if loops:
                # reorder loops if there are more than 2
                if len(loops) > 2:
                    if self.loft:
                        loops = bridge_sort_loops(bm, loops, self.loft_loop)
                    else:
                        loops = bridge_match_loops(bm, loops)
        
        # saving cache for faster execution next time
        if not cached:
            cache_write("Bridge", object, bm, input_method, False, False,
                loops, False, False)
        if loops:
            # calculate new geometry
            vertices = []
            faces = []
            max_vert_index = len(bm.verts)-1
            for i in range(1, len(loops)):
                if not self.loft and i%2 == 0:
                    continue
                lines = bridge_calculate_lines(bm, loops[i-1:i+1],
                    self.mode, self.twist, self.reverse)
                vertex_normals = bridge_calculate_virtual_vertex_normals(bm,
                    lines, loops[i-1:i+1], edge_faces, edgekey_to_edge)
                segments = bridge_calculate_segments(bm, lines,
                    loops[i-1:i+1], self.segments)
                new_verts, new_faces, max_vert_index = \
                    bridge_calculate_geometry(bm, lines, vertex_normals,
                    segments, self.interpolation, self.cubic_strength,
                    self.min_width, max_vert_index)
                if new_verts:
                    vertices += new_verts
                if new_faces:
                    faces += new_faces
            # make sure faces in loops that aren't used, aren't removed
            if self.remove_faces and old_selected_faces:
                bridge_save_unused_faces(bm, old_selected_faces, loops)
            # create vertices
            if vertices:
                bridge_create_vertices(bm, vertices)
            # create faces
            if faces:
                new_faces = bridge_create_faces(object, bm, faces, self.twist)
                old_selected_faces = [i for i, face in enumerate(bm.faces) \
                    if face.index in old_selected_faces] # updating list
                bridge_select_new_faces(new_faces, smooth)
            # edge-data could have changed, can't use cache next run
            if faces and not vertices:
                cache_delete("Bridge")
            # delete internal faces
            if self.remove_faces and old_selected_faces:
                bridge_remove_internal_faces(bm, old_selected_faces)
            # make sure normals are facing outside
            bmesh.update_edit_mesh(object.data, tessface=False,
                destructive=True)
3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 3576 3577
            bpy.ops.mesh.normals_make_consistent()
        
        # cleaning up
        terminate(global_undo)
        
        return{'FINISHED'}


# circle operator
class Circle(bpy.types.Operator):
    bl_idname = "mesh.looptools_circle"
    bl_label = "Circle"
    bl_description = "Move selected vertices into a circle shape"
    bl_options = {'REGISTER', 'UNDO'}
    
    custom_radius = bpy.props.BoolProperty(name = "Radius",
        description = "Force a custom radius",
        default = False)
    fit = bpy.props.EnumProperty(name = "Method",
        items = (("best", "Best fit", "Non-linear least squares"),
            ("inside", "Fit inside","Only move vertices towards the center")),
        description = "Method used for fitting a circle to the vertices",
        default = 'best')
    flatten = bpy.props.BoolProperty(name = "Flatten",
        description = "Flatten the circle, instead of projecting it on the " \
            "mesh",
        default = True)
    influence = bpy.props.FloatProperty(name = "Influence",
        description = "Force of the tool",
        default = 100.0,
        min = 0.0,
        max = 100.0,
        precision = 1,
        subtype = 'PERCENTAGE')
    radius = bpy.props.FloatProperty(name = "Radius",
        description = "Custom radius for circle",
        default = 1.0,
        min = 0.0,
        soft_max = 1000.0)
    regular = bpy.props.BoolProperty(name = "Regular",
        description = "Distribute vertices at constant distances along the " \
            "circle",
        default = True)
    
    @classmethod
    def poll(cls, context):
        ob = context.active_object
        return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
    def draw(self, context):
        layout = self.layout
        col = layout.column()
        
        col.prop(self, "fit")
        col.separator()
        
        col.prop(self, "flatten")
        row = col.row(align=True)
        row.prop(self, "custom_radius")
        row_right = row.row(align=True)
        row_right.active = self.custom_radius
        row_right.prop(self, "radius", text="")
        col.prop(self, "regular")
        col.separator()
                
        col.prop(self, "influence")
    
    def invoke(self, context, event):
        # load custom settings
        settings_load(self)
        return self.execute(context)
    
    def execute(self, context):
        # initialise
        global_undo, object, bm = initialise()
        settings_write(self)
        # check cache to see if we can save time
        cached, single_loops, loops, derived, mapping = cache_read("Circle",
            object, bm, False, False)
        if cached:
            derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
        else:
            # find loops
            derived, bm_mod, single_vertices, single_loops, loops = \
                circle_get_input(object, bm, context.scene)
            mapping = get_mapping(derived, bm, bm_mod, single_vertices,
                False, loops)
            single_loops, loops = circle_check_loops(single_loops, loops,
                mapping, bm_mod)
        
        # saving cache for faster execution next time
        if not cached:
            cache_write("Circle", object, bm, False, False, single_loops,
                loops, derived, mapping)
        
        move = []
        for i, loop in enumerate(loops):
            # best fitting flat plane
            com, normal = calculate_plane(bm_mod, loop)
            # if circular, shift loop so we get a good starting vertex
            if loop[1]:
                loop = circle_shift_loop(bm_mod, loop, com)
            # flatten vertices on plane
            locs_2d, p, q = circle_3d_to_2d(bm_mod, loop, com, normal)
            # calculate circle
            if self.fit == 'best':
                x0, y0, r = circle_calculate_best_fit(locs_2d)
            else: # self.fit == 'inside'
                x0, y0, r = circle_calculate_min_fit(locs_2d)
            # radius override
            if self.custom_radius:
                r = self.radius / p.length
            # calculate positions on circle
            if self.regular:
                new_locs_2d = circle_project_regular(locs_2d[:], x0, y0, r)
            else:
                new_locs_2d = circle_project_non_regular(locs_2d[:], x0, y0, r)
            # take influence into account
            locs_2d = circle_influence_locs(locs_2d, new_locs_2d,
                self.influence)
            # calculate 3d positions of the created 2d input
            move.append(circle_calculate_verts(self.flatten, bm_mod,
                locs_2d, com, p, q, normal))
            # flatten single input vertices on plane defined by loop
            if self.flatten and single_loops:
                move.append(circle_flatten_singles(bm_mod, com, p, q,
                    normal, single_loops[i]))
        
        # move vertices to new locations
        move_verts(object, bm, mapping, move, -1)
        
        # cleaning up
        if derived:
            bm_mod.free()
        terminate(global_undo)
        
        return{'FINISHED'}


# curve operator
class Curve(bpy.types.Operator):
    bl_idname = "mesh.looptools_curve"
    bl_label = "Curve"
    bl_description = "Turn a loop into a smooth curve"
    bl_options = {'REGISTER', 'UNDO'}
    
    boundaries = bpy.props.BoolProperty(name = "Boundaries",
        description = "Limit the tool to work within the boundaries of the "\
            "selected vertices",
        default = False)
    influence = bpy.props.FloatProperty(name = "Influence",
        description = "Force of the tool",
        default = 100.0,
        min = 0.0,
        max = 100.0,
        precision = 1,
        subtype = 'PERCENTAGE')
    interpolation = bpy.props.EnumProperty(name = "Interpolation",
        items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
            ("linear", "Linear", "Simple and fast linear algorithm")),
        description = "Algorithm used for interpolation",
        default = 'cubic')
    regular = bpy.props.BoolProperty(name = "Regular",
        description = "Distribute vertices at constant distances along the" \
            "curve",
        default = True)
    restriction = bpy.props.EnumProperty(name = "Restriction",
        items = (("none", "None", "No restrictions on vertex movement"),
            ("extrude", "Extrude only","Only allow extrusions (no "\
                "indentations)"),
            ("indent", "Indent only", "Only allow indentation (no "\
                "extrusions)")),
        description = "Restrictions on how the vertices can be moved",
        default = 'none')
    
    @classmethod
    def poll(cls, context):
        ob = context.active_object
        return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
    def draw(self, context):
        layout = self.layout
        col = layout.column()
        
        col.prop(self, "interpolation")
        col.prop(self, "restriction")
        col.prop(self, "boundaries")
        col.prop(self, "regular")
        col.separator()
        
        col.prop(self, "influence")
    
    def invoke(self, context, event):
        # load custom settings
        settings_load(self)
        return self.execute(context)
    
    def execute(self, context):
        # initialise
        global_undo, object, bm = initialise()
        settings_write(self)
        # check cache to see if we can save time
        cached, single_loops, loops, derived, mapping = cache_read("Curve",
            object, bm, False, self.boundaries)
        if cached:
            derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
        else:
            # find loops
            derived, bm_mod, loops = curve_get_input(object, bm,
                self.boundaries, context.scene)
            mapping = get_mapping(derived, bm, bm_mod, False, True, loops)
            loops = check_loops(loops, mapping, bm_mod)
        verts_selected = [v.index for v in bm_mod.verts if v.select \
            and not v.hide]
        
        # saving cache for faster execution next time
        if not cached:
            cache_write("Curve", object, bm, False, self.boundaries, False,
                loops, derived, mapping)
        
        move = []
        for loop in loops:
            knots, points = curve_calculate_knots(loop, verts_selected)
            pknots = curve_project_knots(bm_mod, verts_selected, knots,
                points, loop[1])
            tknots, tpoints = curve_calculate_t(bm_mod, knots, points,
                pknots, self.regular, loop[1])
            splines = calculate_splines(self.interpolation, bm_mod,
                tknots, knots)
            move.append(curve_calculate_vertices(bm_mod, knots, tknots,
                points, tpoints, splines, self.interpolation,
                self.restriction))
        
        # move vertices to new locations
        move_verts(object, bm, mapping, move, self.influence)
        
        # cleaning up 
        if derived:
            bm_mod.free()
        terminate(global_undo)

        return{'FINISHED'}


# flatten operator
class Flatten(bpy.types.Operator):
    bl_idname = "mesh.looptools_flatten"
    bl_label = "Flatten"
    bl_description = "Flatten vertices on a best-fitting plane"
    bl_options = {'REGISTER', 'UNDO'}
    
    influence = bpy.props.FloatProperty(name = "Influence",
        description = "Force of the tool",
        default = 100.0,
        min = 0.0,
        max = 100.0,
        precision = 1,
        subtype = 'PERCENTAGE')
    plane = bpy.props.EnumProperty(name = "Plane",
        items = (("best_fit", "Best fit", "Calculate a best fitting plane"),
            ("normal", "Normal", "Derive plane from averaging vertex "\
            "normals"),
            ("view", "View", "Flatten on a plane perpendicular to the "\
            "viewing angle")),
        description = "Plane on which vertices are flattened",
        default = 'best_fit')
    restriction = bpy.props.EnumProperty(name = "Restriction",
        items = (("none", "None", "No restrictions on vertex movement"),
            ("bounding_box", "Bounding box", "Vertices are restricted to "\
            "movement inside the bounding box of the selection")),
        description = "Restrictions on how the vertices can be moved",
        default = 'none')
    
    @classmethod
    def poll(cls, context):
        ob = context.active_object
        return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
    def draw(self, context):
        layout = self.layout
        col = layout.column()
        
        col.prop(self, "plane")
        #col.prop(self, "restriction")
        col.separator()
        
        col.prop(self, "influence")
    
    def invoke(self, context, event):
        # load custom settings
        settings_load(self)
        return self.execute(context)
    
    def execute(self, context):
        # initialise
        global_undo, object, bm = initialise()
        settings_write(self)
        # check cache to see if we can save time
        cached, single_loops, loops, derived, mapping = cache_read("Flatten",
            object, bm, False, False)
        if not cached:
            # order input into virtual loops
            loops = flatten_get_input(bm)
            loops = check_loops(loops, mapping, bm)
        
        # saving cache for faster execution next time
        if not cached:
            cache_write("Flatten", object, bm, False, False, False, loops,
                False, False)
        
        move = []
        for loop in loops:
            # calculate plane and position of vertices on them
            com, normal = calculate_plane(bm, loop, method=self.plane,
                object=object)
            to_move = flatten_project(bm, loop, com, normal)
            if self.restriction == 'none':
                move.append(to_move)
            else:
                move.append(to_move)
        move_verts(object, bm, False, move, self.influence)
        
        # cleaning up
        terminate(global_undo)
        
        return{'FINISHED'}


Bart Crouch's avatar
Bart Crouch committed
# gstretch operator
class GStretch(bpy.types.Operator):
    bl_idname = "mesh.looptools_gstretch"
    bl_label = "Gstretch"
    bl_description = "Stretch selected vertices to Grease Pencil stroke"
    bl_options = {'REGISTER', 'UNDO'}
    
    conversion = bpy.props.EnumProperty(name = "Conversion",
        items = (("distance", "Distance", "Set the distance between vertices "\
            "of the converted grease pencil stroke"),
            ("limit_vertices", "Limit vertices", "Set the minimum and maximum "\
            "number of vertices that converted GP strokes will have"),
            ("vertices", "Exact vertices", "Set the exact number of vertices "\
            "that converted grease pencil strokes will have. Short strokes "\
            "with few points may contain less vertices than this number."),
            ("none", "No simplification", "Convert each grease pencil point "\
            "to a vertex")),
        description = "If grease pencil strokes are converted to geometry, "\
            "use this simplification method",
        default = 'limit_vertices')
    conversion_distance = bpy.props.FloatProperty(name = "Distance",
        description = "Absolute distance between vertices along the converted "\
            "grease pencil stroke",
        default = 0.1,
        min = 0.000001,
        soft_min = 0.01,
        soft_max = 100)
    conversion_max = bpy.props.IntProperty(name = "Max Vertices",
        description = "Maximum number of vertices grease pencil strokes will "\
            "have, when they are converted to geomtery",
        default = 32,
        min = 3,
        soft_max = 500,
        update = gstretch_update_min)
    conversion_min = bpy.props.IntProperty(name = "Min Vertices",
        description = "Minimum number of vertices grease pencil strokes will "\
            "have, when they are converted to geomtery",
        default = 8,
        min = 3,
        soft_max = 500,
        update = gstretch_update_max)
    conversion_vertices = bpy.props.IntProperty(name = "Vertices",
        description = "Number of vertices grease pencil strokes will "\
            "have, when they are converted to geometry. If strokes have less "\
            "points than required, the 'Spread evenly' method is used.",
        default = 32,
        min = 3,
        soft_max = 500)
Bart Crouch's avatar
Bart Crouch committed
    delete_strokes = bpy.props.BoolProperty(name="Delete strokes",
        description = "Remove Grease Pencil strokes if they have been used "\
            "for Gstretch. WARNING: DOES NOT SUPPORT UNDO",
Bart Crouch's avatar
Bart Crouch committed
        default = False)    
    influence = bpy.props.FloatProperty(name = "Influence",
        description = "Force of the tool",
        default = 100.0,
        min = 0.0,
        max = 100.0,
        precision = 1,
        subtype = 'PERCENTAGE')
    method = bpy.props.EnumProperty(name = "Method",
        items = (("project", "Project", "Project vertices onto the stroke, "\
            "using vertex normals and connected edges"),
            ("irregular", "Spread", "Distribute vertices along the full "\
            "stroke, retaining relative distances between the vertices"),
            ("regular", "Spread evenly", "Distribute vertices at regular "\
            "distances along the full stroke")),
        description = "Method of distributing the vertices over the Grease "\
            "Pencil stroke",
        default = 'regular')
Bart Crouch's avatar
Bart Crouch committed
    @classmethod
    def poll(cls, context):
        ob = context.active_object
        return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
Bart Crouch's avatar
Bart Crouch committed
    
    def draw(self, context):
        layout = self.layout
        col = layout.column()
        
        col.prop(self, "method")
        col.prop(self, "delete_strokes")
        col.separator()
        
        col_conv = col.column(align=True)
        col_conv.prop(self, "conversion", text="")
        if self.conversion == 'distance':
            col_conv.prop(self, "conversion_distance")
        elif self.conversion == 'limit_vertices':
            row = col_conv.row(align=True)
            row.prop(self, "conversion_min", text="Min")
            row.prop(self, "conversion_max", text="Max")
        elif self.conversion == 'vertices':
            col_conv.prop(self, "conversion_vertices")
Bart Crouch's avatar
Bart Crouch committed
        col.separator()
Bart Crouch's avatar
Bart Crouch committed
        col.prop(self, "influence")
    
    def invoke(self, context, event):
        # flush cached strokes
        if 'Gstretch' in looptools_cache:
            looptools_cache['Gstretch']['single_loops'] = []
Bart Crouch's avatar
Bart Crouch committed
        # load custom settings
        settings_load(self)
        return self.execute(context)
    
    def execute(self, context):
        # initialise
        global_undo, object, bm = initialise()
        settings_write(self)
        
        # check cache to see if we can save time
        cached, safe_strokes, loops, derived, mapping = cache_read("Gstretch",
            object, bm, False, False)
Bart Crouch's avatar
Bart Crouch committed
        if cached:
            if safe_strokes:
                strokes = gstretch_safe_to_true_strokes(safe_strokes)
            # cached strokes were flushed (see operator's invoke function)
            elif object.grease_pencil:
                strokes = gstretch_get_strokes(object)
            else:
                derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
                strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
Bart Crouch's avatar
Bart Crouch committed
            derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
        else:
            # find loops
            derived, bm_mod, loops = get_connected_input(object, bm,
                context.scene, input='selected')
            mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
            loops = check_loops(loops, mapping, bm_mod)
            # get strokes
            if object.grease_pencil:
                strokes = gstretch_get_strokes(object)
            else:
                strokes = gstretch_get_fake_strokes(object, bm_mod, loops)
Bart Crouch's avatar
Bart Crouch committed
        
        # saving cache for faster execution next time
        if not cached:
            if strokes:
                safe_strokes = gstretch_true_to_safe_strokes(strokes)
            else:
                safe_strokes = []
            cache_write("Gstretch", object, bm, False, False,
                safe_strokes, loops, derived, mapping)

Bart Crouch's avatar
Bart Crouch committed
        # pair loops and strokes
        ls_pairs = gstretch_match_loops_strokes(loops, strokes, object, bm_mod)
        ls_pairs = gstretch_align_pairs(ls_pairs, object, bm_mod, self.method)
        
        move = []
        if not loops:
            # no selected geometry, convert GP to verts
            if strokes:
                move.append(gstretch_create_verts(object, bm, strokes,
                    self.method, self.conversion, self.conversion_distance,
                    self.conversion_max, self.conversion_min,
                    self.conversion_vertices))
                for stroke in strokes:
                    gstretch_erase_stroke(stroke, context)
        elif ls_pairs:
Bart Crouch's avatar
Bart Crouch committed
            for (loop, stroke) in ls_pairs:
                move.append(gstretch_calculate_verts(loop, stroke, object,
                    bm_mod, self.method))
                if self.delete_strokes:
                    if type(stroke) != bpy.types.GPencilStroke:
                        # in case of cached fake stroke, get the real one
                        if object.grease_pencil:
                            strokes = gstretch_get_strokes(object)
                            ls_pairs = gstretch_match_loops_strokes(loops,
                                strokes, object, bm_mod)
                            ls_pairs = gstretch_align_pairs(ls_pairs, object,
                                bm_mod, self.method)
                            for (l, s) in ls_pairs:
                                if l == loop:
                                    stroke = s
                                    break
Bart Crouch's avatar
Bart Crouch committed
                    gstretch_erase_stroke(stroke, context)
Bart Crouch's avatar
Bart Crouch committed
        # move vertices to new locations
        bmesh.update_edit_mesh(object.data, tessface=True,
                destructive=True)
Bart Crouch's avatar
Bart Crouch committed
        move_verts(object, bm, mapping, move, self.influence)
        
        # cleaning up 
        if derived:
            bm_mod.free()
        terminate(global_undo)
        
        return{'FINISHED'}


# relax operator
class Relax(bpy.types.Operator):
    bl_idname = "mesh.looptools_relax"
    bl_label = "Relax"
    bl_description = "Relax the loop, so it is smoother"
    bl_options = {'REGISTER', 'UNDO'}
    
    input = bpy.props.EnumProperty(name = "Input",
        items = (("all", "Parallel (all)", "Also use non-selected "\
                "parallel loops as input"),
            ("selected", "Selection","Only use selected vertices as input")),
        description = "Loops that are relaxed",
        default = 'selected')
    interpolation = bpy.props.EnumProperty(name = "Interpolation",
        items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
            ("linear", "Linear", "Simple and fast linear algorithm")),
        description = "Algorithm used for interpolation",
        default = 'cubic')
    iterations = bpy.props.EnumProperty(name = "Iterations",
        items = (("1", "1", "One"),
            ("3", "3", "Three"),
            ("5", "5", "Five"),
            ("10", "10", "Ten"),
            ("25", "25", "Twenty-five")),
        description = "Number of times the loop is relaxed",
        default = "1")
    regular = bpy.props.BoolProperty(name = "Regular",
        description = "Distribute vertices at constant distances along the" \
            "loop",
        default = True)
    
    @classmethod
    def poll(cls, context):
        ob = context.active_object
        return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
    def draw(self, context):
        layout = self.layout
        col = layout.column()
        
        col.prop(self, "interpolation")
        col.prop(self, "input")
        col.prop(self, "iterations")
        col.prop(self, "regular")
    
    def invoke(self, context, event):
        # load custom settings
        settings_load(self)
        return self.execute(context)
    
    def execute(self, context):
        # initialise
        global_undo, object, bm = initialise()
        settings_write(self)
        # check cache to see if we can save time
        cached, single_loops, loops, derived, mapping = cache_read("Relax",
            object, bm, self.input, False)
        if cached:
            derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
        else:
            # find loops
            derived, bm_mod, loops = get_connected_input(object, bm,
                context.scene, self.input)
            mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
            loops = check_loops(loops, mapping, bm_mod)
        knots, points = relax_calculate_knots(loops)
        
        # saving cache for faster execution next time
        if not cached:
            cache_write("Relax", object, bm, self.input, False, False, loops,
                derived, mapping)
        
        for iteration in range(int(self.iterations)):
            # calculate splines and new positions
            tknots, tpoints = relax_calculate_t(bm_mod, knots, points,
                self.regular)
            splines = []
            for i in range(len(knots)):
                splines.append(calculate_splines(self.interpolation, bm_mod,
                    tknots[i], knots[i]))
            move = [relax_calculate_verts(bm_mod, self.interpolation,
                tknots, knots, tpoints, points, splines)]
            move_verts(object, bm, mapping, move, -1)
        
        # cleaning up
        if derived:
            bm_mod.free()
        terminate(global_undo)
        
        return{'FINISHED'}


# space operator
class Space(bpy.types.Operator):
    bl_idname = "mesh.looptools_space"
    bl_label = "Space"
    bl_description = "Space the vertices in a regular distrubtion on the loop"
    bl_options = {'REGISTER', 'UNDO'}
    
    influence = bpy.props.FloatProperty(name = "Influence",
        description = "Force of the tool",
        default = 100.0,
        min = 0.0,
        max = 100.0,
        precision = 1,
        subtype = 'PERCENTAGE')
    input = bpy.props.EnumProperty(name = "Input",
        items = (("all", "Parallel (all)", "Also use non-selected "\
                "parallel loops as input"),
            ("selected", "Selection","Only use selected vertices as input")),
        description = "Loops that are spaced",
        default = 'selected')
    interpolation = bpy.props.EnumProperty(name = "Interpolation",
        items = (("cubic", "Cubic", "Natural cubic spline, smooth results"),
            ("linear", "Linear", "Vertices are projected on existing edges")),
        description = "Algorithm used for interpolation",
        default = 'cubic')
    
    @classmethod
    def poll(cls, context):
        ob = context.active_object
        return(ob and ob.type == 'MESH' and context.mode == 'EDIT_MESH')
    
    def draw(self, context):
        layout = self.layout
        col = layout.column()
        
        col.prop(self, "interpolation")
        col.prop(self, "input")
        col.separator()
        
        col.prop(self, "influence")
    
    def invoke(self, context, event):
        # load custom settings
        settings_load(self)
        return self.execute(context)
    
    def execute(self, context):
        # initialise
        global_undo, object, bm = initialise()
        settings_write(self)
        # check cache to see if we can save time
        cached, single_loops, loops, derived, mapping = cache_read("Space",
            object, bm, self.input, False)
        if cached:
            derived, bm_mod = get_derived_bmesh(object, bm, context.scene)
        else:
            # find loops
            derived, bm_mod, loops = get_connected_input(object, bm,
                context.scene, self.input)
            mapping = get_mapping(derived, bm, bm_mod, False, False, loops)
            loops = check_loops(loops, mapping, bm_mod)
        
        # saving cache for faster execution next time
        if not cached:
            cache_write("Space", object, bm, self.input, False, False, loops,
                derived, mapping)
        
        move = []
        for loop in loops:
            # calculate splines and new positions
            if loop[1]: # circular
                loop[0].append(loop[0][0])
            tknots, tpoints = space_calculate_t(bm_mod, loop[0][:])
            splines = calculate_splines(self.interpolation, bm_mod,
                tknots, loop[0][:])
            move.append(space_calculate_verts(bm_mod, self.interpolation,
                tknots, tpoints, loop[0][:-1], splines))
        # move vertices to new locations
        move_verts(object, bm, mapping, move, self.influence)
        
        # cleaning up
        if derived:
            bm_mod.free()
        terminate(global_undo)
        
        return{'FINISHED'}


##########################################
####### GUI and registration #############
##########################################

# menu containing all tools
class VIEW3D_MT_edit_mesh_looptools(bpy.types.Menu):
    bl_label = "LoopTools"
    
    def draw(self, context):
        layout = self.layout
        
Campbell Barton's avatar
Campbell Barton committed
        layout.operator("mesh.looptools_bridge", text="Bridge").loft = False
        layout.operator("mesh.looptools_circle")
        layout.operator("mesh.looptools_curve")
        layout.operator("mesh.looptools_flatten")
Bart Crouch's avatar
Bart Crouch committed
        layout.operator("mesh.looptools_gstretch")
Campbell Barton's avatar
Campbell Barton committed
        layout.operator("mesh.looptools_bridge", text="Loft").loft = True
        layout.operator("mesh.looptools_relax")
        layout.operator("mesh.looptools_space")


# panel containing all tools
class VIEW3D_PT_tools_looptools(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'
    bl_context = "mesh_edit"
    bl_label = "LoopTools"
Brendon Murphy's avatar
Brendon Murphy committed
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        col = layout.column(align=True)
        lt = context.window_manager.looptools
        
        # bridge - first line
        split = col.split(percentage=0.15, align=True)
        if lt.display_bridge:
            split.prop(lt, "display_bridge", text="", icon='DOWNARROW_HLT')
        else:
            split.prop(lt, "display_bridge", text="", icon='RIGHTARROW')
Campbell Barton's avatar
Campbell Barton committed
        split.operator("mesh.looptools_bridge", text="Bridge").loft = False
        # bridge - settings
        if lt.display_bridge:
            box = col.column(align=True).box().column()
            #box.prop(self, "mode")
            
            # top row
            col_top = box.column(align=True)
            row = col_top.row(align=True)
            col_left = row.column(align=True)
            col_right = row.column(align=True)
            col_right.active = lt.bridge_segments != 1
            col_left.prop(lt, "bridge_segments")