Hi everyone!
I’ve been meaning to create this Python script for Rhino for a long time, and I finally made it happen. It lets you extend surface edges quickly and easily, featuring a live preview so you can see the changes before applying them.
You just select the edges, set the extension length and step, and the surfaces update immediately. The script remembers your last settings to make repeated use smoother.
Hope this tool makes your workflow a bit easier. Give it a try!
"""
Extend Multi Sides for Rhino
Author: leex
Date: 2025-06-01
This script allows you to extend surface edges with live preview and adjustable step.
Select edges, set extension length, preview, and apply.
The script remembers your last used settings, which are saved to your system TEMP folder as ExtendMultiSides_last_value.txt.
Enjoy!
"""
import Rhino
import scriptcontext as sc
import System.Drawing
import os
import tempfile
def get_settings_path():
tempdir = tempfile.gettempdir()
return os.path.join(tempdir, "ExtendMultiSides_last_value.txt")
def save_last_value(val):
path = get_settings_path()
try:
with open(path, "w") as f:
f.write(str(val))
except:
pass
def load_last_value(default=2.01):
path = get_settings_path()
try:
if os.path.exists(path):
with open(path, "r") as f:
return float(f.read())
except:
pass
return default
def edge_has_only_seam_trims(edge, brep):
trims = edge.TrimIndices()
if not trims:
return False
for trim_index in trims:
trim = brep.Trims[trim_index]
if trim.TrimType != Rhino.Geometry.BrepTrimType.Seam:
return False
return True
def is_periodic_edge(edge, brep):
return edge_has_only_seam_trims(edge, brep)
def get_previews(objs_faces, dist):
preview_ids = []
if hasattr(get_previews, "preview_ids"):
for pid in get_previews.preview_ids:
sc.doc.Objects.Delete(pid, True)
for obj_id, data in objs_faces.items():
brep = data["brep"]
faces_dict = data["faces"]
for face_index, isos in faces_dict.items():
face = brep.Faces[face_index]
ext_face = face
for iso in isos:
ext_face = ext_face.Extend(iso, dist, True) or ext_face
pid = sc.doc.Objects.AddBrep(ext_face.ToBrep())
rhobj = sc.doc.Objects.Find(pid)
if rhobj:
rhobj.Attributes.ObjectColor = System.Drawing.Color.FromArgb(120, 80, 220, 80)
rhobj.Attributes.ColorSource = Rhino.DocObjects.ObjectColorSource.ColorFromObject
rhobj.CommitChanges()
preview_ids.append(pid)
sc.doc.Views.Redraw()
get_previews.preview_ids = preview_ids
return preview_ids
def remove_previews():
if hasattr(get_previews, "preview_ids"):
for pid in get_previews.preview_ids:
sc.doc.Objects.Delete(pid, True)
get_previews.preview_ids = []
def ExtendMultiSides():
go = Rhino.Input.Custom.GetObject()
go.SetCommandPrompt("Select edges to extend")
go.GeometryFilter = Rhino.DocObjects.ObjectType.EdgeFilter
go.SubObjectSelect = True
go.EnablePreSelect(True, True)
go.GetMultiple(1, 0)
if go.ObjectCount == 0:
return
objs_faces = {}
for i in range(go.ObjectCount):
objref = go.Object(i)
obj_id = objref.ObjectId
brep = objref.Brep()
ci = objref.GeometryComponentIndex
trim = None
edge = None
if str(ci.ComponentIndexType) == "BrepEdge":
edge = brep.Edges[ci.Index]
trim = edge.Trims[0] if edge.Trims else None
elif str(ci.ComponentIndexType) == "BrepTrim":
trim = brep.Trims[ci.Index]
edge = trim.Edge
if not trim or not edge:
continue
if is_periodic_edge(edge, brep):
continue
face = trim.Face
face_index = face.FaceIndex
if obj_id not in objs_faces:
objs_faces[obj_id] = {"brep": brep, "faces": {}}
if face_index not in objs_faces[obj_id]["faces"]:
objs_faces[obj_id]["faces"][face_index] = set()
objs_faces[obj_id]["faces"][face_index].add(trim.IsoStatus)
dist = load_last_value(default=2.01)
step = 0.25
get_previews(objs_faces, dist)
set_length = True
done = False
while not done:
gn = Rhino.Input.Custom.GetNumber()
gn.SetCommandPrompt("Enter extension length (Press Esc when done). Step: {:.2f}".format(step))
less_option = gn.AddOption("Less")
more_option = gn.AddOption("More")
step_option = gn.AddOption("SetStep")
done_option = gn.AddOption("Done")
gn.SetDefaultNumber(dist)
gn.SetLowerLimit(0.01, True)
result = gn.Get()
if result == Rhino.Input.GetResult.Option:
idx = gn.Option().Index
if idx == less_option:
dist = max(dist - step, 0.01)
set_length = True
get_previews(objs_faces, dist)
continue
elif idx == more_option:
dist = dist + step
set_length = True
get_previews(objs_faces, dist)
continue
elif idx == step_option:
gn_step = Rhino.Input.Custom.GetNumber()
gn_step.SetCommandPrompt("Enter new step value")
gn_step.SetDefaultNumber(step)
gn_step.SetLowerLimit(0.001, True)
step_result = gn_step.Get()
if step_result == Rhino.Input.GetResult.Number:
step = gn_step.Number()
continue
elif idx == done_option:
done = True
break
elif result == Rhino.Input.GetResult.Number:
dist = gn.Number()
set_length = True
get_previews(objs_faces, dist)
continue
else:
break
remove_previews()
if set_length:
for obj_id, data in objs_faces.items():
brep = data["brep"]
faces_dict = data["faces"]
new_breps = []
for i, face in enumerate(brep.Faces):
if i in faces_dict:
ext_face = face
extending_isos = set(faces_dict[i])
for iso in extending_isos:
ext_face = ext_face.Extend(iso, dist, True) or ext_face
new_breps.append(ext_face.ToBrep())
else:
dupl = face.DuplicateFace(False)
if dupl:
new_breps.append(dupl)
joined = Rhino.Geometry.Brep.JoinBreps(new_breps, sc.doc.ModelAbsoluteTolerance)
sc.doc.Objects.Delete(obj_id, True)
if joined and len(joined) == 1:
sc.doc.Objects.AddBrep(joined[0])
elif joined:
for jb in joined:
sc.doc.Objects.AddBrep(jb)
sc.doc.Views.Redraw()
save_last_value(dist)
if __name__ == "__main__":
ExtendMultiSides()