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