[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

1 Like

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

1 Like

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!

1 Like

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)

1 Like

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

1 Like

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

5 Likes