[Request] Quick & Dirty lay flat

If someone is interested, can you make this one (in Python):

  • Call the script via macro or keystroke
  • if object wasn’t pre-selected, an option to select the object
  • click on a planar face of the object
  • that face lays flat on world plane

that’s it

whatever orientation that face happens to be in space is how it plops to Z=0
(as in, there’s no describing the plane using something like Orient3pt)

for example, say you select a surface with Auto CPlane turned on. Whatever that chooses for X,Y,Z can just keep X,Y point the same and just remaps to the regular cplane with Z being at zero.

just looking for a one-click deal to get an object laying flat with desired surface facing downwards.

thanks

Do you want to just move the bottom face or do you want to reorient the entire object such that the bottom face is z= 0

the whole object moves

Hello,
It can be run in ScriptEditor in Rhino8.
I made it so easy that it might work weird…


SetFaceToXYPlane.cs (1.3 KB)

Hi @11159 – How would I possibly make this work for meshes?

Thanks for posting!

Hi, @Alan_Farkas

I have not touched Mesh much, so I don’t know how to find the proper normals for a pentagonal or larger mesh,

so I tried to run it for triangular and quadrilateral mesh faces.

If I need more than pentagons, I will try to figure out how to do it.
SetMeshFaceToXYPlane.cs (1.7 KB)

Amazing, thanks so much!

Hi!

How to get it work in Rhino7?
Can I add it to new button?

Thanks :slight_smile:

Since there is no ScriptEditor in Rhino7, it must be written in Python using EditPythonScript.

I got it converted with Gemini and it works.
The comments are probably correct, as they were made automatically by the AI..


SetFaceToXYPlane.py (2.9 KB)

Thank you very much. This is exactly what I needed :slight_smile:

Here’s a python script for anyone wanting this functionality

select an object you want laid flat on worldXY and this will attempt to orient it in such a way that the machinable side is facing up (a piece with a pocket in it will face up)

a preview shows where it will be moved or copied to on XY z=0.

probably only works good on flat/rectangular type objects. I’m not really sure what happens with a bunch of curved surfaces. ymmv

# -*- coding: utf-8 -*-

import rhinoscriptsyntax as rs
import scriptcontext as sc
import Rhino
import math
import System.Drawing as SD

# ============================================================
# SETTINGS (STICKY, PERSISTS DURING SESSION)
# ============================================================

STICKY_KEY_COPY = "FLATPLACE_COPY_MODE"

def get_sticky_copy_default():
    if sc.sticky.has_key(STICKY_KEY_COPY):
        return bool(sc.sticky[STICKY_KEY_COPY])
    return False

def set_sticky_copy_value(val):
    sc.sticky[STICKY_KEY_COPY] = bool(val)

# ============================================================
# BASIC UTILITIES
# ============================================================

def rotation_between(vec_from, vec_to, origin=Rhino.Geometry.Point3d(0,0,0)):
    v1 = Rhino.Geometry.Vector3d(vec_from)
    v2 = Rhino.Geometry.Vector3d(vec_to)
    if not v1.Unitize() or not v2.Unitize():
        return None
    axis = Rhino.Geometry.Vector3d.CrossProduct(v1, v2)
    if axis.IsTiny():
        axis = Rhino.Geometry.Vector3d.ZAxis
    angle = Rhino.Geometry.Vector3d.VectorAngle(v1, v2)
    return Rhino.Geometry.Transform.Rotation(angle, axis, origin)

def get_bbox_center(obj_id):
    bb = rs.BoundingBox(obj_id)
    xs = [pt.X for pt in bb]
    ys = [pt.Y for pt in bb]
    zs = [pt.Z for pt in bb]
    return Rhino.Geometry.Point3d(
        (min(xs) + max(xs)) / 2.0,
        (min(ys) + max(ys)) / 2.0,
        (min(zs) + max(zs)) / 2.0
    )

def get_xy_bbox(obj_id):
    bb = rs.BoundingBox(obj_id)
    xs = [p.X for p in bb]
    ys = [p.Y for p in bb]
    return (min(xs), min(ys), max(xs), max(ys))

def get_bbox_xy_rectangle(obj_id):
    minx, miny, maxx, maxy = get_xy_bbox(obj_id)
    pts = [
        Rhino.Geometry.Point3d(minx, miny, 0),
        Rhino.Geometry.Point3d(maxx, miny, 0),
        Rhino.Geometry.Point3d(maxx, maxy, 0),
        Rhino.Geometry.Point3d(minx, maxy, 0),
        Rhino.Geometry.Point3d(minx, miny, 0),
    ]
    return Rhino.Geometry.Polyline(pts)

# ============================================================
# FLATTEN ONE OBJECT USING SAME "WHICH WAY IS UP" LOGIC
# ============================================================

