Creating a geometry in python using curves from mesh

Hello, im trying to recreate this type of component (hexagonal) using GhPython. I created the mesh and have the curves from the mesh, and i also have the normal center point for each hexagone. Im trying to create the geometry in Python to loft, extrude and create a circle in the middle using random values as radius.

I dont really know where to start in my script and i was wondering if someone could help me!

Thank you

How about sharing what you already have?

This is what i got so far. I didn’t started my python script cause i dont really know where to start!

Objet_02_Marc-Antoine.3dm (6.6 MB) Objet_02_Marc-Antoine.gh (401.8 KB)

Have you come up with a solution yet? If not, I’ll take a look.

Hello! No im still struggling to find a solution! It would be amazing if you can help!

OK, I’ll see what I can do. Who’s the reference from?

It’s for one of my class at the university. We have to reproduce a pavillion of our choice. So i choosed this one.

I’ve checked your files out and your Python component seems to be empty?
Is their any specific reason why you want to do this in Python and not with vanilla components?

Overall it seems quite ambitious, if you’re a Python beginner!

I’ve noticed that your hex mesh is kinda irregular, whereas your reference shows rather regular cells? Is this intended? It could potentially make things more challenging.
If I remember correctly, you can’t tessellate a double curved surface with real regular, planar hexagons though. That should be geometrically impossible. The hexagons in your reference don’t seem to be planar either, judging from the look of the top structure.

The tricky part will probably be to get the bottom spherical surface. It’s hard to say if lofting or sweeping will work in this case.
The pavillon was probably done by subtracting spheres from extruded cells? That must have been a nightmare to do.

Yes i also think they did not used python to create the hexagone! But yes our exercise is to try and make it in python but i have no idea how to generate the script to make it happen! And yes, i tried to make the mesh with planar hexagones and make sure they were basicaly the same shape but i couldnt make it work! So if you could help me with that it would be amazing! And i will try to figure out with python a way to create a geometry maybe with a offset + loft? A lot of questions that i have to figure out!

Ouf, that’s a hardcore class, you’ve joined there. :wink:

Yes, that’s because it’s geometrically impossible to tessellate an anticlastic base surface with planar and regular hexagons. You could possibly planarize them (with Kangaroo), but that wouldn’t make them regular (same side lengths).

Also you can have one of two things only, when making three-dimensional cassettes out of the base grid cells, by for instance extruding: either planar sides - where the hexagons are joined to one another , or a planar top and bottom.

However, your reference pavilion doesn’t seem to have started from planar or totally regular hexagons either. The top surface seems to be flat, because they needed a flat surface to be placed on the bed of the CNC mill to carve out the rest of the geometry, and in one photo you can see that they milled even the sides, which could potentially point to them being at least slightly non-planar.

Yes, I’ve done similar stuff in the past, but the hard thing will be to get the spherical bottom surface. The top and sides should be fairly straightforward to do.
I’d also start from the mesh - which is simpler - then starting from a grid of lines or curves. You’ll need to get specific information here and there that curves simply can not provide.

Since your hexagonal base mesh faces are not planar, you could start by going through all of them individually. You need to use the ngon functions, instead of the regular quad and tri mesh ones, because you have a ngon mesh!
For each ngon, you get the boundary vertices and their normals. Then you compute an average, pseudo-ngon face normal from them by adding all the vertex normals and dividing them by their count. It’s a pseudo normal, because the ngon isn’t planar and has more than one normal.
You still need to get the ngon center - there’s probably a function for that -, and then you construct a plane at this point and with the pseudo normal as the z-axis.
After that you want to shoot a line from each boundary vertex in direction of its corresponding normal to intersect with this plane. The intersection points are the vertices of the planar top face.

The non-planar base will be used for the curvy bottom part, so you can define a cassette thickness by moving the plane along its z-axis before doing the intersection part. This will offset it a desired distance away from the base ngon.

