Working script to create metric bolts on the fly as block items (but some feedback needed)

addBolt.py (10.7 KB)
addBoltv0.2.py (11.5 KB) (sticky settings)
addBolt_v0.3.py (11.7 KB) (multiple bolts)
addBolt_v0.4.py (15.5 KB) (dynamic preview)
addBolt_v0.51.py (16.5 KB) (now works in all unit systems)
addBolt_v0.6.py (16.7 KB)
addBolt_v0.7.py (16.8 KB)

Attached is a working script that adds metric bolts (hex, flat head, socket allen, M3 - M8). I wrote this mainly as a coding exercise.

Would like to get some feedback from more experienced coders if the way i did this with classes is done properly or not. Suggestions for optimizing the structure of the code highly appreciated…

edit: would like to record last selected bolt options to change the defaults at next run…how?

edit: uploaded version 0.4 which that allows to add multiple bolts of the same type to (multiple) surfaces and added preview of the to be placed geometry

If you have additional suggestions for improvement of the functionality that would be nice too.

show the code (or download the above .py file)
from __future__ import division
import rhinoscriptsyntax as rs
from Rhino.ApplicationSettings import *
import Rhino
import math
import scriptcontext as sc
from System.Drawing import Color
import Rhino.RhinoMath as rm


"""
addBolt version 0.7

this script will add a bolt (named block) in Metric size to the rhino document
Bolt must be oriented on a surface and can be aligned with a center point
needed user input : metric size, length, bolt type, (center)point on (poly)surface
to do:
-rewrite Bolt creation in Rhino Common so that redraw can stay on for showing selected center point

changes in v0.2:
- added sticky values to size, length and type to make insertion of multiple bolts of the same type faster
changes in v0.3:
-add multiple bolts of same type to one or multiple surfaces
changes in v0.4:
-added preview during placement
-replaced most rhinoscriptsyntax with Rhino common calls to avoid adding objects to scene too early
changes in v0.5:
-now works in all unit systems
-fixed bug in socket_allen
changes in v0.6:
-refactored spiral creation and made it generic with extra bolt property 'lenght_with_thread'
 which should make it applicable to all kinds of bolts that might get added later
-split user options from main function
changes in v.0.7:
-all options in command line at once (code by Willem Derks)

script written by Gijs de Zwart
www.studiogijs.nl

"""

class Bolt():
    def __init__(self,msize,length):
        diameter         = { 'M3':3.0, 'M4':4.0, 'M5':5.0, 'M6':6.0, 'M8':8.0 }
        pitch            = { 'M3':0.5, 'M4':0.7, 'M5':1.0, 'M6':1.0, 'M8':1.25 }
        self.diameter    = diameter[msize]
        self.length      = length
        self.pitch       = pitch[msize]
        self.name        = msize+"x"+str(length)
        self.breps       = []
        self.curves      = []
        self.msize       = msize
        self.length_with_head = False
    def __repr__(self):
        cls = self.__class__.__name__
        return '{}({}x{})'.format(cls, self.msize, self.length)

    def __str__(self):
        return self.name
        

class hexBolt(Bolt):
    def __init__(self, msize, length):
        Bolt.__init__(self, msize, length)
        head_width       = { 'M3':5.5, 'M4':7.0, 'M5':8.0, 'M6':10, 'M8':13.0 }
        head_height      = { 'M3':2.2, 'M4':3.0, 'M5':3.2, 'M6':4.0, 'M8':5.3 }
        self.head_width  = head_width[msize]
        self.head_height = head_height[msize]
        self.name       += "(DIN993)"

class flatBolt(Bolt):
    def __init__(self,msize,length):
        Bolt.__init__(self, msize, length)
        head_width       = { 'M3':6.0, 'M4':8.0, 'M5':10.0, 'M6':12.0, 'M8':16.0 }
        head_height      = { 'M3':1.7, 'M4':2.3, 'M5':2.8, 'M6':3.3, 'M8':4.4 }
        hex_socket       = { 'M3':2.0, 'M4':2.5, 'M5':3.0, 'M6':4.0, 'M8':5.0 }
        self.head_width  = head_width[msize]
        self.head_height = head_height[msize]
        self.head_dheight= self.head_height-(self.head_width-self.diameter)/2
        self.hex_socket  = hex_socket[msize]
        self.name       += "(DIN7991)"
