Pattern on surfaces

Hi friends,

long time no post from me. I ran into a problem with generating a pattern, the algorithm is quite simple, random rectangles, scaled and rotated:

I need to create it on different surfaces, one is planar, two not:

I managed to create it on the planar surface, but I am not sure if it is the best solution performance wise:

I went after several approaches with: “Region Union” on the rectangle curves, “Boundary” surface from curves, “Split Surface” with the target region, “point in BREP” to select final pattern:

I could live with this, but I have two more surfaces to deal with, who are not planar. Any idea maybe which direction I could go?

Cheers and thanks, Volker

RectPattern.gh (21.8 KB)

Hi Volker,

That seems quite hard to do honestly. I’ve tried to use Map To Surface to do this. It seems to work fine for the bottom surface, but not for the pipe surface in the middle. It seems that middle surface has quite a few hidden seams that can generate lines where you wouldn’t want them.

Is the extrusion important? Projecting the pattern onto a surface seems easy compared to neatly extruding that pattern.

RectPattern_b.gh (20.3 KB)

1 Like

One of the solution is to use Geodesic with a departure point, a direction and a distance.
For the moment I can’t publish my script and I don’t know if there are some Geodesic script doing that. You could use this script to mimic geodesic path. I will look tonight if I could tweak the script, must not be difficult.


The idea is simple
Populate the geometry with point, find the normal (Brep closest point), put a plane, random rotate this plane around Z, use X direction of the plane as direction of geodesic.
Walk along the mesh for a certain distance.
You then have a path following the surface. Then pipe this path.
You just need to input the number of points, the length of the curve and it must work.

I didn’t go for a deep modification, there are many unnecessary things. But it seems to work.

A fast tweak of old script


RectPattern LD.gh (20.4 KB)

Hi Bert,
thanks a lot for the response. I went a similar direction like you. But I used “Surface Morph”, that way I was able to create the hole surface planar and morph it in the end to my surface, similar to your solution. The only problem with our definitions is, that the pattern gets distorted when morphed or mapped onto a radial surface. My solution is to unroll the radial surface first, create the pattern on it, bent/flow the new pattern to a straight rectangle and then morph it to the target surface. That way the distortion seems to be gone. Still not a very responsive definition, but with a few switches I might be able to generate different patterns. I don’t really have to extrude the pattern, but I need nice curves for fabrication in the end. Will post my definition as soon as I cleaned it a bit.
Cheers and thanks, Volker

Hi Laurent,
thank you too for your response. Very cool direction, I was thinking of a “walker” script too. I was looking at the Nudibranch examples, they have a few walk on mesh nodes too. Because I need fabrication curves in the end, I guess I have to work with surfaces, but maybe generate the curves with meshes and then project them onto the surface or use the tubes and find the intersection between both? Sadly performance is not so much better than the morphing direction. Will continue on both directions and post as soon as I found a solution.
Cheers and thanks, Volker

You can project the curve or reuse end point and use geodesic from RhinoCommon
https://developer.rhino3d.com/api/RhinoCommon/html/M_Rhino_Geometry_Surface_ShortPath.htm

Instead of using pipes it is also possible to

  • Generate a first geodesic curve from point A to point B
  • Generate a lateral path from A but with a 90° rotation from the direction of geodesic curve in A=> A’ points
  • Generate a lateral path from B but with a 90° rotation from the direction of geodesic curve in B => B’ point
  • Generate geodesic path A’ to B’.
    You will end with just curves on surfaces.

Tell me, I can do it.

Also for walking on mesh there are others possibilities, the simple way is the way I post so walk for an amount of distance, then project on mesh and redo it. The other way is to use mesh faces to walk, so you no more need to project on mesh. It is fast.

Hi Laurent,
great stuff you are doing, had a look at your instagram account. Tried to follow you on your Fornes style stuff a while ago.

Sounds like a great approach with the short path command. Would love to learn from you. I know a bit of Python and will also try to come up with a script, but I am very interested how you would do it.

Ok I will do it in C#, it is the language I use.

Yeah, very curious. Wish I could do C#, maybe I am able to read and understand