You can use this plane also to place the circle for the opening. The top surface will be a loft between the new boundary and this circle.
The side geometries are simply a loft between the original ngon boundary and the new planar top boundary.

All that is missing now is the bottom part. And this is where it gets tricky!
The original ngon boundary is non-planar so I don’t know if it is possible to sweep for instance an arc along it and the circular opening?
The arc profile should be pretty easy to get with a start point, end point, and direction vector. There’s an arc constructor for that! It’s the same as Arc SED in vanilla Grasshopper.

Here’s the relevant mesh documentation:
https://developer.rhino3d.com/api/RhinoCommon/html/T_Rhino_Geometry_Mesh.htm
You can find what you can do with ngons under Ngons.

To do the plane-line-intersections you’ll need some of this:
https://developer.rhino3d.com/api/RhinoCommon/html/T_Rhino_Geometry_Intersect_Intersection.htm

Here’s what you can do with arcs:
https://developer.rhino3d.com/api/RhinoCommon/html/T_Rhino_Geometry_Arc.htm

If you get stuck with a specific part, simply ask!

that pavillion reminded me this kind of chiseled shape

this is the complete project description (found through google reverse image search… so I think it’s ok to share)

I’m following this conversation with great interest, in my mind trying to rebuild this pavillion from a “shape-wise” point of view might be much more difficult than trying to re-apply the logic behind it (which -to be very clear- I don’t think I’d be able to do it myself in first place :joy: :joy: :joy: not to mention doing it in Python…)

I’ve managed to do a small demo in Python, but it still has a few hickups that I need to address.

2 Likes

Wow it’s exactly what im trying to acheive!

Do you mind showing me the script how you arrived to that?

Amazing, i will try to do it! But it seems really logical the way you explained it!

Hello! I have been able to create circles in each planar face of my mesh. Im just struggling to give each circle a different radius by using python! And also, im trying to create a surface between the hexagonals curves and the circles but somehow, not all the the faces are creating the holes, but some are creating the opening…?

Here is the script!

Objet_02_Marc-Antoine.gh (165.3 KB)

1 Like

Hi @mike_002 and @inno,

Here’s a revision of my Python script from the weekend.
It mostly works now, after figuring out some of the bugs.

The script only produces polylines that then can be lofted with vanilla Grasshopper components, and thus made into three-dimensional geometry.
I’ve included a workflow with meshes and one with polysurfaces in the Grasshopper file. I’d use the meshes for prototyping and finding a desired design and the breps for manufacturing or really sharp renderings, since it’s more precise.

The top faces of the cells or cassettes are all planar, like in the reference.
The domes on their undersides aren’t totally spherical or round. I couldn’t figure that out, because sweeping somehow doesn’t seem to work with a planar and a non-planar rail, as well as planar section curve?
In this example, I’ve left them faceted to illustrate this, but the resolution can be dialled up much higher to make them much smoother, which in turn makes the model quite heavy.

The radius or size of the holes is determined randomly, but there are lots of parameters that you change to vary things.

Here’s the script:

from ghpythonlib import treehelpers
import Rhino.Geometry as rg
import random
import math

random.seed(Seed)


def normal_at(mesh, test_pt, max_distance=0.01):
    """Gets the normal at a mesh point that is close the the test point.

    Args:
      mesh (Rhino.Geometry.NgonMesh): A mesh with computed face normals
      test_pt (Rhino.Geometry.Point3d): A test point to search from
      max_distance (float): Optional upper bound on the distance
        from the test point to the mesh, by default 0.01

    Returns:
      The normal at the closest mesh point.
    """
    closest_mesh_pt = mesh.ClosestMeshPoint(test_pt, max_distance)
    if closest_mesh_pt is None:
        return
    return mesh.NormalAt(closest_mesh_pt)


def average_vector(vectors):
    """Returns the arithmetic average vector of a list of vectors."""
    a = rg.Vector3d.Zero
    for v in vectors:
        a += v
    a /= len(vectors)
    return a


