Pattern maker with dynamic preview

I picked up this script that I once made in rhino script and enhanced it with new options and dynamic preview.
I think the usage is quite self explanatory.

There are a few more things I want to implement as you can see in the comments, but I’m also open for others’ suggestions.

"""
This script allows you draw make patterns from multiple curves at once.
It assumes all curves are more or less the same size, and have a reasonable
width and height to begin with.
It will then randomly use the curves to make a pattern.
The pattern is then applied to a boundary curve that must be closed.
It uses getpoint in order to enable dynamic redraw when changing the pattern 
settings.

***********************************
* script written by Gijs de Zwart *
* www.studiogijs.nl               *
* April, 2016                     *
***********************************


implemented:

* user selects 1 or more curves to use as pattern
* user selects boundary to fill (closed curve)
* boundary gets filled with a preview of the endresult
* curves outside the boundary are removed
* curves that intersect with the boundary are removed
* pattern looks at the size of the curve(s) (boundingbox) to get correct spacing
* possibility to enter a margin from the border of the boundary
* possibility to give curve(s) a random scale/ rotation
* possibility to offset each row

to do:
possibility to scale the curve(s) that are used in the boundary
possibility to flip curve(s)
error checking
"""
import rhinoscriptsyntax as rs
import Rhino.Geometry.Curve as Curve
import random as rand
import Rhino
import System.Drawing.Color as Color
import scriptcontext as sc

