Twist mesh with a Python script

Hello, The code below will import a mesh and create a line through the mesh. I now want to twist the mesh using the line as the twist axis. But all of my attempts have failed. If anyone could give me some guidance on how to do this I would be grateful!

import os
import rhinoscriptsyntax as rs
import random

def import_random_mesh_and_process():
    print("Step 1: Starting script.")

    # Path to your folder containing OBJ files
    folder_path = r"C:\Users\jdm037\Desktop\rhino_python_scripts\meshes_simple"

    # Delete all visible geometry in the scene
    print("Step 2: Deleting all visible geometry.")
    visible_objects = rs.ObjectsByType(0, True, True)  # 0 = all objects, include_hidden=False, include_lights=True
    if visible_objects:
        rs.DeleteObjects(visible_objects)
        print("Step 2: All visible geometry deleted.")
    else:
        print("Step 2: No visible geometry to delete.")

    # Get a list of all .OBJ files in the folder
    print("Step 3: Gathering .OBJ files.")
    mesh_files = [f for f in os.listdir(folder_path) if f.endswith('.obj')]

    if not mesh_files:
        print("Step 3: No OBJ files found in the specified folder.")
        return
    print("Step 3: OBJ files found.")

    # Select a random OBJ file
    print("Step 4: Selecting a random OBJ file.")
    random_mesh_file = random.choice(mesh_files)
    mesh_file_path = os.path.join(folder_path, random_mesh_file)
    print("Step 4: Selected file - {}".format(random_mesh_file))

    # Import the selected OBJ file into Rhino
    print("Step 5: Importing the selected OBJ file.")
    try:
        rs.Command('-Import "{}" _Enter'.format(mesh_file_path))
        print("Step 5: Successfully imported - {}".format(random_mesh_file))
    except Exception as e:
        print("Step 5: Failed to import mesh: {}".format(e))
        return

    # Find the longest dimension of the imported mesh and create a line
    print("Step 6: Processing the imported mesh.")
    try:
        # Get the imported mesh
        imported_mesh = rs.LastCreatedObjects(select=False)
        if not imported_mesh:
            print("Step 6: No mesh was imported.")
            return

        print("Step 6: Imported mesh detected.")
        imported_mesh_id = imported_mesh[0]
        bounding_box = rs.BoundingBox(imported_mesh_id)
        if not bounding_box:
            print("Step 6: Failed to calculate bounding box for the imported mesh.")
            return

        print("Step 6: Bounding box calculated.")
        # Calculate the center of the bounding box
        center_x = (bounding_box[0][0] + bounding_box[6][0]) / 2
        center_y = (bounding_box[0][1] + bounding_box[6][1]) / 2
        center_z = (bounding_box[0][2] + bounding_box[6][2]) / 2
        center = (center_x, center_y, center_z)

        # Calculate dimensions along X, Y, Z
        x_length = abs(bounding_box[1][0] - bounding_box[0][0])
        y_length = abs(bounding_box[3][1] - bounding_box[0][1])
        z_length = abs(bounding_box[4][2] - bounding_box[0][2])

        print("Step 6: Dimensions calculated.")
        # Determine the longest axis
        longest_length = max(x_length, y_length, z_length)
        axis = ["X", "Y", "Z"][ [x_length, y_length, z_length].index(longest_length) ]

        # Create a line along the longest axis, centered on the mesh
        if axis == "X":
            line_start = (center[0] - longest_length, center[1], center[2])
            line_end = (center[0] + longest_length, center[1], center[2])
        elif axis == "Y":
            line_start = (center[0], center[1] - longest_length, center[2])
            line_end = (center[0], center[1] + longest_length, center[2])
        else:  # Z
            line_start = (center[0], center[1], center[2] - longest_length)
            line_end = (center[0], center[1], center[2] + longest_length)

        rs.AddLine(line_start, line_end)
        print("Step 7: Created a line along the {}-axis through the center.".format(axis))
    except Exception as e:
        print("Step 7: Failed to process mesh: {}".format(e))

# Run the function
import_random_mesh_and_process()

I don’t see anything resembling twist in the rhinoscriptsyntax so if anyone else finds one, it may be a nifty shortcut. Until then you can probably do something like this:

# assume you already have the variables defined as "your_...."
import Rhino.Geometry as rhg
morph = rhg.Morphs.TwistSpaceMorph()
morph.TwistAngleRadians = your_angle
morph.TwistAxis = your_line
morph.Morph(your_mesh)

Thanks, Will! Since posting this question I found a solution, but it’s a bit complicated. I will test your approach out to see how it compares. I’ll post my full code below in case it’s interesting to anyone.