def flatten_one(obj_id):
    brep = rs.coercebrep(obj_id)
    if not brep:
        print("Not a valid Brep/Polysurface.")
        return False

    # ---- 1) Collect planar faces ----
    planar_faces = []  # (face, normal, area, uv)
    for face in brep.Faces:
        if not face.IsPlanar():
            continue

        amp = Rhino.Geometry.AreaMassProperties.Compute(face)
        if not amp:
            continue
        area = amp.Area

        udom = face.Domain(0)
        vdom = face.Domain(1)
        u = (udom[0] + udom[1]) * 0.5
        v = (vdom[0] + vdom[1]) * 0.5

        normal = face.NormalAt(u, v)
        if not normal.Unitize():
            continue

        planar_faces.append((face, normal, area, (u, v)))

    if len(planar_faces) < 2:
        print("Not enough planar faces to determine up.")
        return False

    # ---- 2) Find best opposite pair using LARGEST FACE rule ----
    best_pair = None
    best_dominant_face_area = -1.0

    for i in range(len(planar_faces)):
        fA, nA, aA, uvA = planar_faces[i]
        for j in range(i + 1, len(planar_faces)):
            fB, nB, aB, uvB = planar_faces[j]
            if Rhino.Geometry.Vector3d.Multiply(nA, nB) < -0.8:
                dominant = max(aA, aB)
                if dominant > best_dominant_face_area:
                    best_dominant_face_area = dominant
                    best_pair = ((fA, nA, aA, uvA), (fB, nB, aB, uvB))

    if not best_pair:
        planar_faces.sort(key=lambda x: x[2], reverse=True)
        best_pair = (planar_faces[0], planar_faces[1])

    (faceA, nA, areaA, uvA), (faceB, nB, areaB, uvB) = best_pair

    # ---- 3) Machining side = smaller-area face ----
    machining_face = faceA if areaA < areaB else faceB

    # ---- 4) Sample point+normal ----
    udom_m = machining_face.Domain(0)
    vdom_m = machining_face.Domain(1)
    u_m = (udom_m[0] + udom_m[1]) * 0.5
    v_m = (vdom_m[0] + vdom_m[1]) * 0.5

    pt = machining_face.PointAt(u_m, v_m)
    normal = machining_face.NormalAt(u_m, v_m)
    normal.Unitize()

    # ---- 5) Move sample point to origin ----
    to_origin = Rhino.Geometry.Transform.Translation(-pt.X, -pt.Y, -pt.Z)
    sc.doc.Objects.Transform(obj_id, to_origin, True)

    # ---- 6) Rotate machining normal → +Z ----
    rot = rotation_between(normal, Rhino.Geometry.Vector3d.ZAxis)
    if rot:
        sc.doc.Objects.Transform(obj_id, rot, True)

    # ---- 7) Sit on Z=0 ----
    bb2 = rs.BoundingBox(obj_id)
    zmin = min(p.Z for p in bb2)
    lift = Rhino.Geometry.Transform.Translation(0, 0, -zmin)
    sc.doc.Objects.Transform(obj_id, lift, True)

    # ---- 8) Align longest flat edge → +Y ----
    brep_flat = rs.coercebrep(obj_id)
    longest = 0.0
    best_vec = None

    if brep_flat:
        for edge in brep_flat.Edges:
            crv = edge.ToNurbsCurve()
            if not crv or not crv.IsLinear():
                continue
            p0 = crv.PointAtStart
            p1 = crv.PointAtEnd
            v = Rhino.Geometry.Vector3d(p1 - p0)
            v.Z = 0.0
            length = v.Length
            if length > longest:
                longest = length
                best_vec = v

    if best_vec and longest > 1e-6:
        v = Rhino.Geometry.Vector3d(best_vec)
        if v.Unitize():

            if Rhino.Geometry.Vector3d.Multiply(v, Rhino.Geometry.Vector3d.YAxis) < 0:
                v = -v

            yaxis = Rhino.Geometry.Vector3d.YAxis
            ang = Rhino.Geometry.Vector3d.VectorAngle(v, yaxis)
            cross = Rhino.Geometry.Vector3d.CrossProduct(v, yaxis)
            if cross.Z < 0:
                ang = -ang

            center3 = Rhino.Geometry.BoundingBox(rs.BoundingBox(obj_id)).Center
            rot_z = Rhino.Geometry.Transform.Rotation(
                ang, Rhino.Geometry.Vector3d.ZAxis, center3
            )
            sc.doc.Objects.Transform(obj_id, rot_z, True)

    # ---- 9) POST-FLATTEN FIX: smallest horizontal face must face UP ----
    brep2 = rs.coercebrep(obj_id)
    if brep2:
        top_area = None
        bottom_area = None

        for face in brep2.Faces:
            if not face.IsPlanar():
                continue

            amp2 = Rhino.Geometry.AreaMassProperties.Compute(face)
            if not amp2:
                continue
            area2 = amp2.Area

            udom = face.Domain(0)
            vdom = face.Domain(1)
            u = 0.5*(udom[0] + udom[1])
            v = 0.5*(vdom[0] + vdom[1])
            n = face.NormalAt(u, v)
            n.Unitize()

            if abs(n.Z) > 0.9:
                if n.Z > 0:
                    top_area = area2
                else:
                    bottom_area = area2

        if top_area is not None and bottom_area is not None:
            if bottom_area < top_area:
                centerpt = get_bbox_center(obj_id)
                flip_x = Rhino.Geometry.Transform.Rotation(
                    math.radians(180),
                    Rhino.Geometry.Vector3d.XAxis,
                    centerpt
                )
                sc.doc.Objects.Transform(obj_id, flip_x, True)

                bb3 = rs.BoundingBox(obj_id)
                zmin3 = min(p.Z for p in bb3)
                lift2 = Rhino.Geometry.Transform.Translation(0, 0, -zmin3)
                sc.doc.Objects.Transform(obj_id, lift2, True)

    return True