class DIN912Bolt(Bolt):
    def __init__(self,msize,length):
        Bolt.__init__(self, msize, length)
        head_width       = { 'M3':5.5, 'M4':7.0, 'M5':8.5, 'M6':10.0, 'M8':13.0 }
        head_height      = { 'M3':3.0, 'M4':4.0, 'M5':5.0, 'M6':6.0, 'M8':8.0 }
        hex_socket       = { 'M3':2.5, 'M4':3.0, 'M5':4.0, 'M6':5.0, 'M8':6.0 }
        hex_socket_depth = { 'M3':1.3, 'M4':2.0, 'M5':2.5, 'M6':3.0, 'M8':3.5 }
        self.head_width  = head_width[msize]
        self.head_height = head_height[msize]
        self.hex_socket  = hex_socket[msize]
        self.hex_socket_depth = hex_socket_depth[msize]
        self.name       += "(DIN912)"


def getBoltProperties():
    
    
    #collect previous settings
    msize = sc.sticky["msize"] if sc.sticky.has_key("msize") else 0
    boltlength= sc.sticky["boltlength"] if sc.sticky.has_key("boltlength") else 0
    bolttype= sc.sticky["bolttype"] if sc.sticky.has_key("bolttype") else 0


    get_o = Rhino.Input.Custom.GetOption()
    get_o.SetCommandPrompt("Set Bolt Parameters")

    msizes = ['M3','M4','M5','M6','M8']
    msizelist_index = get_o.AddOptionList("Metric_size", msizes, msize)

    lengths=['12mm','16mm','20mm','25mm','30mm','40mm','50mm','60mm','70mm','80mm']
    lengthlist_index = get_o.AddOptionList("Bolt_Length", lengths, boltlength)

    headstyles=['hex_head','flat_head','socket_allen']
    headstylelist_index = get_o.AddOptionList("Bolt_Type", headstyles, bolttype)

    #accept Enter as an option
    get_o.AcceptNothing(True)

    while True:
        # perform the get operation. This will prompt the user to
        # input a point, but also allow for command line options
        # defined above
        get_rc = get_o.Get()
        if get_o.CommandResult()!= Rhino.Commands.Result.Success:
            return None,None,None
            
        if get_rc==Rhino.Input.GetResult.Nothing:
            pass
            
            
            
        elif get_rc==Rhino.Input.GetResult.Option:
            
            if get_o.OptionIndex() == msizelist_index:
              msize = get_o.Option().CurrentListOptionIndex
              #print 'set ',msizes[msize]
              
            if get_o.OptionIndex() == lengthlist_index:
              boltlength = get_o.Option().CurrentListOptionIndex
              #print 'set ',lengths[boltlength]
              
            if get_o.OptionIndex() == headstylelist_index:
              bolttype = get_o.Option().CurrentListOptionIndex
              #print 'set ',headstyles[bolttype]
            continue
        
        break
        
    
    
    sc.sticky["msize"] = msize
    sc.sticky["boltlength"] = boltlength
    sc.sticky["bolttype"] = bolttype
    
    
    
    size = msizes[msize]
    length = lengths[boltlength]
    length = int(length[:-2])
    headstyle = headstyles[bolttype]
    
    
    return size, length, headstyle


