Rhino surfaces to Bitmap Gradients

In All, Python, Rhino by admin0 Comments

Create and paint bitmap from rhino input  in python. An example with gradients.

In rhino you need to draw:

  • Layer “Frame” > contains a rectangle with the exact size of the bitmap you want to make (1 unit = 1 pixel). Start with lower left corner at 0,0 because I was lazy to add more scripting to handle this.
  • Layer “ZONE_III” > contains the planar surfaces that contain the “end color” of the gradient – each surface as a unique name, matching a name assigned to the corresponding gradient zone.
  • Layer “ZONE_II” > contains the gradients zones (XY-planar untrimmed nurbs surfaces).

If a gradient zone is a “donut” make it so:

Loft surface between the 2 borders, starting from the interior one

If it is a “U” make it so:

Edge surface between the 2 edge curves, starting from the interior one

  • Layer “curve_g” > contains a curve that will guide the transition of the color withing the gradient (typically if it is a straight line going up diagonally, the gradient will be linear).
  • Obviously don’t put anything else on those layers.

In this example I use the script to make photoshop masks for a panorama made of 6 faces of a cube. The bitmap is the unwrapped cube. In this case, when drawing the contours of the gradients in rhino I had to be cautious that the edges match when folding back the box.

What the script does.

  • Find which pixels are in which surfaces from ZONE_III and ZONE_II.
  • Paint pixels from ZONE_III with color 1.
  • For the pixels in ZONE_II, evaluate their location on the surface along U, get a parameter between 0 and 1 (color 2 and color 1), modify this parameter if the gradient is not linear, and paint the pixels from ZONE_II in a computed color between color 2 and 1 related to the modified parameter.
  • Rest of the bitmap is created and left transparent.
#-------------------------------------------------------------------------------
# Name:        bitmap_gradient_from_loft
# Purpose:     make gradients from loft surfaces to use in Photoshop as masks for instance
# Author:      Marie Prunault
# Created:     26-07-2016
# Copyright:   (c) Marie Prunault 2016
#-------------------------------------------------------------------------------

import Rhino as rh
import rhinoscriptsyntax as rs
import scriptcontext as sc
import math
import System.Drawing.Color as col
import System.Drawing.Bitmap as btmp
import os
import System.Drawing.Imaging.ImageFormat as imgg

#---------------------------CONSTANTS
tol=sc.doc.ModelAbsoluteTolerance
#------------------------------------------------GENERIC STUFF BUT ACTUALLY I DON'T NEED ALL IN THE END
#I normally get those functions from my self-made library, always at hand
def dump(obj):
    '''print all members for instance of a class'''
    for attr in dir(obj):
        print "obj.%s = %s" % (attr, getattr(obj, attr))
def rhob_to_rcob(rhinoob):
    '''from GUID to rhino Geometry base, almost ready'''
    if isinstance(rhinoob,list):
        geoms=[]
        for rhinoobject in rhinoob:
            objref= rh.DocObjects.ObjRef(rhinoobject)
            geomel = objref.Geometry()
            geoms.append(geomel)
        return geoms
    else:
        objref= rh.DocObjects.ObjRef(rhinoob)
        geom = objref.Geometry()
        return geom
def BoundingBox( rhobjects, plane=rh.Geometry.Plane.WorldXY, in_world_coords=True ):
    xform=rh.Geometry.Transform.ChangeBasis(rh.Geometry.Plane.WorldXY,plane)
    bbox=rh.Geometry.BoundingBox.Empty
    for object in rhobjects:
        objectbbox=object.GetBoundingBox(xform)
        if objectbbox: bbox = rh.Geometry.BoundingBox.Union(bbox,objectbbox)
    corners = bbox.GetCorners()
    if in_world_coords and plane is not None:
        plane_to_world = rh.Geometry.Transform.ChangeBasis(plane, rh.Geometry.Plane.WorldXY)
        count=0
        for pt in corners:
            pt.Transform(plane_to_world)
            corners[count]=pt
            count+=1
        return corners,bbox
    return corners,bbox