import os
import rhinoscriptsyntax as rs
import random
import Rhino
import scriptcontext as sc

def import_random_mesh_and_process():
    print("Step 1: Starting script.")

    folder_path = r"C:\Users\jdm037\Desktop\rhino_python_scripts\meshes_simple"

    print("Step 2: Deleting all visible geometry.")
    visible_objects = rs.ObjectsByType(0, True, True)
    if visible_objects:
        rs.DeleteObjects(visible_objects)
        print("Step 2: All visible geometry deleted.")
    else:
        print("Step 2: No visible geometry to delete.")

    print("Step 3: Gathering .OBJ files.")
    mesh_files = [f for f in os.listdir(folder_path) if f.endswith('.obj')]
    if not mesh_files:
        print("Step 3: No OBJ files found in the specified folder.")
        return

    print("Step 4: Selecting a random OBJ file.")
    random_mesh_file = random.choice(mesh_files)
    mesh_file_path = os.path.join(folder_path, random_mesh_file)
    print("Step 4: Selected file - {}".format(random_mesh_file))

    print("Step 5: Importing the selected OBJ file.")
    try:
        rs.Command('-Import "{}" _Enter'.format(mesh_file_path))
        print("Step 5: Successfully imported - {}".format(random_mesh_file))
    except Exception as e:
        print("Step 5: Failed to import mesh: {}".format(e))
        return

    print("Step 6: Processing the imported mesh.")
    try:
        imported_mesh = rs.LastCreatedObjects(select=False)
        if not imported_mesh:
            print("Step 6: No mesh was imported.")
            return

        imported_mesh_id = imported_mesh[0]
        bounding_box = rs.BoundingBox(imported_mesh_id)
        if not bounding_box:
            print("Step 6: Failed to calculate bounding box for the imported mesh.")
            return

        center_x = (bounding_box[0][0] + bounding_box[6][0]) / 2
        center_y = (bounding_box[0][1] + bounding_box[6][1]) / 2
        center_z = (bounding_box[0][2] + bounding_box[6][2]) / 2

        x_length = abs(bounding_box[1][0] - bounding_box[0][0])
        y_length = abs(bounding_box[3][1] - bounding_box[0][1])
        z_length = abs(bounding_box[4][2] - bounding_box[0][2])

        longest_length = max(x_length, y_length, z_length)
        axis = ["X", "Y", "Z"][ [x_length, y_length, z_length].index(longest_length) ]

        if axis == "X":
            line_start = (center_x - longest_length, center_y, center_z)
            line_end = (center_x + longest_length, center_y, center_z)
        elif axis == "Y":
            line_start = (center_x, center_y - longest_length, center_z)
            line_end = (center_x, center_y + longest_length, center_z)
        else:
            line_start = (center_x, center_y, center_z - longest_length)
            line_end = (center_x, center_y, center_z + longest_length)

        print("Step 6: Twist axis from {} to {}".format(line_start, line_end))
        rs.AddLine(line_start, line_end)

        print("Step 8: Applying twist using RhinoCommon.")

        mesh = rs.coercegeometry(imported_mesh_id)
        if not mesh:
            print("RhinoCommon fallback: Failed to coerce geometry.")
            return
        
        # Create twisting effect
        twist_angle = 360
        line = Rhino.Geometry.Line(
            Rhino.Geometry.Point3d(*line_start),
            Rhino.Geometry.Point3d(*line_end)
        )
        twist_center = line.PointAt(0.5)
        
        def twist_deform(pt):
            """
            Custom deformation function for twisting.
            """
            t = line.ClosestParameter(pt)
            angle = twist_angle * t
            axis = line.UnitTangent
            rotation = Rhino.Geometry.Transform.Rotation(
                Rhino.RhinoMath.ToRadians(angle),
                axis,
                twist_center
            )
            pt.Transform(rotation)
            return pt

        twisted_mesh = mesh.Duplicate()
        for i in range(twisted_mesh.Vertices.Count):
            vertex = twisted_mesh.Vertices[i]
            pt = twist_deform(Rhino.Geometry.Point3d(vertex.X, vertex.Y, vertex.Z))
            twisted_mesh.Vertices.SetVertex(i, pt.X, pt.Y, pt.Z)

        sc.doc.Objects.Replace(imported_mesh_id, twisted_mesh)
        sc.doc.Views.Redraw()
        print("Twist successfully applied using RhinoCommon.")

    except Exception as e:
        print("Step 8: Failed to process mesh: {}".format(e))

# Run the function
import_random_mesh_and_process()

Will,