def createBolt():
    # *************************************************
    # *********** CREATE THE BOLT GEOMETRY ************
    # *************************************************
    size, length, headstyle = getBoltProperties()
    if headstyle=='hex_head':
        #create a hex bolt object
        bolt      = hexBolt(size, length)
        #create the head
        radius    = bolt.head_width/2/math.cos(math.pi/6)#calculate radius of circle that circumscribes the hexagon
        hexagon = Rhino.Geometry.Circle(Rhino.Geometry.Plane.WorldXY, radius)
        hexagon = hexagon.ToNurbsCurve(1,6)
        head = Rhino.Geometry.Extrusion
        vec = Rhino.Geometry.Vector3d(0,0,bolt.head_height)
        head = head.CreateExtrusion(hexagon, vec)
        head = head.ToBrep()
        head = head.CapPlanarHoles(sc.doc.ModelAbsoluteTolerance)

        #create threaded part
        circle = Rhino.Geometry.Circle(Rhino.Geometry.Plane.WorldXY, bolt.diameter/2)
        thread = Rhino.Geometry.Cylinder(circle,-bolt.length).ToBrep(True, True)

        #add objects to bolt
        bolt.breps.append(head)
        bolt.breps.append(thread)

        #add cosmetic thread
        addCosmeticThread(bolt)
        
        #scale objects
        bolt = setScale(bolt)
              
        addBolt(bolt)

    if headstyle=='flat_head':
        #create a flat head bolt object and some helper points
        bolt      = flatBolt(size,length)
        bolt.length_with_head = True
        point1    = Rhino.Geometry.Point3d(0,0,0)
        point2    = Rhino.Geometry.Point3d(0,0,-bolt.head_height)#position of head end
        plane2    = Rhino.Geometry.Plane(point2, Rhino.Geometry.Plane.WorldXY.ZAxis)
        point3    = Rhino.Geometry.Point3d(0,0,-bolt.head_dheight)#position of edge start
        plane3    = Rhino.Geometry.Plane(point3, Rhino.Geometry.Plane.WorldXY.ZAxis)
        
        #create the hexagon socket
        radius    = bolt.hex_socket/2/math.cos(math.pi/6)#calculate radius of circle that circumscribes the hexagon
        hexagon = Rhino.Geometry.Circle(Rhino.Geometry.Plane.WorldXY, radius)
        hexagon = hexagon.ToNurbsCurve(1,6)
        
        socket = Rhino.Geometry.Extrusion
        
        vec = Rhino.Geometry.Vector3d(0,0,-bolt.head_height)
        socket = socket.CreateExtrusion(hexagon, vec)
        socket = socket.ToBrep()

        #create the head
        circle1   = Rhino.Geometry.Circle(plane2,bolt.diameter/2)
        circle2   = Rhino.Geometry.Circle(plane3,bolt.head_width/2)
        circle3   = Rhino.Geometry.Circle(Rhino.Geometry.Plane.WorldXY, bolt.head_width/2)
        edge      = Rhino.Geometry.Cylinder(circle2, bolt.head_dheight).ToBrep(False,False)
        
        #create top face
        curvelist = Rhino.Collections.CurveList()
        curvelist.Add(hexagon)
        curvelist.Add(circle3)
        topface = Rhino.Geometry.Brep.CreatePlanarBreps(curvelist)[0]
        no_pt=Rhino.Geometry.Point3d.Unset
        straight_loft=Rhino.Geometry.LoftType.Straight
        head = Rhino.Geometry.Brep.CreateFromLoft([circle1.ToNurbsCurve(),circle2.ToNurbsCurve()],no_pt,no_pt,straight_loft,False)[0] 
        circle = Rhino.Geometry.Circle(plane2, bolt.diameter/2)
        thread = Rhino.Geometry.Cylinder(circle,-bolt.length+bolt.head_height).ToBrep(True, False)
        tol = sc.doc.ModelAbsoluteTolerance
        
        #stitch everything together
        head.Join(edge, tol, False)
        head.Join(topface, tol, False)
        head.Join(thread,tol, False)
        head.Join(socket, tol, True)
        head = head.CapPlanarHoles(sc.doc.ModelAbsoluteTolerance)
        bolt.breps.append(head)

        #add cosmetic thread
        addCosmeticThread(bolt)
        
        #scale objects
        bolt = setScale(bolt)
        
        addBolt(bolt)

    if headstyle=='socket_allen':
        #create a flat head bolt object and some helper points
        bolt      = DIN912Bolt(size,length)
        point1    = Rhino.Geometry.Point3d(0,0,0)
        point2    = Rhino.Geometry.Point3d(0,0,bolt.head_height)#position of head end
        plane2    = Rhino.Geometry.Plane(point2, Rhino.Geometry.Plane.WorldXY.ZAxis)
        point3    = Rhino.Geometry.Point3d(0,0,(bolt.head_height-bolt.hex_socket_depth))
        plane3    = Rhino.Geometry.Plane(point3, Rhino.Geometry.Plane.WorldXY.ZAxis)
        #create the hexagon socket
        radius    = bolt.hex_socket/2/math.cos(math.pi/6)#calculate radius of circle that circumscribes the hexagon
        hexagon = Rhino.Geometry.Circle(plane2, radius)
        hexagon = hexagon.ToNurbsCurve(1,6)
        
        socket = Rhino.Geometry.Extrusion
        
        vec = Rhino.Geometry.Vector3d(0,0,-bolt.hex_socket_depth)
        socket = socket.CreateExtrusion(hexagon, vec)
        socket = socket.ToBrep()

        #create the head
        
        circle   = Rhino.Geometry.Circle(plane2, bolt.head_width/2)
        head     = Rhino.Geometry.Cylinder(circle, -bolt.head_height).ToBrep(False,False)
        
        #create threaded part
        circle1   = Rhino.Geometry.Circle(Rhino.Geometry.Plane.WorldXY, bolt.diameter/2)
        thread    = Rhino.Geometry.Cylinder(circle1, -bolt.length).ToBrep(True, False)

        
        #create top face
        curvelist = Rhino.Collections.CurveList()
        curvelist.Add(hexagon)
        curvelist.Add(circle)
        topface = Rhino.Geometry.Brep.CreatePlanarBreps(curvelist)[0]
        
        circle3 = Rhino.Geometry.Circle(Rhino.Geometry.Plane.WorldXY,bolt.head_width/2)
        curvelist1= Rhino.Collections.CurveList()
        curvelist1.Add(circle1)
        curvelist1.Add(circle3)
        bottomface = Rhino.Geometry.Brep.CreatePlanarBreps(curvelist1)[0]
        
        tol=sc.doc.ModelAbsoluteTolerance
        #stitch everything together:
        head.Join(topface, tol, False)
        head.Join(bottomface, tol, False)
        head.Join(socket, tol, False)
        head.Join(thread, tol, True)
        head = head.CapPlanarHoles(tol)
        bolt.breps.append(head)
        
        #add cosmetic thread
        addCosmeticThread(bolt)
        
        #scale objects
        bolt = setScale(bolt)
        
        addBolt(bolt)
    