def linecrvintersect(line,curve):
    global tol
    cbox=rh.Geometry.BoundingBox.Empty
    bbox=BoundingBox([curve])[1]
    bool,ends=rh.Geometry.Intersect.Intersection.LineBox(line,bbox,tol)
    linecurve=rh.Geometry.LineCurve(line.PointAt(ends[0]),line.PointAt(ends[1]))
    return linecurve,rh.Geometry.Intersect.Intersection.CurveCurve(linecurve,curve,tol,tol)
def pttopix(point3d):
    return rh.Geometry.Point3d(int(math.floor(point3d[0])),int(math.floor(point3d[1])),0)
def collect_overlaps(curve,curves):
    global tol
    newcurves=[]
    for crv in curves:
        intersection=rh.Geometry.Intersect.Intersection.CurveCurve(curve,crv,tol,tol)
        if intersection.Count>0:
            for i in intersection:
                try:
                    newcurves.append(curve.Trim(i.OverlapA))
                except:
                    pass
    if len(newcurves)>0:
        jcurves=rh.Geometry.Curve.JoinCurves(newcurves)
        return jcurves
    else:
        return newcurves
def dupborder(brep, join=True):
    global tol
    curves=brep.DuplicateNakedEdgeCurves(True,True)
    if not join:
        return curves
    else:
        return rh.Geometry.Curve.JoinCurves(curves,tol*2.1)
def propersolidfromextrusion(badbrep):
    '''badbrep is not that bad but comes from extruding brep face and gives wrong results when inquiring if a point is inside'''
    #sc.doc = rh.RhinoDoc.ActiveDoc#UNCOMMENT IN GH - COMMENT IN RHINO
    k=sc.doc.Objects.AddBrep(badbrep)
    cleanbrep=rhob_to_rcob([k])[0]
    rs.DeleteObject(k)
    #sc.doc = ghdoc#UNCOMMENT IN GH - COMMENT IN RHINO
    return cleanbrep
def remap_parameter_from_curve(nparameter,cbox):
    start=(cbox.bbox[0][0]+((rh.Geometry.Vector3d(cbox.bbox[0][1]-cbox.bbox[0][0]))*nparameter))
    lc=rh.Geometry.LineCurve(start,start+rh.Geometry.Vector3d(cbox.bbox[0][2]-cbox.bbox[0][1]))
    i=rh.Geometry.Intersect.Intersection.CurveCurve(lc,cbox.curve,tol,tol)
    return (i[0].PointA-start).Length / (lc.GetLength())

#------------------------------------------------FUNCTIONS SPECIFIC TO THIS SCRIPT
def getvalgrad(pos,remapcurve,cb=None,linear=False):
    if cb==None:
        col1=col.FromArgb(255,0,0,0)
        col2=col.FromArgb(0,0,0,0)
    else:
        col0,col1=cb[0],cb[1]
    if not linear:
        pos=remap_parameter_from_curve(pos,remapcurve)
    intA=rh.Geometry.Interval(col0.A,col1.A)
    poscolA=int(math.floor(intA.ParameterAt(pos)))
    intR=rh.Geometry.Interval(col0.R,col1.R)
    poscolR=int(math.floor(intR.ParameterAt(pos)))
    intG=rh.Geometry.Interval(col0.G,col1.G)
    poscolG=int(math.floor(intG.ParameterAt(pos)))
    intB=rh.Geometry.Interval(col0.B,col1.B)
    poscolB=int(math.floor(intB.ParameterAt(pos)))
    poscol=col.FromArgb(poscolA,poscolR,poscolG,poscolB)
    return poscol

#------------------------------------------------CLASSES SPECIFIC TO THIS SCRIPT
class curvewithbboxXY(object):
    '''adds bounding box in XY plane to curve'''
    def __init__(self,curve):
        self.curve=curve
        self.bbox=BoundingBox([curve])