def RunCommand():

    class fillcrv():
        def __init__(self,crv):
            bbox=crv.GetBoundingBox(True)
            self.crv=crv
            self.width=bbox.Max.X-bbox.Min.X
            self.height=bbox.Max.Y-bbox.Min.Y
            self.h_space=self.width
            self.v_space=self.height
            self.lower=bbox.Min


    def getSpace(offset, space):
        #returns horizontal and vertical space and row offset (skew)
        h_space=0
        v_space=0
        for crv in curves:
            if h_space<crv.h_space:
                h_space=crv.h_space

            if v_space<crv.v_space:
                v_space=crv.v_space
        h_space+= h_space*space/100
        v_space+= v_space*space/100
        h_skew=h_space*offset/100
        return [h_space,v_space,h_skew]

    def CalcStartCondition(boundary):
        #returns width, height and startpoint transformation
        #and changes boundary to offset margin if requested

        bbox=boundary.GetBoundingBox(True)
        cen_boundary=bbox.Center
        plane=Rhino.Geometry.Plane.WorldXY
        plane.Origin = bbox.Min

        b_width=bbox.Max.X-bbox.Min.X

        b_height=bbox.Max.Y-bbox.Min.Y

        #calculate translation vector from curve to boundary
        start=bbox.Min-curves[0].lower

        return [b_width, b_height, start, boundary]

    def CreatePatternCurves(v_amount, h_amount, v_space, h_space, h_skew, start, boundary):
        #creates patterned curves in rectangular grid
        trans=start
        reset=start[0]
        odd=0
        i=0

        patterncurves=[]
        for y in range(0,v_amount):

            #reset transformation in x-direction
            trans[0]=reset

            if odd%2==1:
                trans[0]+=h_skew
            odd+=1
            for x in range(0,h_amount):

                i=rand.randint(0,len(curves)-1)
                copy=curves[i].crv.Duplicate()
                copy.Translate(trans)
                cen=copy.GetBoundingBox(True).Center

                random = optRandom.CurrentValue

                if random and copy!=None:
                    rnd=rand.randint(90,110)/100
                    rot_rnd=rand.uniform(-.1,.1)
                    scale=Rhino.Geometry.Transform.Scale(cen,rnd)
                    rotation=Rhino.Geometry.Transform.Rotation(rot_rnd, cen)
                    copy.Transform(scale)
                    copy.Transform(rotation)

                #check if point to copy is inside boundary and margin
                margin=optMargin.CurrentValue

                if boundary.Contains(cen) == Rhino.Geometry.PointContainment.Inside:

                    #check if it intersects with the boundarycurve:
                    #uses intersection tolerance to leave a margin
                    intersection_tolerance = margin+0.01
                    overlap_tolerance = 0.0
                    intersections = Rhino.Geometry.Intersect.Intersection.CurveCurve(
                                        boundary,
                                        copy,
                                        intersection_tolerance,
                                        overlap_tolerance
                                        )
                    if not intersections:
                        patterncurves.append(copy)

                #increment horizontal transform
                trans[0]+=h_space

            #increment vertical transform
            trans[1]+=v_space

        return patterncurves

    def fillBoundary(crv,boundary, offset, space, random):
        #returns pattern curves within boundary
        b_width, b_height, start, boundary = CalcStartCondition(boundary)
        #determine horizontal and vertical spacing and skew
        h_space,v_space,h_skew = getSpace(offset, space)

        # calculate amount of curves needed
        h_amount=int(b_width/h_space)+1

        v_amount=int(b_height/v_space)+1
        #create the pattern
        patterncurves=CreatePatternCurves(v_amount, h_amount, v_space, h_space, h_skew, start, boundary)
        return patterncurves


    def OnDynamicDraw(sender, e):
        #try:
        random=optRandom.CurrentValue
        offset=optOffset.CurrentValue
        space=optSpace.CurrentValue
        h_space,v_space,h_skew = getSpace(offset, space)
        patterncurves = fillBoundary(curves, boundary, offset, space, random)

        for curve in patterncurves:
            e.Display.DrawCurve(curve, Color.LightCyan, 1)
        """
        except Exception as ex:
            template = "An exception of type {0} occured. Arguments:\n{1!r}"
            message = template.format(type(ex).__name__, ex.args)
            print message
            return
        """

    def addPatternToRhino():
        if boolCanOffset==False:
            print "couldn't offset curve, used boundary instead"
        patterncurves = fillBoundary(curves, boundary, offset, space, random)
        for i in range(0, len(patterncurves)):
            sc.doc.Objects.AddCurve(patterncurves[i])
        sc.doc.Views.Redraw()

    boundary = rs.GetObject("select boundarycurve to fill", rs.filter.curve)
    if not boundary:
        return
    boundary = rs.coercecurve(boundary)
    crvs=rs.GetObjects("select curves to use as pattern", rs.filter.curve)
    if not crvs:
        return
    if not boundary.IsClosed:
        return

    curves=[]
    for crv in crvs:
        crv=rs.coercecurve(crv)
        crv=fillcrv(crv)
        curves.append(crv)

    gp=Rhino.Input.Custom.GetPoint()
    gp.SetCommandPrompt("pattern properties")

    optMargin=Rhino.Input.Custom.OptionDouble(0,0,100)
    gp.AddOptionDouble("margin",optMargin)

    optRandom = Rhino.Input.Custom.OptionToggle(True, "Off", "On")
    gp.AddOptionToggle("add_randomness", optRandom)

    optOffset = Rhino.Input.Custom.OptionInteger(50,0,100)
    gp.AddOptionInteger("offset_percentage",optOffset)

    optSpace = Rhino.Input.Custom.OptionInteger(50,0,500)
    gp.AddOptionInteger("space_percentage",optSpace)
    gp.DynamicDraw+=OnDynamicDraw

    #start collecting user options
    while True:
        gp.Get()
        if gp.Result()==Rhino.Input.GetResult.Option:
            continue
        break

    random=optRandom.CurrentValue
    offset=optOffset.CurrentValue
    margin=optMargin.CurrentValue
    space=optSpace.CurrentValue
    boolCanOffset=True

    addPatternToRhino()
if( __name__ == "__main__" ):
    RunCommand()

3 Likes

Nice work! I couldn’t understand how to control the rotation or the scale of the objects. I also noticed that when I used two different curves they sometimes overlap (see the image bellow). Another thing I noticed is that if you use two curves instead of one there is more randomness.

Cheers!

@Filipe_Brandao Make sure to place the curves you want to array on the same spot. This is something I want to fix. So for now make sure the curves you want to array are in more or less the same origin.
Next version of the script will allow separate controls for x and y spacing.

@Filipe_Brandao