def addBolt(bolt):
    # ***********************************************
    # ******** ADDING THE BOLT TO THE SCENE *********
    # ***********************************************
    old_osnap_state = ModelAidSettings.OsnapModes #record Osnap state to reset later
    
    bolts=0
    while True:
        Rhino.UI.MouseCursor.SetToolTip("select surface or face")
        # this function ask the user to select a point on a surface to insert the bolt on
        # Surface to orient on
        gs = Rhino.Input.Custom.GetObject()
        gs.SetCommandPrompt("Surface to orient on")
        gs.GeometryFilter = Rhino.DocObjects.ObjectType.Surface
        gs.Get()
        if gs.CommandResult()!=Rhino.Commands.Result.Success:
            ModelAidSettings.OsnapModes = old_osnap_state #reset to previous Osnap state
            print str(bolts) + " "+bolt.__repr__() + " bolt(s) added to the document"
            Rhino.UI.MouseCursor.SetToolTip("")
            
            return

        objref = gs.Object(0)
        # get selected surface object
        obj = objref.Object()
        if not obj: 
            ModelAidSettings.OsnapModes = old_osnap_state #reset to previous Osnap state
            
            print str(bolts) + " "+bolt.__repr__() + " bolt(s) added to the document"
            return

        # get selected surface (face)
        global surface
        surface = objref.Surface()
        if not surface: return Rhino.Commands.Result.Failure
        # Unselect surface
        obj.Select(False)
    
        # Point on surface to orient to / activate center Osnap
        cen()

        gp=Rhino.Input.Custom.GetPoint()
        gp.SetCommandPrompt("Point on surface to orient to")
        gp.Constrain(surface, False)
        #display the geometry to be created
        gp.DynamicDraw+=drawbreps
        gp.Get()
        
        if gp.CommandResult()!=Rhino.Commands.Result.Success:
            ModelAidSettings.OsnapModes = old_osnap_state #reset to previous Osnap state
            print str(bolts) + " "+bolt.__repr__() + " bolt(s) added to the document"
            return 

        getrc, u, v = surface.ClosestPoint(gp.Point())
        if getrc:
            getrc, target_plane = surface.FrameAt(u,v)
            if getrc:
                # Build transformation
                source_plane = Rhino.Geometry.Plane.WorldXY
                xform = Rhino.Geometry.Transform.PlaneToPlane(source_plane, target_plane)
                # Do the transformation
                
                objs=bolt.breps
                rhobj=[]
                for brep in objs :
                    rhobj.append(sc.doc.Objects.AddBrep(brep)) 
                for curve in bolt.curves:
                    rhobj.append(sc.doc.Objects.AddCurve(curve))
                rs.AddBlock(rhobj,[0,0,0],bolt.name, True)
                newbolt = rs.InsertBlock2(bolt.name, xform)
                bolts +=1