class Zoneg(object):
    '''gradient area'''
    def __init__(self,brep,nucl):
        zcrv=rh.Geometry.LineCurve(rh.Geometry.Point3d(0.0,0.0,0.0),rh.Geometry.Point3d(0.0,0.0,1000.0))
        self.brep=brep
        self.pixels=[]
        self.region=brep.Faces[0]
        self.nucl=nucl
        self.nsurface=self.region.UnderlyingSurface()
        self.domainU=self.nsurface.Domain(0)
        nucext=(nucl.Faces[0].CreateExtrusion(zcrv,True))
        solext=(self.region.CreateExtrusion(zcrv,True))
        self.nuclsolid=propersolidfromextrusion(nucext)
        self.solid=propersolidfromextrusion(solext)
    def get_pixels_in(self,remapcurve,colors,linear,height):
        #populates instances of pixels contained in that zone
        global tol
        corners=BoundingBox([self.brep,self.nucl])[0]
        first,last=pttopix(corners[0]),pttopix(corners[2])
        for x in xrange(first[0],last[0]+1):
            distances=set()
            point=rh.Geometry.Point3d(x+0.5,-0.5,500.0)
            boundingpts=rh.Geometry.Intersect.Intersection.ProjectPointsToBreps([self.solid,self.nuclsolid],[point],rh.Geometry.Vector3d.YAxis,tol)
            for bp in boundingpts:
                di=int(math.floor(point.DistanceTo(bp)))
                distances.add(di)
            k=list(distances)
            k.sort()
            for i in xrange(len(k)-1):
                interval=k[i],k[i+1]-1
                y=((interval[0]+interval[1])/2.0)+0.5
                testpoint=rh.Geometry.Point3d(x+0.5,y,500.0)
                flag1=self.solid.IsPointInside(testpoint,tol,False)
                flag2=self.nuclsolid.IsPointInside(testpoint,tol,True)
                if (not flag2) and (not flag1):
                    pass
                else:
                    for j in xrange(interval[0],interval[1]+1):
                        gradpos=0
                        if flag1:
                            tittestpoint=rh.Geometry.Point3d(x+0.5,j+0.5,500.0)
                            pos=(self.nsurface.ClosestPoint(tittestpoint))[1]
                            gradpos=self.domainU.NormalizedParameterAt(pos)
                        picture.SetPixel(x,height-j-1,getvalgrad(gradpos,remapcurve,colors,linear))
    print "job done!"

#---------------------------------FILE INPUT - done quick and dirty
#sc.doc = rh.RhinoDoc.ActiveDoc#UNCOMMENT IN GH - COMMENT IN RHINO
z2=[]
test=rs.ObjectsByLayer('ZONE_II')
testsort2=[(rs.ObjectName(t),t) for t in test]
testsort2.sort()
for name,t in testsort2:
    z2.extend(rhob_to_rcob([t]))
    
z3=[]
test=rs.ObjectsByLayer('ZONE_III')
testsort3=[(rs.ObjectName(t),t) for t in test]
testsort3.sort()
for name,t in testsort3:
    z3.extend(rhob_to_rcob([t]))
    
zGroups=zip(z3,z2)
gradcurve=rs.ObjectsByLayer('curve_g')
gc=rhob_to_rcob(gradcurve)[0] #later we could have different ones each associated with a gradient zone

frame=rs.ObjectsByLayer('Frame')[0]
rframe=rhob_to_rcob(frame)
fc,fb=BoundingBox([rframe])
width=int(math.floor(fc[0].DistanceTo(fc[1])))
height=int(math.floor(fc[0].DistanceTo(fc[3])))
#sc.doc = ghdoc#UNCOMMENT IN GH - COMMENT IN RHINO

if __name__ == '__main__':
    #------------------------------------------------USER INPUT TO MODIFY HERE
    c1=col.FromArgb(255,232,240,17)
    c2=col.FromArgb(128,138,197,81)
    thisdir=os.getcwd()
    filename='test1.png'
    picpath=os.path.join(thisdir,filename)
    linear=False

    #------------------------------------------------CREATE THE BITMAP!
    imformat=imgg.Png
    picture=btmp(width,height)
    Curvewithbbox=curvewithbboxXY(gc)
    for z3,z2 in zGroups:
        z=Zoneg(z2,z3)
        z.get_pixels_in(Curvewithbbox,[c1,c2],linear,height)
    picture.Save(picpath,imformat)