below an update. Curves no longer need to be on the same spot :relaxed:.



import rhinoscriptsyntax as rs
import Rhino.Geometry.Curve as Curve
import random as rand
import Rhino
import System.Drawing.Color as Color
import scriptcontext as sc

def RunCommand():

    class fillcrv():
        def __init__(self,crv):
            bbox=crv.GetBoundingBox(True)
            self.crv=crv
            self.width=bbox.Max.X-bbox.Min.X
            self.height=bbox.Max.Y-bbox.Min.Y
            self.h_space=self.width
            self.v_space=self.height
            self.lower=bbox.Min


    def getSpace(offset, x_space, y_space):
        #returns horizontal and vertical space and row offset (skew)
        h_space=0
        v_space=0
        for crv in curves:
            if h_space<crv.h_space:
                h_space=crv.h_space

            if v_space<crv.v_space:
                v_space=crv.v_space
        h_space+= h_space*x_space/100
        v_space+= v_space*y_space/100
        
        if h_space==0:
            h_space=10
        if v_space==0:
            v_space=10
        h_skew=h_space*offset/100
        return [h_space,v_space,h_skew]

    def CalcStartCondition(boundary):
        #returns width, height and startpoint transformation
        #and changes boundary to offset margin if requested

        bbox=boundary.GetBoundingBox(True)
        cen_boundary=bbox.Center
        plane=Rhino.Geometry.Plane.WorldXY
        plane.Origin = bbox.Min

        b_width=bbox.Max.X-bbox.Min.X

        b_height=bbox.Max.Y-bbox.Min.Y

        #calculate translation vector from curve to boundary

        start=bbox.Min-curves[0].lower

        return [b_width, b_height, start, boundary]

    def CreatePatternCurves(v_amount, h_amount, v_space, h_space, h_skew, start, boundary):
        #creates patterned curves in rectangular grid
        trans=start
        reset=start[0]
        odd=0
        i=0
        scfactor=0.1
        patterncurves=[]
        for y in range(0,v_amount):

            #reset transformation in x-direction
            trans[0]=reset

            if odd%2==1:
                trans[0]+=h_skew
            odd+=1
            i=0
            CurveCount=len(curves)-1
            random = optRandom.CurrentValue
            for x in range(0,h_amount):
                if random:
                    v=rand.randint(0,CurveCount)
                    copy=curves[v].crv.Duplicate()
                else:
                    
                    if i>CurveCount:
                        i=0
                    copy=curves[i].crv.Duplicate()
                copy.Translate(trans)
                cen=copy.GetBoundingBox(True).Center
                
                if optScaleCurves.CurrentValue==True:
                    
                    scfactor=0.2+0.8/v_amount*y
                    scale=Rhino.Geometry.Transform.Scale(cen,scfactor)
                    copy.Transform(scale)
                
                

                #check if point to copy is inside boundary and margin
                margin=optMargin.CurrentValue

                if boundary.Contains(cen) == Rhino.Geometry.PointContainment.Inside:

                    #check if it intersects with the boundarycurve:
                    #uses intersection tolerance to leave a margin
                    intersection_tolerance = margin+0.01
                    overlap_tolerance = 0
                    intersections = Rhino.Geometry.Intersect.Intersection.CurveCurve(
                                        boundary,
                                        copy,
                                        intersection_tolerance,
                                        overlap_tolerance
                                        )
                    if intersections.Count==0:
                        patterncurves.append(copy)

                #increment horizontal transform
                trans[0]+=h_space
                i+=1

            #increment vertical transform
            trans[1]+=v_space

        return patterncurves

    def fillBoundary(crv,boundary, offset, x_space, y_space, random):
        #returns pattern curves within boundary
        b_width, b_height, start, boundary = CalcStartCondition(boundary)
        #determine horizontal and vertical spacing and skew
        h_space,v_space,h_skew = getSpace(offset, x_space, y_space)

        # calculate amount of curves needed
        h_amount=int(b_width/h_space)+1

        v_amount=int(b_height/v_space)+1
        #create the pattern
        patterncurves=CreatePatternCurves(v_amount, h_amount, v_space, h_space, h_skew, start, boundary)
        return patterncurves


    def OnDynamicDraw(sender, e):

            random=optRandom.CurrentValue
            offset=optOffset.CurrentValue
            x_space=optSpaceX.CurrentValue
            y_space=optSpaceY.CurrentValue
            h_space,v_space,h_skew = getSpace(offset, x_space, y_space)
            patterncurves = fillBoundary(curves, boundary, offset, x_space, y_space, random)
    
            for curve in patterncurves:
                e.Display.DrawCurve(curve, Color.LightCyan, 1)
            
    def addPatternToRhino():
        patterncurves = fillBoundary(curves, boundary, offset, x_space, y_space, random)
        for i in range(0, len(patterncurves)):
            sc.doc.Objects.AddCurve(patterncurves[i])
        sc.doc.Views.Redraw()

    boundary = rs.GetObject("select boundarycurve to fill", rs.filter.curve)
    if not boundary:
        return
    boundary = rs.coercecurve(boundary)
    crvs=rs.GetObjects("select curves to use as pattern", rs.filter.curve)
    if not crvs:
        return
    if not boundary.IsClosed:
        return

    curves=[]
    origin=Rhino.Geometry.Point3d(0,0,0)
    for crv in crvs:
        crv=rs.coercecurve(crv)
        bb=crv.GetBoundingBox(True)
        center=bb.Center
        trans=origin-center
        crv.Translate(trans)
        #sc.doc.Objects.AddCurve(crv)

        c=crv.Duplicate()
        c=fillcrv(c)
        curves.append(c)

    gp=Rhino.Input.Custom.GetPoint()
    gp.SetCommandPrompt("pattern properties")

    optMargin=Rhino.Input.Custom.OptionDouble(0,0,100)
    gp.AddOptionDouble("margin",optMargin)

    
    optRandom = Rhino.Input.Custom.OptionToggle(False, "Off", "On")
    gp.AddOptionToggle("add_randomness", optRandom)
    
    optScaleCurves = Rhino.Input.Custom.OptionToggle(False, "Off", "On")
    gp.AddOptionToggle("scale_curves", optScaleCurves)

    optOffset = Rhino.Input.Custom.OptionInteger(0,-200,200)
    gp.AddOptionInteger("offset_percentage",optOffset)

    optSpaceX = Rhino.Input.Custom.OptionInteger(0,-50,500)
    gp.AddOptionInteger("x_space_percentage",optSpaceX)
    
    optSpaceY = Rhino.Input.Custom.OptionInteger(0,-50,500)
    gp.AddOptionInteger("y_space_percentage",optSpaceY)
    gp.DynamicDraw+=OnDynamicDraw
    patterncurves=[]
    #start collecting user options
    while True:
        gp.Get()
        if gp.Result()==Rhino.Input.GetResult.Option:
            continue
        break
    
    random=optRandom.CurrentValue
    offset=optOffset.CurrentValue
    margin=optMargin.CurrentValue
    x_space=optSpaceX.CurrentValue
    y_space=optSpaceY.CurrentValue
    boolCanOffset=True

    addPatternToRhino()
if( __name__ == "__main__" ):
    RunCommand()

3 Likes

Great work! I’ll look into your code to see if I can learn something. I have written a script in C# for grasshopper that does similar stuff for a project that I was working on, but without inscribing the pattern in a shape or randomizing it. I did those latter parts with grasshopper components.

private void RunScript(Curve curva, int repX, int repY, double distX, double distY, double racioX, double racioY, ref object A)
  {
    List<Curve> padrao = new List<Curve>(); //lista com o padrão de poligonos

    double propX = distX * racioX;
    double propY = distY * racioY;

    for(int i = 0; i < repX; i++){
      for(int j = 0; j < repY; j++){
        Curve tempCurva = curva.DuplicateCurve();
        tempCurva.Translate(distX * i + (propX * (j % 2)), distY * j + (propY * (i % 2)), 0);
        padrao.Add(tempCurva);
      }
    }

    A = padrao;

  }