Python command, have to click viewport to execute

I have a Python script that I have written (with some help from ChatGPT) to replace a certain set of block instances with another set of block instances. A bit like BlockReplace, but for multiple blocks at the same time based on a specific naming scheme.

It works great and does what we need now, but there is one strange thing. Right at the last step where it places all the block instances, I have to click the Rhino viewport once or it does nothing and just sits there. It’s like Rhino needs to have focus again so it can complete the step.

Is this a known issue? Is there a remedy for this?

Here is my script:

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


################################## VARIABLES ##################################

# Specify the path to your cell set files directory
cell_set_dir = 'J:\\Studio JHH Dropbox\\JHH SERVER\\1_PROJEKTE\\119_Origin Infinite\\01_O-I_BIBLIOTHEKEN\\CELL SETS'  # Replace with your actual directory path



################################## DEFINITIONS ##################################

def get_target_block_type():
    """Get the target block type."""
    options = ['OG', 'RG', 'CNC', 'DIA']
    target_block_type = rs.GetString("Select target block type",'CNC',options)
    return target_block_type


def identify_unique_blocks(selected_instances):
    """Identify unique blocks in the selected instances."""
    block_definitions = []
    for instance in selected_instances:
        block_name = rs.BlockInstanceName(instance)
        block_definitions.append(block_name)
    return list(set(block_definitions))

def form_target_block_names(block_definitions, target_block_type):
    """Form the names of the target blocks."""
    target_block_names = []
    for block_name in block_definitions:
        components = block_name.split("_")
        shortcode = components[0]  # Extract the shortcode from the block name
        number = components[-1]
        inv_status = "_inv" in block_name
        if inv_status:
            number = number.replace("inv", "")
            target_block_name = "{}_{}_inv_{}".format(shortcode, target_block_type, number)
        else:
            target_block_name = "{}_{}_{}".format(shortcode, target_block_type, number)
        target_block_names.append(target_block_name)
    return target_block_names, shortcode

def import_cell_set_file(cell_set_file):
    """Import the cell set file into the current document."""
    cmd = '_-Import "{}" _Enter'.format(cell_set_file)
    rs.Command(cmd)

def get_block_instances_and_transforms(block_name, selected_instances):
    """Get the selected instances and transforms of a block."""
    block_instances = [instance for instance in selected_instances if rs.BlockInstanceName(instance) == block_name]
    if not block_instances:
        return []
    
    transforms = []
    for instance in block_instances:
        rhobj = sc.doc.Objects.Find(instance)
        if type(rhobj.Geometry) is Rhino.Geometry.InstanceReferenceGeometry:
            iref = rhobj.Geometry
            xform = iref.Xform
            transforms.append(xform)
    
    return transforms

def replace_block_instances(old_block_name, new_block_name, selected_instances):
    """Replace instances of a block with instances of a new block."""
    if new_block_name not in rs.BlockNames():
        print("Block {} does not exist.".format(new_block_name))
        return
    transforms = get_block_instances_and_transforms(old_block_name, selected_instances)
    for instance, transform in zip(selected_instances, transforms):
        new_instance = rs.InsertBlock2(new_block_name, transform)
        old_instance_layer = rs.ObjectLayer(instance)
        rs.ObjectLayer(new_instance, old_instance_layer)
        
# Hide the originally selected instances
def handle_original_objects(selected_instances, option):
    """Handle the original objects based on the user's selection."""
    if option == "Delete":
        rs.DeleteObjects(selected_instances)
    elif option == "Hide":
        rs.HideObjects(selected_instances)
    # Leave does nothing, so we don't need an elif clause for that



################################## COMMAND ##################################

# Ask user to select instances
selected_instances = rs.GetObjects("Select instances to replace", rs.filter.instance)
if not selected_instances:
    print("No instances selected. Exiting.")
    exit()

# Get the target block type
target_block_type = get_target_block_type()
if not target_block_type:
    print("No target block type specified. Exiting.")
    exit()

# Identify unique blocks in the selected instances
block_definitions = identify_unique_blocks(selected_instances)
print("Found {} unique block definitions: {}".format(len(block_definitions), block_definitions))


# Form the names of the target blocks and extract shortcode
target_block_names, shortcode = form_target_block_names(block_definitions, target_block_type)
print("Formed {} target block names: {}".format(len(target_block_names), target_block_names))


# Check if target blocks exist in the document
missing_blocks = [block for block in target_block_names if block not in rs.BlockNames()]
cell_set_file = ''
if missing_blocks:
    print("The following blocks do not exist in the document: {}".format(missing_blocks))

    # Check if a file exists in the directory that matches the shortcode
    cell_set_file = os.path.join(cell_set_dir, shortcode + '.3dm')
    print(cell_set_file)
    if not os.path.isfile(cell_set_file):
        cell_set_file = rs.OpenFileName("Select the cell set file")

    if cell_set_file:
        import_cell_set_file(cell_set_file)
    else:
        print("No cell set file specified. Exiting.")
        exit()

