Edge Fillet (OpenGL drawing/BGL)

update 2011-08-01
current GIT repo : https://github.com/zeffii/GL-fillet/

http://1.bp.blogspot.com/-rq1u2zMby-Y/TjGuucyk5II/AAAAAAAAASA/UKT0tHKLLcY/s1600/GL-fillet_GUI3.png

This code is presented purely as an example of opengl/bgl drawing and how to calculate world coordinates to screen coordinates, in the hope someone learning blender and python will find it useful.

this script will be elaborated further, but presently it works on several assumptions.

  • you have applied location/rotation/scale transforms before working with the object.
  • one vertex selected
  • vertex must be connected to 2 other vertices (no more, no less)
  • be in object mode, to draw the fillet.
  • hit esc to end drawing.

script doesnt yet:

  • a whole bunch of stuff I can’t remember now.

check the README.txt on github.

1 Like

this is the early version, for recent versions go to the first post.


import bpy
import bgl
import mathutils
import bpy_extras
import math

from mathutils import Vector
from mathutils.geometry import interpolate_bezier as bezlerp
from bpy_extras.view3d_utils import location_3d_to_region_2d as loc3d2d

# [ ] MILESTONE 3
# [ ] draw faux vertices
# [ ] make shift+rightlick, draw a line from selected vertex to mouse cursor.
# [x] draw opengl filleted line.
# [ ] allow mouse wheel to define segment numbers.
# [ ] enter to accept, and make real. esc to cancel.

''' temporary constants '''

NUM_SEGS = 15
NUM_VERTS = NUM_SEGS + 1
HALF_RAD = 0.5
# KAPPA = 0.5522847498  # approximates the circle, fascinating magic number! 
KAPPA = 4 * (( math.sqrt(2) - 1) / 3 )
mode = 'TRIG'


''' helper functions '''


def find_index_of_selected_vertex(obj):

    # force 'OBJECT' mode temporarily. [TODO]
    selected_verts = [i.index for i in obj.data.vertices if i.select]
    
    # prevent script from operating if currently >1 vertex is selected.
    verts_selected = len(selected_verts)
    if verts_selected != 1:
        return None
    else:
        return selected_verts[0]



def find_connected_verts(obj, found_index):

    edges = obj.data.edges
    connecting_edges = [i for i in edges if found_index in i.vertices[:]]
    if len(connecting_edges) != 2:
        return None
    else:
        connected_verts = []
        for edge in connecting_edges:
            cvert = set(edge.vertices[:]) 
            cvert.remove(found_index)
            connected_verts.append(cvert.pop())
        
        return connected_verts



def find_distances(obj, connected_verts, found_index):
    edge_lengths = []
    for vert in connected_verts:
        co1 = obj.data.vertices[vert].co
        co2 = obj.data.vertices[found_index].co
        edge_lengths.append([vert, (co1-co2).length])
    return edge_lengths

    
    
def generate_fillet(obj, c_index, max_rad, f_index):
        
    def get_first_cut(outer_point, focal, distance_from_f):
        co1 = obj.data.vertices[focal].co
        co2 = obj.data.vertices[outer_point].co
        real_length = (co1-co2).length
        ratio = distance_from_f / real_length
        
        # must use new variable, cannot do co1 += obj_center, changes in place.
        new_co1 = co1 + obj_centre
        new_co2 = co2 + obj_centre        
        return new_co1.lerp(new_co2, ratio)
        
    obj_centre = obj.location    
    distance_from_f = max_rad * HALF_RAD

    # make imaginary line between outerpoints
    outer_points = []
    for point in c_index:
        outer_points.append(get_first_cut(point, f_index, distance_from_f))
 
    # make imaginary line from focal point to halfway between outer_points
    focal_coordinate = obj.data.vertices[f_index].co + obj_centre
    center_of_outer_points = (outer_points[0] + outer_points[1]) / 2
    
    # find radial center, by lerping ab -> ad
    BC = (center_of_outer_points-outer_points[1]).length
    AB = (focal_coordinate-center_of_outer_points).length
    BD = (BC/AB)*BC
    AD = AB + BD
    ratio = AD / AB
    radial_center = focal_coordinate.lerp(center_of_outer_points, ratio)
        
    guide_line = [focal_coordinate, radial_center]
    return outer_points, guide_line



def resposition_arc_points(arc_verts, radial_centre):
    # ensure that every arc point is indeed radial distance away from center.
    revised_arc_points = []

    radial_dist_first = (arc_verts[0]-radial_centre).length
    radial_dist_last = (arc_verts[-1]-radial_centre).length
    desired_radial_distance = (radial_dist_first + radial_dist_last) * 0.5

    for point in arc_verts:
        radial_distance = (point-radial_centre).length
        ratio = 1/(radial_distance / desired_radial_distance)
        new_location = radial_centre.lerp(point, ratio)
        new_distance = (radial_centre-new_location).length
        revised_arc_points.append(new_location)
        print("was", radial_distance, "becomes", new_distance)

    return revised_arc_points



