Here’s a Python script that should work for most surfaces.

Keep in mind that the found extreme points are always just an approximation.
import Rhino.Geometry as rg
import Grasshopper as gh
import scriptcontext as sc
def divide_surface(srf, udiv, vdiv, nested=False):
"""Divides a surface into a grid of uv-points.
Args:
srf (Rhino.Geometry.Surface): Surface to divide
udiv (int): Number of divisions in surface u-direction
vdiv (int): Number of divisions in surface v-direction
nested (bool): Optionally True to return a nested list [column][row],
by default False to return a flat list.
Returns:
The division points."""
num_rows = udiv + 1
num_cols = udiv + 1
udom = rg.Interval(0, udiv)
vdom = rg.Interval(0, vdiv)
S.SetDomain(0, udom)
S.SetDomain(1, vdom)
div_pts = []
for i in xrange(num_rows):
for j in xrange(num_cols):
div_pts.append(rg.Surface.PointAt(srf, i, j))
if nested:
return [div_pts[i:i+num_rows] for i in xrange(0, len(div_pts), num_rows)]
return div_pts
def find_extremes_z(srf, udiv=10, vdiv=10, step=10, max_div=250, __pts=[], __vals=[]):
"""Recursively finds the approximated extremes - highest and lowest point in z - on a surface.
Args:
srf (Rhino.Geometry.Surface): Surface to evaluate the z-extremes for
udiv (int): Optional number of start divisions in surface u-direction, by default 15
vdiv (int): Optional number of start divisions in surface v-direction, by default 15
step (int): Optional step value that increments udiv and vdiv each recursion level, by default 15
max_div (int): Optional maximum number of divisions to in uv-direction of the surface
Returns:
The approximated lowest [0] and hightest point [1] on the surface."""
if udiv >= max_div or vdiv >= max_div:
return __pts
sample_pts = divide_surface(srf, udiv, vdiv)
sample_pts.sort(key=lambda pt: pt.Z)
extremes = [sample_pts[0], sample_pts[-1]]
# print "U:", udiv, "V:", vdiv, "-> Extremes:", [pt.Z for pt in __pts]
if len(__pts) == 0:
__vals = [[] for _ in xrange(len(extremes))]
return find_extremes_z(srf, udiv+step, vdiv+step, step, max_div, extremes, __vals)
for i in xrange(len(extremes)):
difference = abs(extremes[i].Z) - abs(__pts[i].Z)
if difference > 0.0:
__pts[i] = extremes[i]
__vals[i].append(difference)
if len(__vals[0]) == 10:
dsum = sum([sum(lt) for lt in __vals])
if dsum < sc.doc.ModelAbsoluteTolerance:
return __pts
return find_extremes_z(srf, udiv+step, vdiv+step, step, max_div, __pts, __vals)
if __name__ == "__main__":
if S:
if not D:
D = 250
E = find_extremes_z(S, max_div=D)
else:
ghenv.Component.AddRuntimeMessage(
gh.Kernel.GH_RuntimeMessageLevel.Warning,
"Input parameter S failed to collect data"
)
The surface gets recursively more and more divided, until for at least 10 steps there was no change for both extremes found.
At each step all the division points are sorted and the extreme points compared to those found at the previous step.
This doesn’t exclude that for a later, even finer division, there would have been an even better result! This and the fact that the surface can infinitely be subdivided, makes the resulting extremes always an approximation.
max of surface 4.gh (199.6 KB)