Here the script, I used surface because surface is used by shortestPath, but I am sure that ShortestPath is not the fastest way to do that. Using mesh approach then projecting curve on surface (or Brep) could be more effective and more general. But it works.

  private void RunScript(Surface surface, List<Point3d> points, List<double> distances, double width, int seed, ref object A)
  {

    //Laurent Delrieu 11/5/2020   
    List<Curve> lst_curves = new List<Curve>();
    //TODO test on list of distances that must have the same size as list of points ...
    Random rnd = new Random(seed);
    for (int i = 0; i < points.Count; i++)
    {
      //We seek the normal on the mesh, could be done on Brep
      double u, v;
      double distance = distances[i];
      Point3d point = points[i];

      surface.ClosestPoint(point, out u, out v);
      Point3d closestPoint = surface.PointAt(u, v);
      Vector3d normal = surface.NormalAt(u, v);

      //We try with the cross product of normal and X
      Vector3d direction = Vector3d.CrossProduct(normal, Vector3d.XAxis);
      if (direction.Length < 1e-10) // If length to little (near 0 we choose another vector)
      {
        direction = Vector3d.CrossProduct(normal, Vector3d.YAxis);
      }
      //Random angle
      double angle = rnd.NextDouble() * Math.PI * 2.0;
      //random direction
      direction.Rotate(angle, normal);

      Curve firstCurve = CurveOnSurface(surface, point, direction, distance);

      lst_curves.Add(firstCurve);//Add the first curve
      //First lateral move
      Point3d firstPoint = firstCurve.PointAtStart;
      surface.ClosestPoint(firstPoint, out u, out v);
      Vector3d normalFirstPoint = surface.NormalAt(u, v);
      Vector3d directionFirstPoint = firstCurve.TangentAtStart;
      directionFirstPoint.Rotate(Math.PI / 2, normalFirstPoint);//Rotate 90°
      Curve lateral_0 = CurveOnSurface(surface, firstPoint, directionFirstPoint, width);

      //Last lateral move
      Point3d lastPoint = firstCurve.PointAtEnd;
      surface.ClosestPoint(lastPoint, out u, out v);
      Vector3d normalLastPoint = surface.NormalAt(u, v);
      Vector3d directionLastPoint = firstCurve.TangentAtEnd;
      directionLastPoint.Rotate(Math.PI / 2, normalLastPoint);//Rotate 90°
      Curve lateral_1 = CurveOnSurface(surface, lastPoint, directionLastPoint, width);

      lst_curves.Add(lateral_0);
      lst_curves.Add(lateral_1);

      //Closing the rectangle
      double u0, v0;
      surface.ClosestPoint(lateral_0.PointAtEnd, out u0, out v0);
      double u1, v1;
      surface.ClosestPoint(lateral_1.PointAtEnd, out u1, out v1);
      lst_curves.Add(surface.ShortPath(new Point2d(u0, v0), new Point2d(u1, v1), RhinoDoc.ActiveDoc.ModelAbsoluteTolerance * 100));
    }

    A = lst_curves;

  }

  // <Custom additional code> 


  public Curve CurveOnSurface(Surface surface, Point3d departurePoint, Vector3d departureDirection, double distance)
  {
    List<Point3d> lst_points = WalkOnSurface(surface, departurePoint, departureDirection, distance);
    double u0, v0;
    surface.ClosestPoint(lst_points[0], out u0, out v0);
    double u1, v1;
    surface.ClosestPoint(lst_points[lst_points.Count - 1], out u1, out v1);
    Curve curve = surface.ShortPath(new Point2d(u0, v0), new Point2d(u1, v1), RhinoDoc.ActiveDoc.ModelAbsoluteTolerance * 100);

    double t;
    //if OK we take the firts segement that must have the good length
    if  (curve.LengthParameter(distance, out t))
    {
      Curve[] curves_splitted = curve.Split(t);
      curve = curves_splitted[0];
    }
    return curve;
  }

  public List<Point3d> WalkOnSurface(Surface surface, Point3d departurePoint, Vector3d departureDirection, double distance)
  {
    int n = 100;
    double incrementOfDistance = distance / (double) n; //Increment of move abritrary set, could be changed
    List<Point3d> lst_points = new  List<Point3d>();//List to store the points
    //Seeking the closest point, need some test if closest is OK
    double u, v;
    surface.ClosestPoint(departurePoint, out u, out v);
    lst_points.Add(surface.PointAt(u, v));

    Vector3d direction = departureDirection;
    direction.Unitize();
    for (int i = 0; i < (n + 4); i++)//I took some more points in order to have a projected curve of good length
    {
      Point3d nextPoint = lst_points[lst_points.Count - 1] + direction * incrementOfDistance;
      double u1, v1;
      surface.ClosestPoint(nextPoint, out u1, out v1);
      lst_points.Add(surface.PointAt(u1, v1));

      direction = lst_points[lst_points.Count - 1] - lst_points[lst_points.Count - 2];
      if (!direction.Unitize()) //It means the 2 last points are the same
      {
        break;
      }
    }
    return lst_points;
  }

rectangle on mesh brep LEGACY.gh (13.3 KB)

3 Likes

