You’re right. It behaves exactly the same for me. Changing the direction vector also doesn’t do anything. Using a bounding box to scale the extrusion also seems weird to me.
Anyhow, if you only want to create something like a mesh loft between two or more polylines with optional end caps, here’s a Python example of mine:
"""Create a lofted mesh through a set of section polylines."""
__author__ = "diff-arch (https://diff-arch.xyz)"
__version__ = "1.2.1 (2021-10-16)"
import Rhino.Geometry as rg
import scriptcontext as sc
import Grasshopper as gh
import math
class MeshLoft:
"""Creates a lofted mesh through a set of section polylines.
Attributes:
polylines (list): Valid, closed or open polylines to loft
close_loft (bool): Optional, if True the loft will be closed
cap_loft (bool): Optional, if True the loft will be capped
To use:
>>> ml = MeshLoft(polylines)
>>> loft = ml.mesh
"""
def __init__(self, polylines, close_loft=None, cap_loft=None):
"""Inits this mesh loft."""
self.polylines = polylines
self.close_loft = close_loft
self.cap_loft = cap_loft
self.vertices = self.get_vertices()
self.faces = self.construct_faces()
self.mesh = self.construct_mesh()
def get_vertices(self):
"""Gets the vertices for each polyline in self.polylines.
Returns:
The new nested list of vertices per polyline.
"""
pline_vertices = []
for pline in self.polylines:
vertices = []
for i in xrange(len(pline)):
vertices.append(pline[i])
pline_vertices.append(vertices)
return pline_vertices
def index_vertices(self):
"""Indexes the vertices of each polyline with integer numbers,
that are necessary to later assign the vertices to mesh faces.
Returns:
The nested list of vertex indices (e.g. [[0,1,2], [3,4,5], ..]).
"""
vtx_indices = []
for i in xrange(len(self.vertices)):
indices = []
for j in xrange(len(self.vertices[i])):
indices.append(i * len(self.vertices[i]) + j)
vtx_indices.append(indices)
return vtx_indices
def construct_faces(self):
"""Constructs quad faces between all adjacent polyline pairs.
Returns:
The nested list of face tuples (e.g. [(0,1,7,6), ...]).
"""
vtx_indices = self.index_vertices()
faces = []
for i in xrange(len(vtx_indices)):
if i < len(vtx_indices) - 1:
# Loop the polyline vertex indices up to the last curve
for j in xrange(len(vtx_indices[i])-1): # vertices
v1 = j + (i * len(vtx_indices[i]))
v2 = j + (i * len(vtx_indices[i])) + 1
v3 = j + ((i + 1) * len(vtx_indices[i])) + 1
v4 = j + ((i + 1) * len(vtx_indices[i]))
faces.append((v1, v2, v3, v4))
else:
# Loop the polyline vertex indices of the last curve
if self.close_loft:
for j in xrange(len(vtx_indices[i])-1): # vertices
v1 = j + (i * len(vtx_indices[i]))
v2 = j + (i * len(vtx_indices[i])) + 1
v3 = j + 1
v4 = j
faces.append((v1, v2, v3, v4))
return faces
def construct_mesh(self, angle_tolerance=-1.0):
"""Constructs a lofted mesh from self.vertices and self.faces
and adds caps to it, if necessary.
Args:
angle_tol (float): An optional angle tolerance for vertex welding
Returns:
The new mesh loft.
"""
if angle_tolerance == None or angle_tolerance <= 0:
angle_tol = sc.doc.ModelAngleToleranceRadians
mesh = rg.Mesh()
# Add the mesh loft vertices
for pline_vertices in self.vertices:
for vertex in pline_vertices:
mesh.Vertices.Add(vertex)
# Add the mesh loft faces
for face in self.faces:
v0, v1, v2, v3 = face
mesh.Faces.AddFace(v0, v1, v2, v3)
# Add the mesh caps
if self.cap_loft != None and self.cap_loft > 0:
end_curves = [self.polylines[0], self.polylines[-1]]
if self.cap_loft == 1: # flat cap
mesh_caps = [self.construct_flat_cap(c) for c in end_curves]
cap_fails = 0
for cap in mesh_caps:
if cap != None:
mesh.Append(cap)
mesh.Weld(angle_tolerance)
mesh.Vertices.CombineIdentical(False, False)
else:
cap_fails += 1
if cap_fails == len(end_curves):
err = "Both caps failed."
ghenv.Component.AddRuntimeMessage(\
gh.Kernel.GH_RuntimeMessageLevel.Error, err)
if cap_fails == len(end_curves) - 1:
err = "One cap failed."
ghenv.Component.AddRuntimeMessage(\
gh.Kernel.GH_RuntimeMessageLevel.Error, err)
# Compute the normals and compact the mesh
mesh.Normals.ComputeNormals()
mesh.Compact()
return mesh
def construct_flat_cap(self, edge, tolerance=None):
"""Constructs a flat mesh cap for a closed, naked mesh edge.
Edge polylines with a count of 4 or 5 vertices produce single,
triangular or quad faces. A count of more than 5 edge vertices
results in a triangulated mesh. If these edge vertices share a
center vertex contained inside the naked edge, the face count
equals the number of edge segments, and the faces are radially
oriented around this center vertex. If the center vertex lies
outside the closed mesh edge, the triangulated mesh is produced
from the edge polyline itself.
Args:
edge (Rhino.Geometry.Polyline): A polyline representing a
closed, naked mesh edge
tolerance (float): An optional tolerance for planar projection
Returns:
The new mesh cap on success, or None.
"""
if tolerance == None or tolerance <= 0:
tolerance = sc.doc.ModelAbsoluteTolerance
mesh_cap = rg.Mesh()
# Test for planarity of the mesh edge polyline
pcurve = rg.PolylineCurve(edge)
is_planar, plane = pcurve.TryGetPlane()
if not is_planar:
# Construct a planar test edge from the non-planar one
rc, plane = rg.Plane.FitPlaneToPoints(edge)
proj_pts = [plane.ClosestPoint(pt) for pt in edge]
pcurve = rg.PolylineCurve(proj_pts)
# Find the center vertex of the naked mesh edge
center_vtx = edge.CenterPoint()
# Test for center vertex mesh edge containment
rc = pcurve.Contains(center_vtx, plane, tolerance)
if rc == rg.PointContainment.Outside \
or rc == rg.PointContainment.Coincident:
# Mesh cap corresponds to a triangulated polyline
mesh_cap = rg.Mesh.CreateFromClosedPolyline(edge)
if rc == rg.PointContainment.Inside:
if edge.Count - 1 == 3:
# Mesh cap is composed of a single tri face
for i in xrange(edge.Count - 1):
mesh_cap.Vertices.Add(edge[i])
mesh_cap.Faces.AddFace(0, 1, 2)
if edge.Count - 1 == 4:
# Mesh cap is composed of a single quad face
for i in xrange(edge.Count - 1):
mesh_cap.Vertices.Add(edge[i])
mesh_cap.Faces.AddFace(0, 1, 2, 3)
if edge.Count > 5:
fragments = shatter_at_kinks(edge)
if all(s.SegmentCount == 2 for s in fragments):
# Mesh cap is radially quad-ed around the center vertex
discs = get_discontinuities(edge)
closest_disc_idx, closest_dist_sq = find_closest_point(edge.First, discs)
if closest_dist_sq > sc.doc.ModelAbsoluteTolerance:
# Adjust the edge start vertex to a discontinuity one
closest_vtx_idx, _ = closest_point(discs[closest_disc_idx], edge)
edge = adjust_seam(edge, closest_vtx_idx)
for i in xrange(edge.Count):
mesh_cap.Vertices.Add(edge[i])
mesh_cap.Vertices.Add(center_vtx)
for i in xrange(0, edge.Count - 1, 2):
v0 = i
v1 = i + 1
v2 = edge.Count
v3 = edge.Count-2 if i == 0 else i - 1
mesh_cap.Faces.AddFace(v0, v1, v2, v3)
else:
# Mesh cap is radially triangulated around the center vertex
for i in xrange(edge.Count):
mesh_cap.Vertices.Add(edge[i])
mesh_cap.Vertices.Add(center_vtx)
for i in xrange(edge.Count - 1):
v0 = i
v1 = i + 1
v2 = edge.Count
mesh_cap.Faces.AddFace(v0, v1, v2)
if rc == rg.PointContainment.Unset:
err = "Open polylines can't produce capped mesh lofts."
ghenv.Component.AddRuntimeMessage(\
gh.Kernel.GH_RuntimeMessageLevel.Warning, err)
return
# Compute the normals and compact the mesh
mesh_cap.Normals.ComputeNormals()
mesh_cap.Compact()
return mesh_cap
######################## UTILITY FUNCTIONS #########################
def align_polylines(polylines, guide_idx=None):
"""Aligns closed polylines so that their start/end vertices fall inline.
If the optional guide index is not defined, the polylines align to
the start/end vertex of the first polyline.
Args:
polylines (list): A list of closed polylines
guide_idx (int): An optional guide index for the polylines to align to
Returns:
The list of aligned polylines on success, or the unaltered polylines.
"""
if any([True for p in polylines if not p.IsClosed]):
err = "Open polylines can't be aligned."
ghenv.Component.AddRuntimeMessage(\
gh.Kernel.GH_RuntimeMessageLevel.Warning, err)
return polylines
if guide_idx == None or guide_idx < 0:
guide_idx = 0
elif guide_idx > len(polylines[0]):
guide_idx = 0
aligned_polylines = [polylines[guide_idx]]
for i in range(1, len(polylines)):
# Get the guide vertex of the previous polyline
guide_vtx = polylines[i-1][guide_idx]
# Get the closest index of a vertex of the current polyline
closest_idx = polylines[i].ClosestIndex(guide_vtx) # -1 on error
if closest_idx < 0:
return
# Get a list of indices of the current polyline
indices = range(len(polylines[i])-1)
# Restructure the polyline indices to align them
align_indices = indices[closest_idx:] + indices[:closest_idx]
align_vertices = [polylines[i][j] for j in align_indices]
align_vertices.append(align_vertices[0])
aligned_polylines.append(rg.Polyline(align_vertices))
# Update the guide index
guide_idx = closest_idx
return aligned_polylines
def shatter_at_kinks(pline, tol=sc.doc.ModelAngleToleranceRadians):
"""Shatters a polyline into sections at sharp kinks.
Args:
pline (Rhino.Geometry.Polyline): A polyline to shatter
tol (float): An angle tolerance (in radians) between adjacent segment
for a break to happen, by default the document angle tolerence
Returns:
A list of polyline segments.
"""
segments = pline.GetSegments()
fragments = [rg.Polyline()]
for i in xrange(pline.SegmentCount):
current_seg = segments[i]
next_seg = segments[0] if i == pline.SegmentCount-1 else segments[i+1]
angle = rg.Vector3d.VectorAngle(
current_seg.UnitTangent,
next_seg.UnitTangent
)
fragments[-1].Add(current_seg.From)
if angle < tol:
if i == pline.SegmentCount-1:
if pline.IsClosed:
fragments[0].Insert(0, current_seg.From)
fragments.pop()
else:
fragments[-1].Add(current_seg.To)
continue
fragments[-1].Add(current_seg.To)
if i < pline.SegmentCount - 1:
fragments.append(rg.Polyline())
return fragments
def get_discontinuities(pline, tol=sc.doc.ModelAngleToleranceRadians):
"""Returns all discontinuities along a polyline."""
fragments = shatter_at_kinks(pline, tol)
vertices = []
for frag in fragments:
vertices.append(frag.First)
if not pline.IsClosed:
vertices.append(fragments[-1].Last)
return vertices
def find_closest_point(test_pt, points):
"""Finds the closest point to a test point in a list of points.
Args:
test_pt (Rhino.Geometry.Point3d): Point to search from
points (list): Collection of points to search
Returns:
The index of the closest point [0] and the squared distance [1]
between it and the test point.
"""
min_dist_sq = float("inf")
closest_idx = None
for i, pt in enumerate(points):
dd = test_pt.DistanceToSquared(pt)
if dd < min_dist_sq:
min_dist_sq = dd
closest_idx = i
return closest_idx, min_dist_sq
def adjust_seam(pline, index):
"""Adjusts the seam or start point of a closed polyline.
Args:
pline (Rhino.Geometry.Polyline): A polyline to adjust
index (int): Index of a polyline vertex to move the seam to
Returns:
The adjusted polyline.
"""
if not pline.IsClosed():
return pline
new_pline = rg.Polyline()
for i in xrange(index, pline.Count, 1):
new_pline.Add(pline[i])
for i in xrange(1, index+1):
new_pline.Add(pline[i])
return new_pline
The vertices of the input polylines define the number of mesh divisions in u-direction and their number the divisions in v-direction. It’s thus easy to control the mesh subdivisions.
Should be pretty straightforward to translate to C#.