def average_point(points):
    """Returns the arithmetic average point of a list of points."""
    a = rg.Point3d.Origin
    for pt in points:
        a += pt
    a /= len(points)
    return a


def create_polyline(vertices, closed=True):
    """Returns a polyline connecting a number of vertices."""
    pline = rg.Polyline()
    for v in vertices:
        pline.Add(v)
    if closed:
        pline.Add(vertices[0])
    return pline


def tween_between(a, b, count):
    """Returns an x-amount of tween curves between the polylines a and b.
        Both polylines must have the same number of vertices."""
    div_pts = []
    if count < 1:
        return []
    for j in xrange(len(a)):
        line = rg.LineCurve(a[j], b[j])
        params = line.DivideByCount(count + 1, False)
        div_pts.append([line.PointAt(t) for t in params])        
    tween_crvs = []
    for j in xrange(count):
        pline = rg.Polyline()
        for pts in div_pts:
            pline.Add(pts[j])
        tween_crvs.append(rg.PolylineCurve(pline))
    return tween_crvs


a = []

if __name__ == "__main__":

    NPB = []  # non-planar boundaries
    BDS = []  # boundary-defining side subdivisions
    OPB = []  # offset, planar boundaries
    OPS = []  # planar, offset face-defining subdivisions
    CHB = []  # circular, planar hole boundaries
    DDA = []  # dome-defining arcs
    DDR = []  # dome-defining rings

    if NgonMesh is not None:
        NgonMesh.FaceNormals.ComputeFaceNormals()
        section_arcs = []
        section_rings = []
        offset_subdivs = []
        boundary_subdivs = []
        
        for i in xrange(NgonMesh.Ngons.Count):  # loop each ngon face
            vertex_indices = NgonMesh.Ngons.GetNgon(i).BoundaryVertexIndexList()
            vertices = [
                rg.Point3d(NgonMesh.Vertices[j]) for j in vertex_indices
            ]
            boundary_pline = create_polyline(vertices)  # face boundary
            boundary_pline.MergeColinearSegments(AngleTol, True)

            # Divide the initial, non-planar boundary
            div_pts = []  # face boundary division points
            ia = NumArcs + 1
            for j in xrange(boundary_pline.SegmentCount):
                segment = boundary_pline.SegmentAt(j)
                div_pts.append(segment.From)
                crv = rg.LineCurve(segment)
                params = crv.DivideByCount(ia, False)
                for t in params:
                    div_pts.append(crv.PointAt(t))

            div_pts_normals = [
                normal_at(NgonMesh, p, float("inf")) for p in div_pts
            ]
            ngon_pline = create_polyline(div_pts)
            NPB.append(ngon_pline)

            ngon_center = NgonMesh.ClosestPoint(average_point(div_pts))
            ngon_normal = average_vector(div_pts_normals)
            ngon_normal.Unitize()
            if Flip:
                ngon_normal.Reverse()

            # Define the offset, planar boundary
            offset_center = ngon_center + ngon_normal * OutHeight
            plane = rg.Plane(offset_center, ngon_normal)

            ll = 1.0 if OutHeight == 0.0 else OutHeight + OutHeight * 0.5
            offset_pts = []
            for j in xrange(len(div_pts)):
                start = div_pts[j]
                end = start + (div_pts_normals[j] * ll)
                ray = rg.Line(start, end)
                _, t = rg.Intersect.Intersection.LinePlane(ray, plane)
                offset_pts.append(ray.PointAt(t))

            offset_pline = create_polyline(offset_pts)  # is planar
            OPB.append(offset_pline)
            offset_center = average_point(offset_pts)

            # Evaluate the random center hole radius
            max_ngon_radius = float("inf")
            for j in xrange(offset_pline.Count - 1):
                dist2_vertex = offset_center.DistanceToSquared(offset_pline[j])
                midpt = (offset_pline[j] + offset_pline[j + 1]) * 0.5
                dist2_midpt = offset_center.DistanceToSquared(midpt)
                min_dist = math.sqrt(min(dist2_vertex, dist2_midpt))
                if min_dist < max_ngon_radius:
                    max_ngon_radius = min_dist
            max_radius = max_ngon_radius - max_ngon_radius * MinOffset
            min_radius = min(MinRadius, max_radius)
            radius = random.uniform(min_radius, max_radius)
            rest_radius = max_ngon_radius - radius
            
            # Construct the backset circle which can define an inner thickness
            backset_height = max(0.0, min(InHeight, OutHeight))
            backset_center = offset_center - (plane.ZAxis * backset_height)
            backset_plane = rg.Plane(backset_center, plane.ZAxis)
            backset_circle = rg.Circle(backset_plane, backset_center, radius)

            # Construct the dome-defining arcs
            arcs = []
            for j in xrange(len(offset_pts)):
                closest_pt = backset_circle.ClosestPoint(offset_pts[j])
                end_pts = [closest_pt, div_pts[j]]  # arc start and end points
                end_indices = [0, 1]  # indices of the end points
                lengths = [p.DistanceToSquared(offset_pts[j]) for p in end_pts]
                sorted_indices = [
                    p for _, p in sorted(zip(lengths, end_indices))
                ]
                fi = sorted_indices[-1]
                li = sorted_indices[0]
                tangent = offset_pts[j] - end_pts[fi]
                arc = rg.Arc(end_pts[fi], tangent, end_pts[li])
                if fi == end_indices[-1]:
                    arc.Reverse()
                arcs.append(arc)
            section_arcs.append(arcs)

            # Divide the arcs
            r = NumRings + 1  # number of rings
            arc_pts = []
            min_arc_div_dist2 = float("inf")
            for arc in arcs:
                crv = rg.ArcCurve(arc)
                params = crv.DivideByCount(r, True)
                div_pts = [crv.PointAt(t) for t in params]
                dist2 = div_pts[0].DistanceToSquared(div_pts[1])
                if dist2 < min_arc_div_dist2:
                    min_arc_div_dist2 = dist2
                arc_pts.append(div_pts)

            # Construct the dome-defining rings
            rings = []
            for j in xrange(r):
                ring_pts = []
                for points in arc_pts:
                    ring_pts.append(points[j])
                ring_pline = create_polyline(ring_pts)
                rings.append(ring_pline)

                # Construct the offset center hole boundary
            backset_ring = rings[0]
            offset_ring = rg.Polyline()
            for vtx in backset_ring:
                offset_vtx = vtx + backset_plane.ZAxis * backset_height
                offset_ring.Add(offset_vtx)
            CHB.append(offset_ring)

            if backset_height == 0.0:  # backset_ring == rings[0]
                rings.pop(0)  # avoid duplicate polylines
            section_rings.append([r.ToPolylineCurve() for r in rings])

            # Subdivide the planar, offset face in relation to the arc divisions
            num = math.floor((rest_radius) / math.sqrt(min_arc_div_dist2)) - 1
            face_subdivs = tween_between(offset_pline, offset_ring, num)
            offset_subdivs.append(face_subdivs)
            
            # Subdivide the side faces in relation to the arc divisions
            sides_subdivs = tween_between(ngon_pline, offset_pline, SideDivs)
            boundary_subdivs.append(sides_subdivs)

    BDS = treehelpers.list_to_tree(boundary_subdivs)
    OPS = treehelpers.list_to_tree(offset_subdivs)
    DDA = treehelpers.list_to_tree(section_arcs)
    DDR = treehelpers.list_to_tree(section_rings)

voute_de_lefrevre.gh (88.6 KB)

7 Likes

Amazing! Thank you so much for your help! It good to see an other version of what i acheived on my side! Your python script is super full and makes it easier to deal with each value!

Thanks again!!

It was an interesting topic to explore. Feel free to ask if you have questions about the Python script.

2 Likes