def addCosmeticThread(bolt):
        
        normal = Rhino.Geometry.Point3d(0,1,0)
        if  not bolt.length_with_head:
            start = Rhino.Geometry.Point3d(0,0,0)
            turns=bolt.length/bolt.pitch
            lengthvec = Rhino.Geometry.Vector3d(0,0,-bolt.length)
        else:
            start = Rhino.Geometry.Point3d(0,0,-bolt.head_height)
            turns = (bolt.length-bolt.head_height)/bolt.pitch
            lengthvec = Rhino.Geometry.Vector3d(0,0,-bolt.length+bolt.head_height)
        radius = bolt.diameter/2
        spiral = Rhino.Geometry.NurbsCurve.CreateSpiral(start, lengthvec,normal,bolt.pitch,turns,radius, radius)
        bolt.curves.append(spiral)

def setScale(bolt):
    
    scale = rm.UnitScale(sc.doc.ModelUnitSystem.Millimeters, sc.doc.ModelUnitSystem)
    xf=Rhino.Geometry.Transform.Scale(Rhino.Geometry.Plane.WorldXY, scale,scale,scale)
    for brep in bolt.breps:
        brep.Transform(xf)
    for curve in bolt.curves:
        curve.Transform(xf)
    #copy to displaybreps for preview
    global displaybreps
    displaybreps = bolt.breps
     
    return bolt
                
def drawbreps(gp, args ):
    getrc, u, v = surface.ClosestPoint(args.CurrentPoint)
    if getrc:
        getrc, target_plane = surface.FrameAt(u,v)
    xf = Rhino.Geometry.Transform.PlaneToPlane(Rhino.Geometry.Plane.WorldXY,target_plane)
    
    for brep in displaybreps:
        new = brep.Duplicate()
        
        new.Transform(xf)
        args.Display.DrawBrepWires(new, Color.Aquamarine,2)

def cen():
    #this function turns off all Osnaps except center Osnap
    cen = Rhino.ApplicationSettings.OsnapModes.Center
    Rhino.ApplicationSettings.ModelAidSettings.Osnap = True
    Rhino.ApplicationSettings.ModelAidSettings.OsnapModes = cen

if __name__ == "__main__":
    createBolt()
1 Like

a flip option would be interesting while selecting surface to orient on. there is one little bug (mac rhino 5 something) when i am supposed to select the surface to orient on and i zoom in with the mouse wheel it hooks up, i have to cancel it completely. regarding code i cant say much.

could be but then it would only make sense if I have a preview of the bolt while placing it…