Hi Laurent, still trying to get my head around your script. Pretty impressive! Tried on my own in Python, but the walking part gives me headaches. Managed to get random uv points and the shortest path from them, but on a tube surface it looks strange because of the random uv`s. I guess the mystery in your script is the WalkOnSurface function. Get a random point, take an Axis vector as start direction, walk for a amount with this direction, find closest point on surface, repeat walk and direction. Will give up for today, will fight tomorrow again. Thanks again, Volker

Just the walk function might look in GH like this?! Just for my brain in GH first

With the help from Laurent and his C# Script I managed to write a similar Python script node. Random geodesic curves on a surface, based on the walk on surface principle. Long time since my last try with Python, so be gentle, but if someone sees my beginner mistakes, please point out. Have to learn some basic Python Rhino Grasshopper stuff for the next script or to finalize this one. I guess the C# script is much faster, but the Python one works and I am able to manipulate it. Thanks a lot Laurent for the guide and help, I learned so much with it.

How do I post Code nicely like Laurent did?

RectWalkerScript_01.gh (19.3 KB)

#```python

# your code

#```

3 x ` (backtick)
followed by python
and close with 3 x backtick
1 Like

I am happy if my help was useful, I will not correct your Python …
@Gijs was faster and I didn’t know the namebacktick
image

1 Like

so here is my code, if someone is interested. Far from perfect, but seems to work:

import Rhino
import rhinoscriptsyntax as rs
import random
import math

ptList = []
crvList = []

random.seed(rotSeed)

def WalkOnSrf(surface, departPt, direction, steps, distSteps):
    crvPts = []
    crvPts.append(departPt)
    for y in range(steps):
        xform = rs.XformTranslation(direction)
        vecNext = rs.TransformObjects( departPt, xform, True )
        uvNext = rs.SurfaceClosestPoint(surface, vecNext[0])
        ptNext = rs.EvaluateSurface(surface, uvNext[0],uvNext[1])        
        ptNext3d = Rhino.Geometry.Point3d(ptNext)
        crvPts.append(ptNext3d)
        departPt = ptNext3d        
    return crvPts
    
def CrvOnSrf(srf, crvPts):
    geoCrv = rs.ShortPath(srf, crvPts[0], crvPts[len(crvPts)-1])
    return geoCrv

def crvOffset(crvA, width):
    crvAStart = rs.CurveStartPoint(crvA)
    crvAEnd = rs.CurveEndPoint(crvA)    
    crvB = rs.OffsetCurveOnSurface(crvA, srf, width) 
    crvBStart = rs.CurveStartPoint(crvB)
    crvBEnd = rs.CurveEndPoint(crvB)    
    crvC = rs.ShortPath(srf, crvAStart, crvBStart)
    crvD = rs.ShortPath(srf, crvAEnd, crvBEnd)    
    joinCrv = rs.JoinCurves([crvA, crvB, crvC, crvD])    
    return joinCrv


if rs.IsSurface(srf):
    
    for i in range(len(ptsIn)):
        
        ptsStart = ptsIn[i]
        uvStart = rs.SurfaceClosestPoint(srf, ptsStart)
        ptsStart = rs.EvaluateSurface(srf, uvStart[0], uvStart[1])
        ptStart3d = Rhino.Geometry.Point3d(ptsStart)
        
        plane = rs.SurfaceFrame(srf,(uvStart[0], uvStart[1]))   
        uVec = plane.XAxis
        vVec = plane.YAxis
        normal = srf.NormalAt(uvStart[0],uvStart[1])
        direction = rs.VectorCrossProduct(normal, uVec)
        
        angle = random.random() * math.pi * 2
        direction.Rotate(angle, normal)
        direction = rs.VectorUnitize( direction )
        direction = rs.VectorScale(direction, stepSize * random.uniform(scaleMin, scaleMax))
        
        crvPts = WalkOnSrf(srf, ptsStart, direction, walkSteps, stepSize) 
        
        for y in range(len(crvPts)):
            ptList.append(crvPts[y])
        
        crvA = (CrvOnSrf(srf, crvPts))
        
        try:
            crvList.append((crvOffset(crvA, width)[0]))
        except:
            print "Offset Error"

else:
    print "The object is not a surface."

ptOut = ptList
crvOut = crvList

Just a quick parallelization:

private void RunScript(Surface surface, List<System.Object> points, List<System.Object> distances, double width, int seed, ref object A)
{
    Component.Message = "Parallel";
    ConcurrentBag<Curve> recs = new ConcurrentBag<Curve>();
    //TODO test on list of distances that must have the same size as list of points ...
    Random rnd = new Random(seed);
    Parallel.For(0, points.Count, i =>
    {
        Curve[] rec = new Curve[4];
        //We seek the normal on the mesh, could be done on Brep
        double u, v;
        double distance = (double) distances[i];
        Point3d point = (Point3d) points[i];

        surface.ClosestPoint(point, out u, out v);
        Point3d closestPoint = surface.PointAt(u, v);
        Vector3d normal = surface.NormalAt(u, v);

        //We try with the cross product of normal and X
        Vector3d direction = Vector3d.CrossProduct(normal, Vector3d.XAxis);
        if (direction.Length < 1e-10) // If length to little (near 0 we choose another vector)
        {
        direction = Vector3d.CrossProduct(normal, Vector3d.YAxis);
        }
        //Random angle
        double angle = rnd.NextDouble() * Math.PI * 2.0;
        //random direction
        direction.Rotate(angle, normal);

        Curve firstCurve = CurveOnSurface(surface, point, direction, distance);

        rec[0] = firstCurve;//Add the first curve
        //First lateral move
        Point3d firstPoint = firstCurve.PointAtStart;
        surface.ClosestPoint(firstPoint, out u, out v);
        Vector3d normalFirstPoint = surface.NormalAt(u, v);
        Vector3d directionFirstPoint = firstCurve.TangentAtStart;
        directionFirstPoint.Rotate(Math.PI / 2, normalFirstPoint);//Rotate 90°
        Curve lateral_0 = CurveOnSurface(surface, firstPoint, directionFirstPoint, width);

        //Last lateral move
        Point3d lastPoint = firstCurve.PointAtEnd;
        surface.ClosestPoint(lastPoint, out u, out v);
        Vector3d normalLastPoint = surface.NormalAt(u, v);
        Vector3d directionLastPoint = firstCurve.TangentAtEnd;
        directionLastPoint.Rotate(Math.PI / 2, normalLastPoint);//Rotate 90°
        Curve lateral_1 = CurveOnSurface(surface, lastPoint, directionLastPoint, width);

        rec[1] = lateral_0;
        rec[2] = lateral_1;

        //Closing the rectangle
        double u0, v0;
        surface.ClosestPoint(lateral_0.PointAtEnd, out u0, out v0);
        double u1, v1;
        surface.ClosestPoint(lateral_1.PointAtEnd, out u1, out v1);
        rec[3] = surface.ShortPath(new Point2d(u0, v0), new Point2d(u1, v1), RhinoDoc.ActiveDoc.ModelAbsoluteTolerance * 100);
        recs.Add(Curve.JoinCurves(rec)[0]);
    });
    A = recs;
}
// <Custom additional code> 
public Curve CurveOnSurface(Surface surface, Point3d departurePoint, Vector3d departureDirection, double distance)
{
    List<Point3d> lst_points = WalkOnSurface(surface, departurePoint, departureDirection, distance);
    double u0, v0;
    surface.ClosestPoint(lst_points[0], out u0, out v0);
    double u1, v1;
    surface.ClosestPoint(lst_points[lst_points.Count - 1], out u1, out v1);
    Curve curve = surface.ShortPath(new Point2d(u0, v0), new Point2d(u1, v1), RhinoDoc.ActiveDoc.ModelAbsoluteTolerance * 100);

    double t;
    //if OK we take the firts segement that must have the good length
    if  (curve.LengthParameter(distance, out t))
    {
        Curve[] curves_splitted = curve.Split(t);
        curve = curves_splitted[0];
    }
    return curve;
}
public List<Point3d> WalkOnSurface(Surface surface, Point3d departurePoint, Vector3d departureDirection, double distance)
{
    int n = 100;
    double incrementOfDistance = distance / (double) n; //Increment of move abritrary set, could be changed
    List<Point3d> lst_points = new  List<Point3d>();//List to store the points
    //Seeking the closest point, need some test if closest is OK
    double u, v;
    surface.ClosestPoint(departurePoint, out u, out v);
    lst_points.Add(surface.PointAt(u, v));

    Vector3d direction = departureDirection;
    direction.Unitize();
    for (int i = 0; i < (n + 4); i++)//I took some more points in order to have a projected curve of good length
    {
        Point3d nextPoint = lst_points[lst_points.Count - 1] + direction * incrementOfDistance;
        double u1, v1;
        surface.ClosestPoint(nextPoint, out u1, out v1);
        lst_points.Add(surface.PointAt(u1, v1));

        direction = lst_points[lst_points.Count - 1] - lst_points[lst_points.Count - 2];
        if (!direction.Unitize()) //It means the 2 last points are the same
        {
        break;
        }
    }
    return lst_points;
}
// </Custom additional code> 

rectangle on brep parallel.gh (15.2 KB)

3 Likes

Cool, I must keep in mind that ConcurrentBag