By the way, every time I run this code the multipipe radius is the same, even though I’ve tried to define the radius randomly. My impression is it’s this line that’s failing:
rs.Command(‘_-MultiPipe _Radius={} _Enter _Enter’.format(pipe_radius))

Do you have any suggestions?

Thanks,
Joe

import rhinoscriptsyntax as rs
import random
import scriptcontext as sc
import Rhino

def import_random_mesh_and_create_multipipe():
    print("Starting the MultiPipe script...")

    # Path to your folder containing OBJ files
    folder_path = r"C:\Users\jdm037\Desktop\rhino_python_scripts\meshes_simple"
    pipe_diameter_percentage = 0.001  # Set the pipe diameter as a percentage of the largest bounding box dimension
    print("Pipe diameter percentage: {}".format(pipe_diameter_percentage))

    # Get a list of all .OBJ files in the folder
    mesh_files = [f for f in os.listdir(folder_path) if f.endswith('.obj')]
    print("Found {} OBJ files: {}".format(len(mesh_files), mesh_files))

    if not mesh_files:
        print("No OBJ files found in the specified folder. Exiting script.")
        return

    # Select a random OBJ file
    random_mesh_file = random.choice(mesh_files)
    mesh_file_path = os.path.join(folder_path, random_mesh_file)
    print("Randomly selected mesh file: {}".format(random_mesh_file))

    # Import the selected OBJ file into Rhino
    try:
        print("Attempting to import the OBJ file...")
        rs.Command('-Import "{}" _Enter'.format(mesh_file_path))
        print("Successfully imported random mesh: {}".format(random_mesh_file))
    except Exception as e:
        print("Failed to import mesh: {}".format(e))
        return

    # Get the most recently added object (the imported mesh)
    imported_objects = rs.LastCreatedObjects()
    print("Imported object IDs: {}".format(imported_objects))

    if not imported_objects:
        print("No objects were imported. Exiting script.")
        return

    # Check if the object is a mesh
    mesh_id = imported_objects[0]  # Assuming the first imported object is the mesh
    print("Processing object ID: {}".format(mesh_id))

    if rs.IsMesh(mesh_id):
        print("The imported object is confirmed to be a mesh.")

        # Calculate the bounding box of the mesh
        bbox = rs.BoundingBox(mesh_id)
        if not bbox:
            print("Failed to calculate bounding box. Exiting script.")
            return
        print("Bounding box of the mesh: {}".format(bbox))

        # Calculate the maximum dimension of the mesh
        max_dimension = max(
            rs.Distance(bbox[0], bbox[1]),
            rs.Distance(bbox[0], bbox[3]),
            rs.Distance(bbox[0], bbox[4])
        )
        print("Mesh maximum dimension: {}".format(max_dimension))

        # Correctly calculate the pipe radius
        pipe_radius = pipe_diameter_percentage * max_dimension
        print("Calculated pipe radius: {:.3f}".format(pipe_radius))

        # Extract the wireframe (mesh edges as polylines)
        print("Extracting wireframe from the mesh...")
        mesh_obj = sc.doc.Objects.Find(mesh_id)
        if not mesh_obj:
            print("Failed to retrieve the mesh object. Exiting script.")
            return

        mesh_geometry = mesh_obj.Geometry
        if not isinstance(mesh_geometry, Rhino.Geometry.Mesh):
            print("The object is not a valid mesh. Exiting script.")
            return

        edges = mesh_geometry.TopologyEdges
        print("Mesh has {} topology edges.".format(edges.Count))

        edge_curves = []
        for i in range(edges.Count):
            curve = edges.EdgeLine(i).ToNurbsCurve()
            if curve:
                edge_curves.append(curve)
        print("Extracted {} edge curves.".format(len(edge_curves)))

        # Add curves to Rhino document and store them
        print("Adding curves to the Rhino document...")
        curve_ids = []
        for curve in edge_curves:
            curve_id = sc.doc.Objects.AddCurve(curve)
            curve_ids.append(curve_id)
        print("Added {} curves to the document.".format(len(curve_ids)))

        sc.doc.Views.Redraw()

        # Ensure the curves are selected
        rs.UnselectAllObjects()
        rs.SelectObjects(curve_ids)
        print("Selected {} curves for MultiPipe.".format(len(curve_ids)))

        # Explicitly set the radius interactively
        print("Executing _MultiPipe command interactively...")
        rs.Command('_-MultiPipe _Radius={} _Enter _Enter'.format(pipe_radius))

        print("MultiPipe created successfully. Check your Rhino workspace.")
    else:
        print("The imported object is not a mesh. Exiting script.")

# Run the function
import_random_mesh_and_create_multipipe()
type or paste code here