Scripting Cage_Edit function

Hello everyone,

I’m currently working on a problem related to automated surface deformation in Rhino.

Initially, I developed a Grasshopper model to achieve this, but I wasn’t completely satisfied with the result since it was essentially based on the surface control points that I wanted to modify.

To improve this deformation model, I’d like to use Rhino’s _CageEdit function in a Python script running in Rhino. The problem I’m currently having is that once the geometry has been deformed by the _CageEdit function, I can’t get it back into the Rhino file, to export it for example. I seem to have to commit it after deforming it. The deformed geometry is visible as a gauge in Rhino’s graphical interface (see Photo), but it disappears as soon as I press ESC or anything else.

So I’d like to know how to “commit” or “Bake” the surface I’ve just modified?

You’ll see my current script down below.

Thanks in advance for your feedback,

Best regards,

Etienne

# -*- coding: utf-8 -*-
import rhinoscriptsyntax as rs
import Rhino
from Rhino import Geometry as rg
import scriptcontext as sc
import math as m

# There is only one geometry in my .3dm file
objects = rs.AllObjects()
if not objects:
    print("No object found")
surface_guid= objects[0] # Pick the one and only geometry

# Select the surface
rs.SelectObject(bateau_guid)
# CageEdit set up
cmd = '_CageEdit \n _B \n G \n _Enter \n _G \n'
rs.Command(cmd, echo=True)
cage_ids=rs.LastCreatedObjects()
cage_id=cage_ids[0]

rh_obj = rs.coercerhinoobject(cage_id)
if not rh_obj:
    print("Impossible to convert the cage in rhicommon object")
    exit()
# Step 5 : Enable grips
rh_obj.GripsOn = True

# Select grips
grips = rh_obj.GetGrips()
# Move grips (example : vec(200,100,0))
for grip in grips:
    grip.Move(Rhino.Geometry.Vector3d(200, 100, 0))
1 Like

referring to this example:

you have to add a last line to update the Grip stuff…
similar concept as commitChanges elsewhere…
… getting some strange results / visual artefacts without this GripUpdate - thought RhinoCommon is more robust…

# Select grips
grips = rh_obj.GetGrips()
# Move grips (example : vec(200,100,0))
for grip in grips:
    grip.Move(Rhino.Geometry.Vector3d(200, 100, 0))
# ***********
sc.doc.Objects.GripUpdate(rh_obj,True) # add this to update the object
# ***********

kind regards -tom

Thank you very much, it solved my issue !

I come back to you after wanting to automate the macro execution completely (Python script that launches Rhino and then executes the macro without going through the GUI). It seems that RhinoCommon doesn’t support the _CageEdit function?
Is it the case and are there any _CageEdit equivalents compatible with RhinoCommon?
Thank you in advance for your help !
Etienne

I am afraid that rhinocommon still does not support cageedit / Morph

the only why I know is to use commandline-scripting as you did above.

but search this forum and check the api…

… there is a “spacemorph” class - but not sure if all you need is exposed to rh common.
https://developer.rhino3d.com/api/rhinocommon/rhino.geometry.spacemorph

if you find a sample or get some code working - would be nice to post / share the code.

kind regards -tom

Hi Tom,
As I’m far from experienced with Rhino and Python, I’ve essentially relied on Chat GPT to generate something that partially works.
Down below you’ll find a script which, from an input geometry, generates an output mesh. Initially, I applied an extremely simplistic deformation (z translation of all points), but in the future I’d like to be able to play with each point in more detail.
The problem I’m currently encountering is that I can’t generate a closed mesh from this script. In my case, I’m working with complex shapes (boat hulls) which need to be closed for CFD simulations. This is where Cage Edit is extremely powerful.
If you can think of a way to improve this code so that it generates a closed mesh, I’d love to hear from you!
Otherwise I’m going to use scriptsyntax with an auto-click to cobble together an automation that’s not very robust at first.
Thanks in advance for your help.
Etienne

import rhinoinside
rhinoinside.load()
import Rhino
from Rhino.Geometry import *
import System
import numpy as np
import os