yes would be nice

Hi,

Looks good!

You can use the sticky dictionary in scriptcontext to save the settings. It would be a good idea to use a main() function rather than launching the function in line

Is there some repetition, eg in thread creation? Can you move that part out of the if blocks or into a function?

Thanks! I’ll look into that.

I was currently looking at a way to show a preview but this I still find hard, even though I managed to make it work with a few scripts before.

you mean that I put this around the main function?

if __name__ == "__main__":
   CreateBolt()

Yup.

It would also be a good idea to add from __future__ import division to the top as you do some division, sometimes with integers. For instance 3/2 evaluates to 1 here in legacy Python land :open_mouth:

thanks @Dancergraham I’ve added your suggestions and uploaded in first post

Hello,

You could also add the following lines to you Bolt class to get a nice string and repr(for debugging) representation

        self.msize = msize

    def __repr__(self):
        cls = self.__class__.__name__
        return '{}({}, {})'.format(cls, self.msize, self.length)

    def __str__(self):
        return self.name

I’ve uploaded a new version which adds preview and option to add multiple bolts of the same type

I had to use a global variable in order to get the display thing working. Don’t know how I can otherwise send the variables I need over to the Dynamicdraw function.

I use the BoltGen plugin in Rhino. Kind of surprised it doesn’t have a corresponding Grasshopper component. Seems like it’d be easy to do.

Although I’ve mainly written this as an excercise, and BoltGen is in many ways more complete, have you tried the script I wrote? It’s more Rhino style (no dialog boxes), and allows you to insert the bolts in place rather than only at the origin. If you have to add lots of bolts of the same type this will work much faster.

1 Like

I’ll definitely check it out. I tend to use only one or two screw types in a drawing, so boltgen, and then putting them on separate layers and placing them as blocks has always worked for me. But if this is faster, that’s great.

I wonder whether there is a built in Rhinocommon collection type from which you could subclass Bolt. :thinking:

Adding the while loop adds the possibility of creating a bolt manifest, for instance using a counter

from collections import Counter

manifest = Counter()

...

manifest[bolt.name] += 1

...

for bolt_type, no in manifest.items():
    print bolt_type,no

Wow - I think you are demonstrating two of Python’s superpowers here - rapid development and easy sharing and readability of code.

In terms of the general structure of the script, your CreateBolt() function has definitely become too long - almost everything is happening inside one big function, including the addBolt() call at the end.

You can create a new main function as I show below, and then split CreateBolt into a number of different steps as well as placing some of the logic from CreateBolt into separate functions, possibly with arguments where necessary, e.g. to handle different diameters / lengths / bolt types. There is plenty of scope to reduce repetition in the code (DRY coding principle), for instance the ‘add cosmetic spiral’ section seems ideal for splitting out into a separate add_cosmetic_spiral() function. You do not add the spiral to the bolt curves in all of the sections. Is that deliberate? If not, this is the type of inconsistency which you can avoid by refactoring. :slight_smile:

I would also attempt to separate the loop logic out from the geometry creation / adding, for instance by putting a while loop in the main function and calling the necessary functions from that.

This should make the code easier to read, debug and maintain. Well done on the work you’ve done - this is great !

def main():
    bolt = CreateBolt()
    while True:
        addBolt(bolt)

thanks @Dancergraham for your great feedback, this is really helpful. The main function has certainly become too long, I agree.

I don’t quite get what your goal is with this, can you explain? Because as it is right now, one can only add one type of bolt at the same time. I added some code that keeps track of amount of bolts added though and reports the type using the code you posted earlier:

bolts=0
while True:
   ...
      if not ...#no more bolts / esc
         print str(bolts) + " "+bolt.__repr__() + " bolt(s) added to the document"
1 Like

You’re very welcome!

Yes that does the trick. for the counter to become even more useful you would need to store the values between applications, for instance in a user dictionary or a sticky. One option would be to import json and save json.dumps(manifest) into a sticky dictionary, reloading it on the next use. (I havent tested this).