I asked ChatGPT to UnFillet edges

Filleting edges of polysurfaces is quite fast but there’s no proper solution for un filleting them back into straight corners.

I managed to work something out with ChatGPT. The code is far from perfect but it solves an issue that takes otherwise a lot of manual work. I would love to see something similar implemented in Rhino 9…

(Also, I am aware that FilletEdge has an Edit option - but I cannot see how i can change the corner i want to unfillet, the UI is very strange.)

Here’s my test and the script.

# UnFillet edges
import Rhino
import scriptcontext as sc
from Rhino.Geometry import Brep, Curve, AreaMassProperties
from Rhino.Input.Custom import GetObject
from Rhino.DocObjects import ObjectType
from System.Collections.Generic import List


DEBUG = True
MIN_AREA_RATIO = 0.15


def log(x):
    if DEBUG:
        print(x)


def get_face(prompt):
    go = GetObject()
    go.SetCommandPrompt(prompt)
    go.GeometryFilter = ObjectType.Surface
    go.SubObjectSelect = True
    go.EnablePreSelect(False, True)
    go.Get()

    if go.CommandResult() != Rhino.Commands.Result.Success:
        return None, None, None

    objref = go.Object(0)
    rh_obj = objref.Object()
    face = objref.Face()

    if rh_obj is None or face is None:
        return None, None, None

    return rh_obj, rh_obj.Geometry, face.FaceIndex


def shared_edges_between_faces(brep, face_a, face_b):
    edges = []

    for ei in brep.Faces[face_a].AdjacentEdges():
        edge = brep.Edges[ei]

        for ti in edge.TrimIndices():
            tr = brep.Trims[ti]
            if tr.Face and tr.Face.FaceIndex == face_b:
                edges.append(ei)
                break

    return edges


def duplicate_face_brep(brep, face_index):
    return brep.Faces[face_index].DuplicateFace(False)


def extract_breps(result):
    breps = []

    if isinstance(result, Brep):
        breps.append(result)
    elif isinstance(result, tuple) or isinstance(result, list):
        for item in result:
            if isinstance(item, Brep):
                breps.append(item)

    return breps


def brep_area(b):
    amp = AreaMassProperties.Compute(b)
    return amp.Area if amp else 0.0


def face_center(brep, face_index):
    return brep.Faces[face_index].GetBoundingBox(True).Center


def brep_center(brep):
    return brep.GetBoundingBox(True).Center


def preserved_boundary_curves(original_brep, side_face_i, fillet_face_i):
    fillet_edges = set(shared_edges_between_faces(original_brep, side_face_i, fillet_face_i))

    cutters = List[Curve]()

    for ei in original_brep.Faces[side_face_i].AdjacentEdges():
        if ei in fillet_edges:
            continue

        crv = original_brep.Edges[ei].DuplicateCurve()
        if crv:
            cutters.Add(crv)

    return cutters


def choose_best_piece(pieces, original_brep, side_face_i):
    target = face_center(original_brep, side_face_i)

    data = []
    max_area = 0.0

    for p in pieces:
        a = brep_area(p)
        max_area = max(max_area, a)
        data.append((p, a))

    if max_area <= 0:
        return pieces[0]

    candidates = []

    for p, a in data:
        if a < max_area * MIN_AREA_RATIO:
            continue

        d = brep_center(p).DistanceTo(target)
        candidates.append((d, -a, p))

    if candidates:
        candidates.sort()
        return candidates[0][2]

    data.sort(key=lambda x: x[1], reverse=True)
    return data[0][0]


def trim_rebuilt_face(rebuilt_brep, original_brep, side_face_i, fillet_face_i, tol):
    cutters = preserved_boundary_curves(original_brep, side_face_i, fillet_face_i)

    if cutters.Count == 0:
        print("No cutters found for side face", side_face_i)
        return None

    try:
        pieces = rebuilt_brep.Split(cutters, tol)
    except Exception as e:
        print("Curve split failed on side face", side_face_i)
        print(e)
        return None

    if not pieces or len(pieces) == 0:
        print("Split made no pieces on side face", side_face_i)
        return None

    best = choose_best_piece(pieces, original_brep, side_face_i)

    if best:
        try:
            best.Compact()
            best.Faces.ShrinkFaces()
        except:
            pass
        return best

    return None