# ============================================================
# PREVIEW GETPOINT (DRAW CYAN RECTANGLE)
# ============================================================

class PlaceWithPreview(Rhino.Input.Custom.GetPoint):
    def __init__(self, obj_id):
        Rhino.Input.Custom.GetPoint.__init__(self)
        self.obj_id = obj_id
        self.SetCommandPrompt("Pick placement point (drops to World XY)")

    def OnDynamicDraw(self, e):
        try:
            pt = e.CurrentPoint
            pt = Rhino.Geometry.Point3d(pt.X, pt.Y, 0)

            base_rect = get_bbox_xy_rectangle(self.obj_id)

            minx, miny, _, _ = get_xy_bbox(self.obj_id)
            dx = pt.X - minx
            dy = pt.Y - miny
            xform = Rhino.Geometry.Transform.Translation(dx, dy, 0.0)

            moved = Rhino.Geometry.Polyline(base_rect)
            moved.Transform(xform)

            e.Display.DrawPolyline(moved, SD.Color.Cyan, 2)
        except:
            pass

# ============================================================
# PLACE FLATTENED OBJECT AT PICKED POINT
# ============================================================

def move_to_point(obj_id, target_pt):
    minx, miny, _, _ = get_xy_bbox(obj_id)
    dx = target_pt.X - minx
    dy = target_pt.Y - miny
    xform = Rhino.Geometry.Transform.Translation(dx, dy, 0.0)
    sc.doc.Objects.Transform(obj_id, xform, True)

# ============================================================
# MAIN
# ============================================================

def main():
    import Rhino.Input.Custom

    # sticky default
    copy_default = get_sticky_copy_default()

    go = Rhino.Input.Custom.GetObject()
    go.SetCommandPrompt("Select one object to lay flat")
    go.GeometryFilter = (Rhino.DocObjects.ObjectType.Surface |
                         Rhino.DocObjects.ObjectType.PolysrfFilter |
                         Rhino.DocObjects.ObjectType.Brep)

    go.EnablePreSelect(True, True)
    go.EnablePostSelect(True)

    #AddOptionToggle returns (index, toggle) in Rhino Mac py2.7
    copy_toggle = Rhino.Input.Custom.OptionToggle(copy_default, "No", "Yes")
    opt_copy_tuple = go.AddOptionToggle("Copy", copy_toggle)
    opt_copy_toggle = opt_copy_tuple[1]

    selected_id = None

    #keep looping until an object is actually selected
    while True:
        res = go.Get()

        if res == Rhino.Input.GetResult.Cancel:
            return

        if res == Rhino.Input.GetResult.Option:
            # user toggled Copy: update sticky immediately
            set_sticky_copy_value(opt_copy_toggle.CurrentValue)
            continue

        if res == Rhino.Input.GetResult.Object:
            objref = go.Object(0)
            selected_id = objref.ObjectId
            break

        # anything else -> bail
        return

    do_copy = opt_copy_toggle.CurrentValue
    set_sticky_copy_value(do_copy)  # make sure it's stored

    working_id = selected_id

    if do_copy:
        dup = rs.CopyObject(selected_id)
        if not dup:
            print("Copy failed.")
            return
        working_id = dup

    # Hide while we flatten at origin (AND keep hidden until final placement)
    rs.HideObject(working_id)
    sc.doc.Views.Redraw()

    ok = flatten_one(working_id)
    if not ok:
        rs.ShowObject(working_id)
        sc.doc.Views.Redraw()
        return

    # Placement preview (object stays hidden; preview still draws)
    gp = PlaceWithPreview(working_id)
    if gp.Get() != Rhino.Input.GetResult.Point:
        rs.ShowObject(working_id)
        sc.doc.Views.Redraw()
        return

    pt = gp.Point()
    target_pt = Rhino.Geometry.Point3d(pt.X, pt.Y, 0.0)

    # Move while still hidden, then show at final location
    move_to_point(working_id, target_pt)
    rs.ShowObject(working_id)

    sc.doc.Views.Redraw()
    print("Done.")


main()

this is part of a much more involved nesting script I made with chatGPT.. I could share that one too if anyone wants it