OctaneBlender scatter addon

Hi, I’m trying to make an addon that makes octane scattering a little easier, all it does is create a spawner object as I’ve found having the object being scattered on be the spawner is unreliable, appends a material to it, sets that up as the octane geo-node and so on.

I’m trying to do this with ChatGPT (I know that is probably why I am running into issues but without it I wouldn’t even be able to try this) because I do not know anything about coding, and I’m running into an issue, upon creating a spawner I get a bunch of errors saying:

“C:\Users\andre\OctaneBlender28.14\4.0\scripts\addons\octane_init_.py”, line 228, in view_update
File “C:\Users\andre\OctaneBlender28.14\4.0\scripts\addons\octane_init_.py”, line 40, in get_active_render_engine
RecursionError: maximum recursion depth exceeded while calling a Python object
Location: C:\Users\andre\OctaneBlender28.14\4.0\scripts\addons\octane\nodes\base_node_tree.py:892"

I am not sure why this is happening, and also, when I create a spawner for a scatter it slows blender down a lot, making it so that the asset library reloads thumbnails when I try to move an object, and the movement itself gets very choppy.

The script specifies a path to a blend file containing the scatter materials, I cannot attach the file as I am a new user, but it is a standard setup for an octane scatter, with the exception of naming the node which specifies the surface to scatter onto “ScatterSurface” and the nodes that specify objects to scattered “ScatterObject”
this is the script I am using:

import bpy
import os

# A list to keep track of created spawners
created_spawners = []
# Path to the asset file containing the materials
asset_file_path = "D:/assets/OctaneScatterAddon/ScatterAssets/Scatters.blend"

# Function to load materials from the specified asset file
def load_materials_from_asset_file(filepath):
    if not os.path.exists(filepath):
        return []

    # Open the blend file to list materials
    with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to):
        materials = data_from.materials
    
    return materials

# List of materials in the asset file
asset_materials = load_materials_from_asset_file(asset_file_path)

def get_or_create_collection(name, parent=None):
    if name in bpy.data.collections:
        return bpy.data.collections[name]
    else:
        new_collection = bpy.data.collections.new(name)
        if parent:
            parent.children.link(new_collection)
        else:
            bpy.context.scene.collection.children.link(new_collection)
        return new_collection

def append_or_link_material(material_name, asset_path):
    if material_name in bpy.data.materials:
        return bpy.data.materials[material_name]
    
    material_path = os.path.join(asset_path, "Material", material_name)
    with bpy.data.libraries.load(asset_path, link=True) as (data_from, data_to):
        if material_name in data_from.materials:
            data_to.materials = [material_name]
    
    return bpy.data.materials.get(material_name)

class OBJECT_OT_create_scatter_spawner(bpy.types.Operator):
    bl_idname = "object.create_scatter_spawner"
    bl_label = "Scatter on active"
    bl_description = "Creates a new Scatter Spawner cube and assigns it to the active object"
    
    material: bpy.props.EnumProperty(
        name="Material",
        description="Select a material for the Scatter Spawner",
        items=[(mat, mat, "") for mat in asset_materials]
    )
    
    def execute(self, context):
        global created_spawners
        
        # Ensure an active object is selected
        if not context.object:
            self.report({'ERROR'}, "No active object selected")
            return {'CANCELLED'}
        
        active_object = context.object
        active_object_name = active_object.name
        spawner_name = f"{active_object_name}_Spawner"

        # Check for existing spawners with the same name and add suffix if necessary
        spawner_index = 1
        while spawner_name in bpy.data.objects:
            spawner_name = f"{active_object_name}_Spawner_{spawner_index:03d}"
            spawner_index += 1
        
        # Store the current mode and switch to object mode
        current_mode = bpy.context.mode
        if current_mode != 'OBJECT':
            bpy.ops.object.mode_set(mode='OBJECT')
        
        # Create a new cube
        bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, -10))
        
        # Get the created cube object
        cube = bpy.context.object
        cube.name = spawner_name
        
        # Append or link the selected material from the asset file
        scatter_material = append_or_link_material(self.material, asset_file_path)
        
        # Make a single-user copy of the material and assign it to the cube
        if scatter_material:
            single_user_material = scatter_material.copy()
            single_user_material.name = f"{active_object_name}_Spawner"
            if cube.data.materials:
                cube.data.materials[0] = single_user_material
            else:
                cube.data.materials.append(single_user_material)
        
            # Set the material name to the specified property
            cube.data.octane.octane_geo_node_collections.node_graph_tree = single_user_material.name
            
            # Assign the active object as the ScatterSurface
            scatter_node = single_user_material.node_tree.nodes.get("ScatterSurface")
            if scatter_node:
                scatter_node.object_ptr = active_object  # Assign active object directly
        
        # Set the specified string to the second property
        cube.data.octane.octane_geo_node_collections.osl_geo_node = "Scatter on surface"
        
        # Add the spawner to the list
        created_spawners.append(cube.name)

        # Organize the spawners into collections
        scatter_collection = get_or_create_collection("Scatter")
        spawner_collection = get_or_create_collection("ScatterSpawners", scatter_collection)
        if cube.name not in spawner_collection.objects:
            spawner_collection.objects.link(cube)
        
        # Ensure the object is not linked to any other collection
        for collection in bpy.data.collections:
            if collection.name not in {"Scatter", "ScatterSpawners"} and cube.name in collection.objects:
                collection.objects.unlink(cube)

        # Ensure the 3D viewport is in object mode
        if bpy.context.mode != 'OBJECT':
            bpy.ops.object.mode_set(mode='OBJECT')

        # Set the origin to world origin
        bpy.context.view_layer.objects.active = cube
        bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS')
        cube.location = (0, 0, 0)
        
        # Restore the previous mode if it's a valid mode
        try:
            if current_mode != 'OBJECT':
                bpy.ops.object.mode_set(mode=current_mode)
        except Exception as e:
            self.report({'WARNING'}, f"Failed to switch back to mode {current_mode}: {e}")
        
        self.report({'INFO'}, f"{spawner_name} created")
        return {'FINISHED'}