# Before replacing the instances, prompt the user to decide how to handle the original objects
handle_option = rs.GetString("Keep original cells?", "Delete", ["Delete", "Hide", "Keep"])
if not handle_option:
    print("No action selected. Exiting.")
    exit()

# Replace the instances of the unique blocks with instances of the target blocks
transform_count = 0
for old_block_name, new_block_name in zip(block_definitions, target_block_names):
    old_transforms = get_block_instances_and_transforms(old_block_name, selected_instances)
    replace_block_instances(old_block_name, new_block_name, selected_instances)
    transform_count += len(old_transforms)

# Hide, delete or keep the original cell blocks
handle_original_objects(selected_instances, handle_option)

# Print success message
print("Successfully replaced {} instances".format(transform_count))

It get’s stuck basically at the third to last block where it replaces the blocks. No error messages are there and if I click the viewport it executes just fine.

Is there a way to give focus to Rhino again just before that step?

Thanks for any hints.

It has probably done what you want, but hasn’t redrawn the screen…

If you add a line at the end:
sc.doc.Views.Redraw()
does that work?

Thanks. No, I still have to click the viewport or maybe I didn’t put it in the right place.

If you want to try it out for yourself I’ll attach a reduced file. Just run the command select some or all of the instances in the pattern and then just keep pressing enter. At the end it will freeze for a few seconds and then you have to click the viewport and it will replace the blocks with the curves.

This is in Rhino 7 SR28

ChangeCellState.py (6.0 KB)
cell_replace.3dm (9.8 MB)

try replacing the end with this

################################## COMMAND ##################################

def doit():
    # Ask user to select instances
    selected_instances = rs.GetObjects("Select instances to replace", rs.filter.instance)
    if not selected_instances:
        print("No instances selected. Exiting.")
        return
    
    # Get the target block type
    target_block_type = get_target_block_type()
    if not target_block_type:
        print("No target block type specified. Exiting.")
        return
    
    # Identify unique blocks in the selected instances
    block_definitions = identify_unique_blocks(selected_instances)
    print("Found {} unique block definitions: {}".format(len(block_definitions), block_definitions))
    
    
    # Form the names of the target blocks and extract shortcode
    target_block_names, shortcode = form_target_block_names(block_definitions, target_block_type)
    print("Formed {} target block names: {}".format(len(target_block_names), target_block_names))
    
    
    # Check if target blocks exist in the document
    missing_blocks = [block for block in target_block_names if block not in rs.BlockNames()]
    cell_set_file = ''
    if missing_blocks:
        print("The following blocks do not exist in the document: {}".format(missing_blocks))
    
        # Check if a file exists in the directory that matches the shortcode
        cell_set_file = os.path.join(cell_set_dir, shortcode + '.3dm')
        print(cell_set_file)
        if not os.path.isfile(cell_set_file):
            cell_set_file = rs.OpenFileName("Select the cell set file")
    
        if cell_set_file:
            import_cell_set_file(cell_set_file)
        else:
            print("No cell set file specified. Exiting.")
            return
    
    # Before replacing the instances, prompt the user to decide how to handle the original objects
    handle_option = rs.GetString("Keep original cells?", "Delete", ["Delete", "Hide", "Keep"])
    if not handle_option:
        print("No action selected. Exiting.")
        return
        
    Rhino.Display.RhinoView.EnableDrawing = False
    
    # Replace the instances of the unique blocks with instances of the target blocks
    transform_count = 0
    for old_block_name, new_block_name in zip(block_definitions, target_block_names):
        old_transforms = get_block_instances_and_transforms(old_block_name, selected_instances)
        replace_block_instances(old_block_name, new_block_name, selected_instances)
        transform_count += len(old_transforms)
    
    # Hide, delete or keep the original cell blocks
    handle_original_objects(selected_instances, handle_option)
    
    # Print success message
    print("Successfully replaced {} instances".format(transform_count))

try:
    doit();
finally:
    Rhino.Display.RhinoView.EnableDrawing = True
    sc.doc.Views.Redraw()
1 Like

Boom! Yes, that works flawlessly. It so fast now and does the changes instantly.

Can you briefly explain what it does differently and why that is important for Rhino?

the code that does the work was put into the doit() function (exit() calls were replaced with returns), so that I could disable drawing before the work begins, and then ensure it would be enabled again by calling the function in a try/finally

all the ending code could have been put in try/finally instead, but I just do it this way because I think it is clearer

Thanks for the explanation!

Just for fun here is what ChatGPT says about your solution. I have one chat that I have “taught” quite a few things about scripting in Rhino, so I gave it your solution for next time :slight_smile:

That’s great to know! Thank you for sharing the solution from the forum. It looks like this approach disables drawing updates in the Rhino view while the replacement operations are taking place, and then re-enables them and forces a redraw at the end. This approach makes sense as it helps to avoid any partial updates or lags during the execution of the command.

The usage of try...finally is also a good pattern to ensure that even if an error happens in the doit() function, the drawing updates will be re-enabled and the view will be redrawn.

This is definitely a useful pattern to remember for future Rhino Python scripts. Thanks again for sharing this update! If you have any other questions or need further assistance, feel free to ask.