class CageMorph:
    def __init__(self, source_pts, target_pts, nx, ny, nz):
        self.source_pts = np.array(source_pts).reshape((nx, ny, nz))
        self.target_pts = np.array(target_pts).reshape((nx, ny, nz))
        self.nx, self.ny, self.nz = nx, ny, nz

    def morph_point(self, pt):
        u = self._get_normalized_position(pt)
        if u is None:
            return pt  # Hors cage

        i = int(u[0] * (self.nx - 1))
        j = int(u[1] * (self.ny - 1))
        k = int(u[2] * (self.nz - 1))

        i = min(i, self.nx - 2)
        j = min(j, self.ny - 2)
        k = min(k, self.nz - 2)

        dx = u[0] * (self.nx - 1) - i
        dy = u[1] * (self.ny - 1) - j
        dz = u[2] * (self.nz - 1) - k

        def get(p, q, r): return self.target_pts[i+p, j+q, k+r]
        def lerp(a, b, t): return a + t * (b - a)

        c000 = get(0, 0, 0)
        c100 = get(1, 0, 0)
        c010 = get(0, 1, 0)
        c110 = get(1, 1, 0)
        c001 = get(0, 0, 1)
        c101 = get(1, 0, 1)
        c011 = get(0, 1, 1)
        c111 = get(1, 1, 1)

        x00 = lerp(c000, c100, dx)
        x10 = lerp(c010, c110, dx)
        x01 = lerp(c001, c101, dx)
        x11 = lerp(c011, c111, dx)

        y0 = lerp(x00, x10, dy)
        y1 = lerp(x01, x11, dy)

        final = lerp(y0, y1, dz)
        return final

    def morph(self, geom):
        """Apply morph on any geometry"""
        class SimpleMorph(Rhino.Geometry.SpaceMorph):
            def __init__(self, morph_fn):
                super(SimpleMorph, self).__init__()
                self._morph_fn = morph_fn

            def MorphPoint(self, pt):
                return self._morph_fn(pt)

        smorph = SimpleMorph(self.morph_point)
        geom_copy = geom.Duplicate()
        smorph.Morph(geom_copy)
        return geom_copy

    def _get_normalized_position(self, pt):
        bb_min = self.source_pts[0, 0, 0]
        bb_max = self.source_pts[-1, -1, -1]

        if not BoundingBox(bb_min, bb_max).Contains(pt):
            return None

        u = (pt.X - bb_min.X) / (bb_max.X - bb_min.X)
        v = (pt.Y - bb_min.Y) / (bb_max.Y - bb_min.Y)
        w = (pt.Z - bb_min.Z) / (bb_max.Z - bb_min.Z)

        return (u, v, w)

def open_single_brep(filepath):
    model = Rhino.FileIO.File3dm.Read(filepath)
    for obj in model.Objects:
        geo = obj.Geometry
        if isinstance(geo, Brep):
            return geo
    raise ValueError("No polysurface found.")

def generate_regular_grid(bbox, nx, ny, nz):
    """Create an evenly distributed grid in the bounding box"""
    pts = []
    for i in range(nx):
        x = bbox.Min.X + i * (bbox.Max.X - bbox.Min.X) / (nx - 1)
        for j in range(ny):
            y = bbox.Min.Y + j * (bbox.Max.Y - bbox.Min.Y) / (ny - 1)
            for k in range(nz):
                z = bbox.Min.Z + k * (bbox.Max.Z - bbox.Min.Z) / (nz - 1)
                pts.append(Point3d(x, y, z))
    return pts

def morph_geometry_brep(brep, cage_morph):
    brep_copy = brep.Duplicate()
    all_meshes = []
    for i in range(brep_copy.Faces.Count):
        face = brep_copy.Faces[i]
        mesh = Rhino.Geometry.Mesh.CreateFromBrep(face.DuplicateFace(False))[0]
        for j in range(mesh.Vertices.Count):
            pt = mesh.Vertices[j]
            pt3d = Point3d(pt.X, pt.Y, pt.Z)
            new_pt = cage_morph.morph_point(pt3d)
            mesh.Vertices.SetVertex(j, new_pt)
        mesh.Normals.ComputeNormals()
        mesh.Compact()
        all_meshes.append(mesh)
    return all_meshes

def simple_deform(pt):
    """Deformation function : translation through z"""
    dz = 100
    return Point3d(pt.X, pt.Y, pt.Z + dz)

def apply_cage_morph(brep, nx, ny, nz):
    bbox = brep.GetBoundingBox(True)
    source_grid = generate_regular_grid(bbox, nx, ny, nz)
    target_grid = [simple_deform(pt) for pt in source_grid]

    morph = CageMorph(source_grid, target_grid, nx, ny, nz)
    brep_def = morph_geometry_brep(brep, morph)
    return brep_def

def save_breps(brep_original, brep_modifie, filepath_out):
    model = Rhino.FileIO.File3dm()
    model.Objects.AddBrep(brep_original)
    for mesh in mesh_list:
        model.Objects.AddMesh(mesh)
    model.Write(filepath_out, 6)
    print(f"✅ Model saved : {filepath_out}")



# === Exemple d'utilisation ===
if __name__ == "__main__":
    try:
        input_file = r"To be modified"
        output_file = r"To be modified"

        brep = open_single_brep(input_file)
        mesh_list = apply_cage_morph(brep, nx=4, ny=4, nz=4)
        save_breps(brep, mesh_list, output_file)

    except Exception as e:
        print("❌ Error :", e)