Do you know that in Rhino you can apply materials to Sub-Surfaces?
For this purpose and a few others I made this Sub-Surface filter.
It takes Polysurfaces and selects Sub-Surfaces:
- Planar and parallel to CPlane
- Non parallel to CPlane
- Ceilings
- Floors
- Non parallel and planar
- Non parallel and non planar
- Planar and perpendicular to CPlane
- Planar and not perpendicular to CPlane
I started by asking ChatGPT to create a definition that selects subsurfaces and then i asked it to add more definitions one by one for the filters I needed. This way I could debug them one by one.
# -*- coding: utf-8 -*-
# Sort & Select SubSurfaces (Brep Faces) of Polysurfaces relative to current CPlane
# 1) is_planar_and_parallel_to_CplaneXY
# 2) SubSrf_selection
# 3) Command_Line_Choice
# + Copy_Lists_Of_planar_and_parallel
# + Floor_or_Ceiling (Ceilings_lists / Floors_lists)
# + auto-convert selected Extrusions → Breps (polysurfaces)
# + NEW: Copy & filter non_parallel into four buckets:
# - Planar_Surfaces
# - NonPlanar_Surfaces
# - Planar_Perpendicular_to_CPlane
# - Planar_Not_Perpendicular_to_CPlane
# Alias SF
#-RunPythonScript (c:\RhinoPythonScripts\SortFaces.py)
# Sort faces 251010
import Rhino
import rhinoscriptsyntax as rs
import scriptcontext as sc
import math
from System import Guid
# -------------------------------------------------
# Core
# -------------------------------------------------
def is_planar_and_parallel_to_CplaneXY(brep, abs_tol=None, ang_tol_deg=None):
if brep is None:
return [], []
if abs_tol is None:
abs_tol = sc.doc.ModelAbsoluteTolerance
if ang_tol_deg is None:
ang_tol_deg = sc.doc.ModelAngleToleranceDegrees
view = sc.doc.Views.ActiveView
if view is None:
return [], list(range(brep.Faces.Count))
zaxis = view.ActiveViewport.ConstructionPlane().ZAxis
ang_tol_rad = math.radians(ang_tol_deg)
planar_and_parallel, non_parallel = [], []
for fi, face in enumerate(brep.Faces):
if not face.IsPlanar(abs_tol):
non_parallel.append(fi)
continue
ok, pl = face.TryGetPlane(abs_tol)
if not ok:
non_parallel.append(fi)
continue
n = pl.Normal
ang = Rhino.Geometry.Vector3d.VectorAngle(n, zaxis)
if (ang <= ang_tol_rad) or (abs(ang - math.pi) <= ang_tol_rad):
planar_and_parallel.append(fi)
else:
non_parallel.append(fi)
return planar_and_parallel, non_parallel
def SubSrf_selection(obj_id, face_indices):
if not obj_id or not isinstance(obj_id, Guid):
return 0
rh_obj = sc.doc.Objects.Find(obj_id)
if rh_obj is None:
return 0
sel_count = 0
for fi in face_indices:
ci = Rhino.Geometry.ComponentIndex(Rhino.Geometry.ComponentIndexType.BrepFace, int(fi))
ok = False
try:
ok = rh_obj.SelectSubObject(ci, True, True, True, False)
except:
try:
# fallback with one more True as requested earlier
ok = rh_obj.SelectSubObject(ci, True, True, True)
except:
ok = False
if ok:
sel_count += 1
return sel_count
def build_index_maps(obj_ids, abs_tol=None, ang_tol_deg=None):
maps = {}
for oid in obj_ids:
rh_obj = sc.doc.Objects.Find(oid)
if rh_obj is None:
continue
brep = rh_obj.Geometry
if not isinstance(brep, Rhino.Geometry.Brep):
continue
p_and_p, non_p = is_planar_and_parallel_to_CplaneXY(brep, abs_tol, ang_tol_deg)
maps[oid] = {"planar_and_parallel": p_and_p, "non_parallel": non_p}
return maps
def preview_choice(obj_ids, maps, choice_key):
sc.doc.Objects.UnselectAll()
total = 0
for oid in obj_ids:
idxs = maps.get(oid, {}).get(choice_key, [])
if idxs:
total += SubSrf_selection(oid, idxs)
sc.doc.Views.Redraw()
return total
# -------------------------------------------------
# Copy + Floor/Ceiling classification
# -------------------------------------------------
def Copy_Lists_Of_planar_and_parallel(maps):
copy_map = {}
for oid, d in maps.items():
copy_map[oid] = list(d.get("planar_and_parallel", []))
return copy_map
def _bbox_union_for_faces(brep, face_indices, plane):
bbox = None
for fi in face_indices:
face = brep.Faces[int(fi)]
fb = face.GetBoundingBox(plane)
bbox = fb if bbox is None else Rhino.Geometry.BoundingBox.Union(bbox, fb)
return bbox
def _face_centroid_world(brep_face):
try:
dup = brep_face.DuplicateFace(True)
amp = Rhino.Geometry.AreaMassProperties.Compute(dup)
if amp:
return amp.Centroid
except:
pass
return brep_face.GetBoundingBox(True).Center
def Floor_or_Ceiling(planar_and_parallel_copy):
ceilings_map = {}
floors_map = {}
view = sc.doc.Views.ActiveView
if view is None:
return ceilings_map, floors_map
base_cplane = view.ActiveViewport.ConstructionPlane()
tol = sc.doc.ModelAbsoluteTolerance
for oid, face_list in planar_and_parallel_copy.items():
if not face_list:
continue
rh_obj = sc.doc.Objects.Find(oid)
if rh_obj is None:
continue
brep = rh_obj.Geometry
if not isinstance(brep, Rhino.Geometry.Brep):
continue
bbox = _bbox_union_for_faces(brep, face_list, base_cplane)
if bbox is None or not bbox.IsValid:
continue
centroid = bbox.Center
plane_at_ctr = Rhino.Geometry.Plane(base_cplane)
plane_at_ctr.Origin = centroid
above, below = [], []
for fi in face_list:
face = brep.Faces[int(fi)]
c = _face_centroid_world(face)
v = c - plane_at_ctr.Origin
dot = Rhino.Geometry.Vector3d.Multiply(v, plane_at_ctr.ZAxis)
if dot > tol:
above.append(int(fi))
else:
below.append(int(fi))
if above:
ceilings_map[oid] = above
if below:
floors_map[oid] = below
return ceilings_map, floors_map
def _preview_simple_map(simple_map):
sc.doc.Objects.UnselectAll()
total = 0
for oid, idxs in simple_map.items():
if idxs:
total += SubSrf_selection(oid, idxs)
sc.doc.Views.Redraw()
return total
# -------------------------------------------------
# NEW: Copy & filter non_parallel into 4 buckets
# -------------------------------------------------
def Copy_Lists_Of_non_parallel(maps):
"""
Returns a copy of non_parallel indices per object.
{ obj_id: [face_idx, ...], ... }
"""
out = {}
for oid, d in maps.items():
out[oid] = list(d.get("non_parallel", []))
return out
def Filter_NonParallel_Subsets(non_parallel_copy):
"""
Split the copied non_parallel faces into:
- Planar_Surfaces
- NonPlanar_Surfaces
- Planar_Perpendicular_to_CPlane (angle ~ 90° to CPlane Z)
- Planar_Not_Perpendicular_to_CPlane (planar, but not ⟂ and not ∥)
Returns four dicts keyed by object id.
"""
view = sc.doc.Views.ActiveView
if view is None:
return {}, {}, {}, {}
zaxis = view.ActiveViewport.ConstructionPlane().ZAxis
abs_tol = sc.doc.ModelAbsoluteTolerance
ang_tol_rad = math.radians(sc.doc.ModelAngleToleranceDegrees)
planar_map = {}
nonplanar_map = {}
perp_map = {}
not_perp_map = {}
for oid, face_list in non_parallel_copy.items():
if not face_list:
continue
rh_obj = sc.doc.Objects.Find(oid)
if rh_obj is None:
continue
brep = rh_obj.Geometry
if not isinstance(brep, Rhino.Geometry.Brep):
continue
planar_idxs = []
nonplanar_idxs = []
perp_idxs = []
notperp_idxs = []
for fi in face_list:
face = brep.Faces[int(fi)]
if face.IsPlanar(abs_tol):
planar_idxs.append(int(fi))
ok, pl = face.TryGetPlane(abs_tol)
if ok and pl:
n = pl.Normal
ang = Rhino.Geometry.Vector3d.VectorAngle(n, zaxis)
# Perpendicular if within tolerance of 90 degrees
if abs(ang - (math.pi * 0.5)) <= ang_tol_rad:
perp_idxs.append(int(fi))
else:
# These are planar but neither perpendicular nor parallel (since we started from non_parallel)
notperp_idxs.append(int(fi))
else:
# If we can't get a plane robustly, treat as planar-not-perp (still planar but unknown angle)
notperp_idxs.append(int(fi))
else:
nonplanar_idxs.append(int(fi))
if planar_idxs:
planar_map[oid] = planar_idxs
if nonplanar_idxs:
nonplanar_map[oid] = nonplanar_idxs
if perp_idxs:
perp_map[oid] = perp_idxs
if notperp_idxs:
not_perp_map[oid] = notperp_idxs
return planar_map, nonplanar_map, perp_map, not_perp_map
# -------------------------------------------------
# Extrusion → Brep converter
# -------------------------------------------------
def _ensure_polysurfaces(obj_ids):
out_ids = []
for oid in obj_ids:
rh_obj = sc.doc.Objects.Find(oid)
if rh_obj is None:
continue
geom = rh_obj.Geometry
if isinstance(geom, Rhino.Geometry.Extrusion):
brep = geom.ToBrep(True)
if brep:
if sc.doc.Objects.Replace(oid, brep):
out_ids.append(oid)
else:
nid = sc.doc.Objects.AddBrep(brep, rh_obj.Attributes)
if nid:
sc.doc.Objects.Delete(oid, True)
out_ids.append(nid)
else:
out_ids.append(oid)
else:
out_ids.append(oid)
else:
out_ids.append(oid)
sc.doc.Views.Redraw()
return out_ids
# -------------------------------------------------
# UI
# -------------------------------------------------
def Command_Line_Choice():
filt = rs.filter.polysurface | rs.filter.extrusion
# preselect = True (as you requested earlier)
obj_ids = rs.GetObjects("Select polysurfaces (extrusions allowed)", filt, preselect=True, select=False)
if not obj_ids:
return
obj_ids = _ensure_polysurfaces(obj_ids)
maps = build_index_maps(obj_ids)
current_choice = "planar_and_parallel"
preview_choice(obj_ids, maps, current_choice)
# caches for existing features
planar_copy_cache = None
ceilings_cache = None
floors_cache = None
# caches for the new non-parallel subsets
nonpar_copy_cache = None
planar_map_cache = None
nonplanar_map_cache = None
perp_map_cache = None
notperp_map_cache = None
go = Rhino.Input.Custom.GetOption()
go.SetCommandPrompt("Choose SubSurface group. Press Enter to accept.")
opt_planar = go.AddOption("planar_and_parallel")
opt_nonpar = go.AddOption("non_parallel")
opt_ceil = go.AddOption("Ceilings_lists")
opt_floor = go.AddOption("Floors_lists")
# NEW options
opt_np_planar = go.AddOption("Planar_Surfaces")
opt_np_nonplanar = go.AddOption("NonPlanar_Surfaces")
opt_np_perp = go.AddOption("Planar_Perpendicular_to_CPlane")
opt_np_notperp = go.AddOption("Planar_Not_Perpendicular_to_CPlane")
go.AcceptNothing(True)
while True:
res = go.Get()
if res == Rhino.Input.GetResult.Option:
idx = go.OptionIndex()
if idx == opt_planar:
current_choice = "planar_and_parallel"
count = preview_choice(obj_ids, maps, current_choice)
Rhino.RhinoApp.WriteLine("Preview: {0} faces selected.".format(count))
elif idx == opt_nonpar:
current_choice = "non_parallel"
count = preview_choice(obj_ids, maps, current_choice)
Rhino.RhinoApp.WriteLine("Preview: {0} faces selected.".format(count))
elif idx == opt_ceil:
current_choice = "Ceilings_lists"
if planar_copy_cache is None:
planar_copy_cache = Copy_Lists_Of_planar_and_parallel(maps)
if ceilings_cache is None or floors_cache is None:
ceilings_cache, floors_cache = Floor_or_Ceiling(planar_copy_cache)
count = _preview_simple_map(ceilings_cache if ceilings_cache else {})
Rhino.RhinoApp.WriteLine("Preview (Ceilings): {0} faces selected.".format(count))
elif idx == opt_floor:
current_choice = "Floors_lists"
if planar_copy_cache is None:
planar_copy_cache = Copy_Lists_Of_planar_and_parallel(maps)
if ceilings_cache is None or floors_cache is None:
ceilings_cache, floors_cache = Floor_or_Ceiling(planar_copy_cache)
count = _preview_simple_map(floors_cache if floors_cache else {})
Rhino.RhinoApp.WriteLine("Preview (Floors): {0} faces selected.".format(count))
# ------ NEW four subsets from non_parallel copy ------
elif idx == opt_np_planar:
current_choice = "Planar_Surfaces"
if nonpar_copy_cache is None:
nonpar_copy_cache = Copy_Lists_Of_non_parallel(maps)
if planar_map_cache is None:
(planar_map_cache,
nonplanar_map_cache,
perp_map_cache,
notperp_map_cache) = Filter_NonParallel_Subsets(nonpar_copy_cache)
count = _preview_simple_map(planar_map_cache or {})
Rhino.RhinoApp.WriteLine("Preview (Planar from non_parallel): {0} faces selected.".format(count))
elif idx == opt_np_nonplanar:
current_choice = "NonPlanar_Surfaces"
if nonpar_copy_cache is None:
nonpar_copy_cache = Copy_Lists_Of_non_parallel(maps)
if nonplanar_map_cache is None:
(planar_map_cache,
nonplanar_map_cache,
perp_map_cache,
notperp_map_cache) = Filter_NonParallel_Subsets(nonpar_copy_cache)
count = _preview_simple_map(nonplanar_map_cache or {})
Rhino.RhinoApp.WriteLine("Preview (NonPlanar from non_parallel): {0} faces selected.".format(count))
elif idx == opt_np_perp:
current_choice = "Planar_Perpendicular_to_CPlane"
if nonpar_copy_cache is None:
nonpar_copy_cache = Copy_Lists_Of_non_parallel(maps)
if perp_map_cache is None:
(planar_map_cache,
nonplanar_map_cache,
perp_map_cache,
notperp_map_cache) = Filter_NonParallel_Subsets(nonpar_copy_cache)
count = _preview_simple_map(perp_map_cache or {})
Rhino.RhinoApp.WriteLine("Preview (Planar ⟂ CPlane): {0} faces selected.".format(count))
elif idx == opt_np_notperp:
current_choice = "Planar_Not_Perpendicular_to_CPlane"
if nonpar_copy_cache is None:
nonpar_copy_cache = Copy_Lists_Of_non_parallel(maps)
if notperp_map_cache is None:
(planar_map_cache,
nonplanar_map_cache,
perp_map_cache,
notperp_map_cache) = Filter_NonParallel_Subsets(nonpar_copy_cache)
count = _preview_simple_map(notperp_map_cache or {})
Rhino.RhinoApp.WriteLine("Preview (Planar not ⟂ CPlane): {0} faces selected.".format(count))
continue
elif res == Rhino.Input.GetResult.Nothing:
sc.doc.Objects.UnselectAll()
total = 0
if current_choice in ("planar_and_parallel", "non_parallel"):
for oid in obj_ids:
idxs = maps.get(oid, {}).get(current_choice, [])
if idxs:
total += SubSrf_selection(oid, idxs)
elif current_choice == "Ceilings_lists":
if planar_copy_cache is None:
planar_copy_cache = Copy_Lists_Of_planar_and_parallel(maps)
if ceilings_cache is None:
ceilings_cache, floors_cache = Floor_or_Ceiling(planar_copy_cache)
for oid, idxs in (ceilings_cache or {}).items():
if idxs:
total += SubSrf_selection(oid, idxs)
elif current_choice == "Floors_lists":
if planar_copy_cache is None:
planar_copy_cache = Copy_Lists_Of_planar_and_parallel(maps)
if floors_cache is None:
ceilings_cache, floors_cache = Floor_or_Ceiling(planar_copy_cache)
for oid, idxs in (floors_cache or {}).items():
if idxs:
total += SubSrf_selection(oid, idxs)
elif current_choice in ("Planar_Surfaces",
"NonPlanar_Surfaces",
"Planar_Perpendicular_to_CPlane",
"Planar_Not_Perpendicular_to_CPlane"):
if nonpar_copy_cache is None:
nonpar_copy_cache = Copy_Lists_Of_non_parallel(maps)
if any(m is None for m in (planar_map_cache, nonplanar_map_cache, perp_map_cache, notperp_map_cache)):
(planar_map_cache,
nonplanar_map_cache,
perp_map_cache,
notperp_map_cache) = Filter_NonParallel_Subsets(nonpar_copy_cache)
sel_map = {}
if current_choice == "Planar_Surfaces":
sel_map = planar_map_cache or {}
elif current_choice == "NonPlanar_Surfaces":
sel_map = nonplanar_map_cache or {}
elif current_choice == "Planar_Perpendicular_to_CPlane":
sel_map = perp_map_cache or {}
elif current_choice == "Planar_Not_Perpendicular_to_CPlane":
sel_map = notperp_map_cache or {}
for oid, idxs in sel_map.items():
if idxs:
total += SubSrf_selection(oid, idxs)
sc.doc.Views.Redraw()
Rhino.RhinoApp.WriteLine("Selection validated for: {0} (faces: {1})".format(current_choice, total))
break
elif res == Rhino.Input.GetResult.Cancel:
sc.doc.Objects.UnselectAll()
sc.doc.Views.Redraw()
Rhino.RhinoApp.WriteLine("Canceled.")
return
sc.doc.Views.Redraw()
if __name__ == "__main__":
Command_Line_Choice()
This topic is related to:
3 posts - 2 participants