class OBJECT_OT_delete_scatter_spawner(bpy.types.Operator):
    bl_idname = "object.delete_scatter_spawner"
    bl_label = "Delete Scatter Spawner"
    bl_description = "Deletes the specified Scatter Spawner object"
    
    name: bpy.props.StringProperty()
    
    def execute(self, context):
        obj = bpy.data.objects.get(self.name)
        if obj:
            # Unlink from all collections
            for collection in obj.users_collection:
                collection.objects.unlink(obj)
            # Remove the object
            bpy.data.objects.remove(obj, do_unlink=True)
            
            if self.name in created_spawners:
                created_spawners.remove(self.name)
            
            self.report({'INFO'}, f"{self.name} deleted")
        else:
            self.report({'ERROR'}, f"{self.name} not found")
        return {'FINISHED'}

class OBJECT_PT_scatter_spawner_panel(bpy.types.Panel):
    bl_label = "Scatter Spawner Panel"
    bl_idname = "OBJECT_PT_scatter_spawner_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Scatter Spawner"

    def draw(self, context):
        layout = self.layout
        
        # Add button to create a new scatter spawner with material selection
        layout.operator_menu_enum("object.create_scatter_spawner", "material", text="Scatter on active")
        
        # Display the list of created spawners
        layout.label(text="Created Spawners:")
        
        scatter_spawners_collection = bpy.data.collections.get("ScatterSpawners")
        if scatter_spawners_collection:
            self.draw_collection(layout, scatter_spawners_collection)
    
    def draw_collection(self, layout, collection):
        row = layout.row()
        row.label(text=collection.name, icon='OUTLINER_COLLECTION')
        for obj in collection.objects:
            self.draw_object(layout, obj)

    def draw_object(self, layout, obj):
        row = layout.row()
        row.separator(factor=1.5)  # Indentation for hierarchy
        sub = row.row(align=True)
        sub.scale_x = 0.8  # Scale for a darker background
        sub.label(text=obj.name, icon='OBJECT_DATA')
        
        # Create a sub-layout for buttons with some spacing
        button_row = sub.row(align=True)
        button_row.scale_x = 1.2  # Adjust scale for spacing
        button_row.operator("object.select_scatter_spawner", text="", icon='RESTRICT_SELECT_OFF').name = obj.name
        button_row.operator("object.delete_scatter_spawner", text="", icon='X').name = obj.name

class OBJECT_OT_select_scatter_spawner(bpy.types.Operator):
    bl_idname = "object.select_scatter_spawner"
    bl_label = "Select Scatter Spawner"
    bl_description = "Selects the specified Scatter Spawner object"
    
    name: bpy.props.StringProperty()
    
    def execute(self, context):
        # Deselect all objects first
        bpy.ops.object.select_all(action='DESELECT')
        
        obj = bpy.data.objects.get(self.name)
        if obj:
            bpy.context.view_layer.objects.active = obj
            obj.select_set(True)
            self.report({'INFO'}, f"{self.name} selected")
        else:
            self.report({'ERROR'}, f"{self.name} not found")
        return {'FINISHED'}

# Register the classes
def register():
    bpy.utils.register_class(OBJECT_OT_create_scatter_spawner)
    bpy.utils.register_class(OBJECT_PT_scatter_spawner_panel)
    bpy.utils.register_class(OBJECT_OT_select_scatter_spawner)
    bpy.utils.register_class(OBJECT_OT_delete_scatter_spawner)

# Unregister the classes
def unregister():
    bpy.utils.unregister_class(OBJECT_OT_create_scatter_spawner)
    bpy.utils.unregister_class(OBJECT_PT_scatter_spawner_panel)
    bpy.utils.unregister_class(OBJECT_OT_select_scatter_spawner)
    bpy.utils.unregister_class(OBJECT_OT_delete_scatter_spawner)

# Run the register function
if __name__ == "__main__":
    register()