def match_rebuilt_to_original_faces(rebuilt, original_brep, main_a, main_b):
    ca = face_center(original_brep, main_a)
    cb = face_center(original_brep, main_b)

    r0 = rebuilt[0]
    r1 = rebuilt[1]

    score_normal = brep_center(r0).DistanceTo(ca) + brep_center(r1).DistanceTo(cb)
    score_swapped = brep_center(r0).DistanceTo(cb) + brep_center(r1).DistanceTo(ca)

    if score_normal <= score_swapped:
        return r0, r1
    else:
        return r1, r0


def unfillet_manual_faces():
    tol = sc.doc.ModelAbsoluteTolerance
    angle_tol = sc.doc.ModelAngleToleranceRadians

    rh_obj, brep, fillet_i = get_face("Select fillet face to remove")
    if brep is None:
        print("No fillet face selected.")
        return

    rh_obj_a, brep_a, main_a = get_face("Select first side face")
    if brep_a is None or rh_obj_a.Id != rh_obj.Id:
        print("First side face must be on same Brep.")
        return

    rh_obj_b, brep_b, main_b = get_face("Select second side face")
    if brep_b is None or rh_obj_b.Id != rh_obj.Id:
        print("Second side face must be on same Brep.")
        return

    edges_a = shared_edges_between_faces(brep, fillet_i, main_a)
    edges_b = shared_edges_between_faces(brep, fillet_i, main_b)

    if not edges_a or not edges_b:
        print("Selected side faces must touch the fillet face.")
        return

    edge_a = edges_a[0]
    edge_b = edges_b[0]

    print("Fillet face:", fillet_i)
    print("Side faces:", main_a, main_b)
    print("Fillet-side edges:", edge_a, edge_b)

    try:
        result = Brep.ExtendBrepFacesToConnect(
            brep.Faces[main_a],
            edge_a,
            brep.Faces[main_b],
            edge_b,
            tol,
            angle_tol
        )
    except Exception as e:
        print("ExtendBrepFacesToConnect failed:")
        print(e)
        return

    rebuilt = extract_breps(result)

    if len(rebuilt) < 2:
        print("Could not create replacement sharp faces.")
        return

    rebuilt_a, rebuilt_b = match_rebuilt_to_original_faces(
        rebuilt,
        brep,
        main_a,
        main_b
    )

    trimmed_a = trim_rebuilt_face(rebuilt_a, brep, main_a, fillet_i, tol)
    trimmed_b = trim_rebuilt_face(rebuilt_b, brep, main_b, fillet_i, tol)

    if trimmed_a is None or trimmed_b is None:
        print("Trimming failed. Original object was NOT hidden.")
        print("Adding rebuilt faces only for debugging.")
        sc.doc.Objects.AddBrep(rebuilt_a)
        sc.doc.Objects.AddBrep(rebuilt_b)
        sc.doc.Views.Redraw()
        return

    parts = [trimmed_a, trimmed_b]

    skip = set([fillet_i, main_a, main_b])

    for i in range(brep.Faces.Count):
        if i in skip:
            continue

        fb = duplicate_face_brep(brep, i)
        if fb:
            parts.append(fb)

    joined = Brep.JoinBreps(parts, tol)

    if joined and len(joined) > 0:
        final_brep = joined[0]

        try:
            final_brep.Faces.ShrinkFaces()
        except:
            pass

        capped = final_brep.CapPlanarHoles(tol)
        if capped and capped.IsValid:
            final_brep = capped

        sc.doc.Objects.AddBrep(final_brep)
        sc.doc.Objects.Hide(rh_obj.Id, True)
        print("Unfillet complete. Original hidden.")
    else:
        print("Join failed. Original object was NOT hidden.")
        for p in parts:
            sc.doc.Objects.AddBrep(p)

    sc.doc.Views.Redraw()


unfillet_manual_faces()

This topic is related to:

This is a very usefull option!
What’s the difference with the connect tools?

If I am not mistaken, FilletEdge can edit edges that were created with the FilletEdge command. An mostly if it was the last command to edit a polysurface. But don’t quote me on that :stuck_out_tongue:

It seems its connect but a bit more automated, using edge surf on the remaining holes and not having to manually extract and delete the fillet face. I like it.

Two thoughts come to mind:

  • Why should they, if one can use a LLM to create spezialized tools at ease. Testing will cost McNeel way more time to cover “all cases”.
  • What i would really like to see is something like the _PackageManager for scripts. Not very happy, how scripts are currently treated. Downloaded somewhere from the forum, stored somewhere, hidden in the UI.

Strangely FilletEdge with Edit allows me to change the fillet but I cannot pick which fillet I want to change. The functionality seems to be there but not fully implemented.

Can someone from McNeel explain this to me please?