This script was done in 2 parts:
First, uniformly distrubute points. Avoid points too close together or too far apart.
Then add more options:
- Number of points.
- Maximum random angle of rotation.
- Seed
- Use Cplane Z direction instead of surface normal.
Tested on Surfaces, Polysurfaces and Subsurfaces.
Grouped geometry is scattered as more groups.
Similar to PopGeo in Grasshopper.
# UI-driven scatter & orient tool
# Update: If input geometry is already grouped, DO NOT re-use those groups for the copies.
# New scattered copies are first removed from all inherited groups, then grouped per point.
# - Step 1 supports preselect + grouped selection for faces
# - Step 2 supports grouped selection for geometry
# - Deterministic seed for scatter & rotation
# - Preview points are always deleted on exit
import rhinoscriptsyntax as rs
import scriptcontext as sc
import Rhino
import Rhino.Geometry as rg
import random, math
# ---------------- Sampling & helpers ----------------
def area_weighted_picker_with_index(faces, rnd):
areas = [rg.AreaMassProperties.Compute(f).Area for f in faces]
A = sum(areas)
cuml=[]; s=0.0
for a in areas:
s+=a; cuml.append(s)
def pick_idx_face():
t = rnd.random()*A
for i,c in enumerate(cuml):
if t<=c: return i, faces[i], A
return len(faces)-1, faces[-1], A
return pick_idx_face, A
def rand_uv(face, rnd, tries=200):
du,dv = face.Domain(0), face.Domain(1)
u0,u1 = min(du.T0,du.T1), max(du.T0,du.T1)
v0,v1 = min(dv.T0,dv.T1), max(dv.T0,dv.T1)
for _ in range(tries):
u = rnd.uniform(u0,u1); v = rnd.uniform(v0,v1)
if face.IsPointOnFace(u,v) != rg.PointFaceRelation.Exterior: return u,v
return None
def scatter_points_on_faces(faces, N, seed, boost=1.30, K=12):
"""Return (points, face_indices), deterministic w.r.t. seed."""
if not faces or N<=0: return [], []
rnd = random.Random(seed)
pick, A = area_weighted_picker_with_index(faces, rnd)
r = math.sqrt(A/(float(N)*math.pi)) * boost
r2 = r*r
pts = []; face_indices = []
tries = 0; max_tries = 250*N
while len(pts) < N and tries < max_tries:
best = None # (best_d2, point, face_idx)
for _ in range(K):
fi, f, _A = pick()
uv = rand_uv(f, rnd)
if not uv: continue
p = f.PointAt(uv[0], uv[1])
if not pts:
best = (1e9, p, fi); break
md2 = min((p - q).SquareLength for q in pts)
if (best is None) or (md2 > best[0]):
best = (md2, p, fi)
if best is None:
tries += 1; continue
if best[0] >= r2 or (tries % 40 == 0):
pts.append(best[1]); face_indices.append(best[2])
tries += 1
return pts, face_indices
def EvaluateSurfaceFromPoints(faces, points):
planes = []
for p in points:
best = None
bestd2 = 1e300
for f in faces:
rc, u, v = f.ClosestPoint(p)
if not rc: continue
q = f.PointAt(u, v)
d2 = (q - p).SquareLength
if d2 < bestd2:
bestd2 = d2
ok, pl = f.FrameAt(u, v)
if ok: best = pl
if best is None:
best = rg.Plane(p, rg.Vector3d.ZAxis)
planes.append(best)
return planes
def RandomRotatePlanes(planes, max_deg=0.0, seed=1):
if not planes: return []
rnd = random.Random(seed)
out = []
for pl in planes:
p = rg.Plane(pl)
if max_deg > 0.0:
ang = math.radians(rnd.uniform(0.0, max_deg))
rot = rg.Transform.Rotation(ang, p.ZAxis, p.Origin)
p.Transform(rot)
out.append(p)
return out
def CplaneZ(apply_cplane_z, planes):
if not planes: return []
if not apply_cplane_z: return planes
view = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView
cplane = view.ActiveViewport.ConstructionPlane()
newplanes = []
for pl in planes:
p = rg.Plane(pl)
z = cplane.ZAxis
x = pl.XAxis
x -= rg.Vector3d.Multiply(x * z, z)
if x.IsTiny(): x = cplane.XAxis
x.Unitize()
y = rg.Vector3d.CrossProduct(z, x)
newplanes.append(rg.Plane(p.Origin, x, y))
return newplanes
def BoundingBoxBase(guids):
if not guids: return None
view = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView
cplane = view.ActiveViewport.ConstructionPlane()
bb = None
for g in guids:
obj = sc.doc.Objects.Find(g)
if not obj: continue
geo = obj.Geometry
b = geo.GetBoundingBox(cplane)
if bb is None: bb = b
else: bb = rg.BoundingBox.Union(bb, b)
if bb is None or not bb.IsValid:
return cplane
box = rg.Box(cplane, bb)
base_pt = cplane.PointAt(box.X.Mid, box.Y.Mid, box.Z.T0)
return rg.Plane(base_pt, cplane.XAxis, cplane.YAxis)
# ---------- Grouped output per target (one group per point), with inherited groups stripped ----------
def _next_group_name(prefix="Scatter"):
existing = set(rs.GroupNames() or [])
i = 0
while True:
name = "{}_{}".format(prefix, i)
if name not in existing:
return name
i += 1
def OrientGeometry_GroupPerPoint(guids, base_plane, target_planes, group_prefix="Scatter"):
"""
For each target plane:
- copy all 'guids' with PlaneToPlane transform
- remove any inherited group memberships from those copies (so originals' groups don't bind copies)
- create a unique group and add those copies into it
Returns flat list of all new object ids.
"""
if not guids or not target_planes or base_plane is None:
return []
all_ids = []
for _i, pl in enumerate(target_planes):
xform = rg.Transform.PlaneToPlane(base_plane, pl)
ids = rs.TransformObjects(guids, xform, copy=True)
if not ids:
continue
# Strip any inherited group memberships from the new copies
for oid in ids:
try:
rs.RemoveObjectFromAllGroups(oid)
except:
pass
# Now put just this set into its own group
grp = _next_group_name(group_prefix)
if not rs.IsGroup(grp):
rs.AddGroup(grp)
rs.AddObjectsToGroup(ids, grp)
all_ids.extend(ids)
return all_ids
# ---------------- Selection helpers ----------------
def pick_faces():
"""
Step 1: allow preselection and group selection for surfaces/polysurfaces/sub-faces.
We clear selection AFTER capturing to avoid carry-over into step 2.
"""
go = Rhino.Input.Custom.GetObject()
go.SetCommandPrompt("1) Select surface(s), sub-face(s), or polysurface(s) to populate")
go.GeometryFilter = Rhino.DocObjects.ObjectType.Surface | Rhino.DocObjects.ObjectType.Brep
go.SubObjectSelect = True
go.GroupSelect = True # accept grouped geometry
go.EnablePreSelect(True, True) # allow preselect and keep it
if go.GetMultiple(1,0) != Rhino.Input.GetResult.Object:
return []
objrefs = [go.Object(i) for i in range(go.ObjectCount)]
faces = []
for ob in objrefs:
f = ob.Face()
if f:
faces.append(f)
else:
b = ob.Brep()
if not b:
geo = ob.Geometry()
if isinstance(geo, rg.Surface):
b = geo.ToBrep()
if b:
for i in range(b.Faces.Count):
faces.append(b.Faces[i])
# Clear selection to prevent step 2 from reusing it
Rhino.RhinoDoc.ActiveDoc.Objects.UnselectAll()
return faces
def pick_geometry_to_orient():
"""
Step 2: accept grouped geometry (fresh selection to avoid reusing step-1 selection).
"""
go = Rhino.Input.Custom.GetObject()
go.SetCommandPrompt("2) Select geometry to orient (BasePlane from current CPlane bbox)")
go.GeometryFilter = Rhino.DocObjects.ObjectType.AnyObject
go.SubObjectSelect = False
go.GroupSelect = True # accept grouped geometry
go.AcceptNothing(False)
go.EnablePreSelect(False, True) # force a fresh pick for this step
go.DeselectAllBeforePostSelect = True # clear selection first
if go.GetMultiple(1,0) != Rhino.Input.GetResult.Object:
return []
ids = [go.Object(i).ObjectId for i in range(go.ObjectCount)]
Rhino.RhinoDoc.ActiveDoc.Objects.UnselectAll()
return ids
# ---------------- Preview run (uses seed for BOTH scatter and rotation) ----------------
def run_preview(faces, geom_to_orient, base_plane, N, max_deg, seed, use_cplane_z):
pts, _face_idx = scatter_points_on_faces(faces, N, seed=seed, boost=1.30, K=12)
pt_ids = [sc.doc.Objects.AddPoint(p) for p in pts]
planes = EvaluateSurfaceFromPoints(faces, pts)
planes = RandomRotatePlanes(planes, max_deg, seed)
planes = CplaneZ(use_cplane_z, planes)
# GROUPED: one group per plane/point, strip inherited groups from copies
oriented_ids = OrientGeometry_GroupPerPoint(geom_to_orient, base_plane, planes, group_prefix="Scatter")
return pt_ids, oriented_ids
# ---------------- Main UI loop ----------------
def main():
# 1) Surfaces to populate (preselect + group ok)
faces = pick_faces()
if not faces: return
# 2) Geometry to orient (group ok)
geom = pick_geometry_to_orient()
if not geom: return
base_plane = BoundingBoxBase(geom)
if base_plane is None: return
# Defaults
points = 20
max_angle = 360.0
seed = 1
use_cplane_z = False
# First preview run (automatic)
prev_pt_ids = []
prev_oriented_ids = []
def clear_prev():
if prev_oriented_ids:
try: rs.DeleteObjects(prev_oriented_ids)
except: pass
prev_oriented_ids[:] = []
if prev_pt_ids:
try: rs.DeleteObjects(prev_pt_ids)
except: pass
prev_pt_ids[:] = []
rs.EnableRedraw(False)
pt_ids, oriented_ids = run_preview(faces, geom, base_plane, points, max_angle, seed, use_cplane_z)
prev_pt_ids = pt_ids[:]
prev_oriented_ids = oriented_ids[:]
rs.EnableRedraw(True); sc.doc.Views.Redraw()
# Build option loop
go = Rhino.Input.Custom.GetOption()
go.SetCommandPrompt("3) Adjust scatter. Change options or choose KeepScatter to finish.")
opt_points = Rhino.Input.Custom.OptionInteger(points, 1, 10**7)
opt_angle = Rhino.Input.Custom.OptionDouble(max_angle, 0.0, 360.0)
opt_seed = Rhino.Input.Custom.OptionInteger(seed, 0, 10**9)
opt_cpz = Rhino.Input.Custom.OptionToggle(use_cplane_z, "False", "True")
idx_points = go.AddOptionInteger("Points", opt_points)
idx_keep = go.AddOption("KeepScatter")
idx_angle = go.AddOptionDouble("MaxAngle", opt_angle)
idx_seed = go.AddOptionInteger("Seed", opt_seed)
idx_cpz = go.AddOptionToggle("CplaneZ", opt_cpz)
while True:
res = go.Get()
if res == Rhino.Input.GetResult.Option:
idx = go.OptionIndex()
new_points = opt_points.CurrentValue
new_angle = float(opt_angle.CurrentValue)
new_seed = opt_seed.CurrentValue
new_cpz = opt_cpz.CurrentValue
# Exit ONLY if KeepScatter picked (and delete preview points before ending)
if idx == idx_keep:
rs.EnableRedraw(False)
if prev_pt_ids: rs.DeleteObjects(prev_pt_ids)
rs.EnableRedraw(True); sc.doc.Views.Redraw()
break
# Rerun preview if any option changed
changed = (
new_points != points or
abs(new_angle - max_angle) > 1e-9 or
new_seed != seed or
new_cpz != use_cplane_z
)
if changed:
points, max_angle, seed, use_cplane_z = new_points, new_angle, new_seed, new_cpz
rs.EnableRedraw(False)
clear_prev()
pt_ids, oriented_ids = run_preview(faces, geom, base_plane, points, max_angle, seed, use_cplane_z)
prev_pt_ids = pt_ids[:]
prev_oriented_ids = oriented_ids[:]
rs.EnableRedraw(True); sc.doc.Views.Redraw()
continue
elif res == Rhino.Input.GetResult.Nothing:
# Keep dialog open on Enter; do nothing
continue
elif res == Rhino.Input.GetResult.Cancel:
# Cancel: delete preview points before ending
rs.EnableRedraw(False)
if prev_pt_ids: rs.DeleteObjects(prev_pt_ids)
rs.EnableRedraw(True); sc.doc.Views.Redraw()
break
if __name__ == "__main__":
main()
This topic is related to:
and