Quantcast
Channel: Scripting - McNeel Forum
Viewing all articles
Browse latest Browse all 5938

I asked ChatGPT to filter Sub-Surfaces

$
0
0

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

Read full topic


Viewing all articles
Browse latest Browse all 5938

Trending Articles