''' director function '''



def init_functions(self, context):

    obj = context.object 

    # Finding vertex.    
    found_index = find_index_of_selected_vertex(obj)
    if found_index != None:
        print("you selected vertex with index", found_index)
        connected_verts = find_connected_verts(obj, found_index)
    else:
        print("select one vertex, no more, no less")
        return
    

    # Find connected vertices.
    if connected_verts == None:
        print("vertex connected to only 1 other vert, or none at all")
        print("remove doubles, the script operates on vertices with 2 edges")
        return
    else:
        print(connected_verts)
    

    # reaching this stage means the vertex has 2 connected vertices. good.
    # Find distances and maximum radius.
    distances = find_distances(obj, connected_verts, found_index)
    for d in distances:
        print("from", found_index, "to", d[0], "=", d[1])

    max_rad = min(distances[0][1],distances[1][1])
    print("max radius", max_rad)


    return generate_fillet(obj, connected_verts, max_rad, found_index)



''' GL drawing '''



def draw_polyline_from_coordinates(context, points, LINE_TYPE):
    region = context.region
    rv3d = context.space_data.region_3d

    bgl.glColor4f(1.0, 1.0, 1.0, 1.0)

    if LINE_TYPE == "GL_LINE_STIPPLE":
        bgl.glLineStipple(4, 0x5555)
        bgl.glEnable(bgl.GL_LINE_STIPPLE)
        bgl.glColor4f(0.3, 0.3, 0.3, 0.5)
    
    bgl.glBegin(bgl.GL_LINE_STRIP)
    for coord in points:
        vector3d = (coord.x, coord.y, coord.z)
        vector2d = loc3d2d(region, rv3d, vector3d)
        bgl.glVertex2f(*vector2d)
    bgl.glEnd()
    
    if LINE_TYPE == "GL_LINE_STIPPLE":
        bgl.glDisable(bgl.GL_LINE_STIPPLE)
        bgl.glEnable(bgl.GL_BLEND)  # back to uninterupted lines
    
    return

    
def draw_callback_px(self, context):
    
    objlist = context.selected_objects
    names_of_empties = [i.name for i in objlist]

    region = context.region
    rv3d = context.space_data.region_3d
    points, guide_verts = init_functions(self, context)
    
    # draw bevel
    draw_polyline_from_coordinates(context, points, "GL_LINE_STIPPLE")
    
    # draw symmetry line
    draw_polyline_from_coordinates(context, guide_verts, "GL_LINE_STIPPLE")
    
    # get control points and knots.
    h_control = guide_verts[0]
    knot1, knot2 = points[0], points[1]
    kappa_ctrl_1 = knot1.lerp(h_control, KAPPA)
    kappa_ctrl_2 = knot2.lerp(h_control, KAPPA)
    arc_verts = bezlerp(knot1, kappa_ctrl_1, kappa_ctrl_2, knot2, NUM_VERTS)

    # draw fillet ( 2 modes )        
    if mode == 'TRIG':
        radial_centre = guide_verts[1]
        arc_verts = resposition_arc_points(arc_verts, radial_centre)
    if mode == 'KAPPA':
        print("using vanilla kappa, this mode produces a poor approximation")
        pass 
        
    draw_polyline_from_coordinates(context, arc_verts, "GL_BLEND")        
        
    
    # restore opengl defaults
    bgl.glLineWidth(1)
    bgl.glDisable(bgl.GL_BLEND)
    bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
    return


    
''' UI elements '''



class UIPanel(bpy.types.Panel):
    bl_label = "Hello from UI panel"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
 
    scn = bpy.types.Scene
    object = bpy.context.object
    # scn.Monster = object.location.x
    # scn.MyMove = bpy.props.FloatProperty()

     
    def draw(self, context):
        layout = self.layout
        ob = context.object
        scn = context.scene

        row1 = layout.row(align=True)
        # row1.prop(ob, "location")
        row1.operator("dynamic.fillet")
        # row1.prop(ob, 'location', index = 0, text = "Spine Spline", slider = True)




class OBJECT_OT_add_object(bpy.types.Operator):
    bl_idname = "dynamic.fillet"
    bl_label = "Check Vertice"
    bl_description = "Allows the user to dynamically fillet a vert/edge"
    bl_options = {'REGISTER', 'UNDO'}

    '''
    scale = FloatVectorProperty(name='scale',
                                default=(1.0, 1.0, 1.0),
                                subtype='TRANSLATION',
                                description='scaling')
    '''
    
    
    def modal(self, context, event):
        context.area.tag_redraw()
        
        if event.type == 'RIGHTMOUSE':
            if event.value == 'RELEASE':
                print("discontinue drawing")
                context.area.tag_redraw()
                context.region.callback_remove(self._handle)
                return {'CANCELLED'}  
     
        return {'RUNNING_MODAL'}
    
    
    
    def invoke(self, context, event):

        if context.area.type == 'VIEW_3D':
            context.area.tag_redraw()
            context.window_manager.modal_handler_add(self)

            # Add the region OpenGL drawing callback
            # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
            self._handle = context.region.callback_add(
                            draw_callback_px, 
                            (self, context), 
                            'POST_PIXEL')

            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, 
            "View3D not found, cannot run operator")
            context.area.tag_redraw()            
            return {'CANCELLED'}
    
    
    '''    
    
    def execute(self, context):

        #add_object(self, context)
        init_functions(self, context)

        return {'FINISHED'}

    '''


 
bpy.utils.register_module(__name__)

an elaboration draws GL_POINTS
like
http://4.bp.blogspot.com/-Rqbz0j9inQQ/Ti6dqsJZSeI/AAAAAAAAARI/YDy57VCPmY4/s1600/opengl_curve_with_points.png

code here

Straight away we notice the uneven distribution of points, as a result of using bezier interpolation.
While the code does force the correct radial distance of each point, it doesn’t force even spacing - this will require a pure trig method. I don’t imagine it is difficult, but it is something i’ve never done before so I look forward to figuring it out [noparse]:)[/noparse]

updated download link to the current GIT, thanks Kilon for convincing me to go that way!

How to use it
Thanks

Hi swathi, it doesn’t make geometry yet. At the moment it’s only drawing GUI things. The GUI needs a few more tweaks then it will add the new real vertices.

thanks for the quick replay

a quick update, now ‘TRIG’ mode is calculated correctly, i’m ready to implement more features.
http://3.bp.blogspot.com/-_DerzNdtLIQ/TjAEuOkkqJI/AAAAAAAAARY/Ki3DFBgIep4/s1600/trig_edge_fillet_001.png
verts are evenly spaced by using matrix.rotation (thanks to zmj100 for providing a decent example)

This is looking very promising! When it’s able to generate the geometry I’m going to be very eager to test it!

JonathanW, i look forward to testing and using it too!
update screenshot, the panel will contain modes and ‘kappa’ slides (for non symmetric fillet)
http://2.bp.blogspot.com/-IlV_reblWbo/TjGM_WGlq6I/AAAAAAAAARw/UKcxzmKaBgI/s1600/latest_example.png

Some clarification: the ‘kappa’ slide, is an adjustment of the Bezier handles along the original fillet edge. This will produce tangential curve with the option to modulate the curve radius.

http://1.bp.blogspot.com/-rq1u2zMby-Y/TjGuucyk5II/AAAAAAAAASA/UKT0tHKLLcY/s1600/GL-fillet_GUI3.png

short video demo

Wow! im also interested in this ;)Thanx zelffi!

That is starting to look great!

ok, we have the first hints of geometry creation, not quite production ready but worth testing if you are interested.
https://github.com/zeffii/GL-fillet check the newest.

There are still a few small additions to be made,

  • some duplicated code to be shoved into a dedicated function
  • temporarily hijack ctrl+ numpad plus/minus for adding/removing fillet vertices
  • don’t show up in object mode.

@Zeffie,
(chat:) Works, and if you deselect all vertices in edit-mode, Ctrl + and Ctrl - work on your number of vertices for the fillet!

Wow, just nosed in your script!!! Marvelous example of doing such stuff!
Maybe you should ask the blendercoders, if, what you note as ‘ugly’, can be done nice?

Thank you for the kind words pkhg, our conversation early highlighted some lazy assumptions in my code! I will work to fix them or track them down :slight_smile:

The script is now almost final, in that functionally it meets my needs. A note to any dapper user who might use it.

dont

  • do not have more (or less) than 1 vertex selected/active.
  • this vertex must be the corner of 2 edges (not more or less than 2)
  • don’t press ‘check vertex’ if the fillet GL overlay is already displaying, just don’t.
  • do not use ctrl+numpad_plus/numpad_minus to change the number of selected vertices, this interferes with the script
  • do not set fillet ratio to 0.0 [noparse]:slight_smile: [/noparse], what would be the point.

do

  • use any of the GUI controls / sliders.
  • use shift + (numpad_plus/numpad_minus) to increase/decrease the vertex count used for the fillet.
  • hit Enter to make the geometry real.

I do still want to deal with using the cursor to modify the radius, but the code needs a reshuffle.

How do install and use this?

At the moment you run it from text editor.

  • go to text editor
  • hit new
  • paste the content of the .py into it
  • hit run script
  • go to a mesh
  • make sure location/rotation/scale/transforms are applied first. (i know… this is a pain)
  • select one vertex, this vertex must be connected to no less than and no more than 2 other vertices.
  • you might have to toggle editmode/object mode . (yes, also stupid)

It would definitely make sense to extend my script to fillet multiple corners given a radius, throwing an error if one of the edges is too short for a radius fillet. The code is sufficiently modular to allow for it. I want to clean up the script before adding more features.