diff --git a/import_3dm/__init__.py b/import_3dm/__init__.py index e37adba..d9b4c91 100644 --- a/import_3dm/__init__.py +++ b/import_3dm/__init__.py @@ -113,6 +113,18 @@ class Import3dm(Operator, ImportHelper): default=10, min=1, ) + + create_instance_files: BoolProperty( + name="Auto-Create Linked Block Files", + description="Atomatically convert each .3dm block file into a .blend file. (SAVE BEFORE ENABLING THIS)", + default=False, + ) + + overwrite: BoolProperty( + name="Overwrite", + description="Overwrite existing .blend files that contain block definitions.", + default=False, + ) update_materials: BoolProperty( name="Update Materials", @@ -121,6 +133,12 @@ class Import3dm(Operator, ImportHelper): ) def execute(self, context): + # Check if the file is unsaved and create_instance_files is True + # Abort operation + if bpy.data.filepath == "" and self.create_instance_files: + self.report({'ERROR_INVALID_INPUT'}, "Save your file before trying to create any linked block files.") + return {'CANCELLED'} + options = { "filepath":self.filepath, "import_views":self.import_views, @@ -133,8 +151,10 @@ def execute(self, context): "import_instances":self.import_instances, "import_instances_grid_layout":self.import_instances_grid_layout, "import_instances_grid":self.import_instances_grid, + "create_instance_files":self.create_instance_files, + "overwrite":self.overwrite, } - return read_3dm(context, options) + return read_3dm(context, options, block_toggle = True) def draw(self, context): layout = self.layout @@ -163,6 +183,17 @@ def draw(self, context): box.prop(self, "import_instances") box.prop(self, "import_instances_grid_layout") box.prop(self, "import_instances_grid") + + nested_box = box.box() + nested_box.label(text="Linked Blocks (Save before using!)") + nested_box.prop(self, "create_instance_files") + nested_box.prop(self, "overwrite") + + # # Check if the file is unsaved + # if bpy.data.is_dirty: + # box = layout.box() + # box.label(text="Unsaved File") + # box.operator("wm.save_mainfile", text="Save") box = layout.box() box.label(text="Materials") diff --git a/import_3dm/converters/instances.py b/import_3dm/converters/instances.py index 23d1331..6421910 100644 --- a/import_3dm/converters/instances.py +++ b/import_3dm/converters/instances.py @@ -22,6 +22,7 @@ import bpy import rhino3dm as r3d +import os from mathutils import Matrix from mathutils import Vector from math import sqrt @@ -34,7 +35,7 @@ #proper exception handling -def handle_instance_definitions(context, model, toplayer, layername): +def handle_instance_definitions(context, model, toplayer, layername, filepath3dm): """ import instance definitions from rhino model as empty collections """ @@ -44,15 +45,50 @@ def handle_instance_definitions(context, model, toplayer, layername): instance_col.hide_render = True instance_col.hide_viewport = True toplayer.children.link(instance_col) - + + instance_properties = [] + for idef in model.InstanceDefinitions: - idef_col=utils.get_iddata(context.blend_data.collections,idef.Id, idef.Name, None ) + idef_col = utils.get_iddata(context.blend_data.collections, idef.Id, idef.Name, None) try: instance_col.children.link(idef_col) except Exception: pass + name = idef.Name + + if os.path.isfile(idef.SourceArchive): + source_archive = idef.SourceArchive + # Look for file in sub-directories + elif str(idef.UpdateType) != "InstanceDefinitionUpdateType.Static": + name3dm = os.path.basename(idef.SourceArchive) + dirname = os.path.dirname(filepath3dm) + match = False + for root, dirs, files in os.walk(dirname): + for file in files: + if ".3dm" in file: + if name3dm.lower() == file.lower(): + source_archive = os.path.join(root, file) + print("Changed source archive from " + idef.SourceArchive + " to " + source_archive) + match = True + break + if match: + break + if not match: + source_archive = idef.SourceArchive + print("File \"" + name3dm + "\" could not be found!") + else: + source_archive = "" + + update_type = idef.UpdateType + + # Save relevant block data for handling of linked block files + instance_dict = {"Name":name, "SourceArchive":source_archive, "UpdateType":update_type} + instance_properties.append(instance_dict) + + return instance_properties + def import_instance_reference(context, ob, iref, name, scale, options): #TODO: insert reduced mesh proxy and hide actual instance in viewport for better performance on large files iref.empty_display_size=0.5 diff --git a/import_3dm/read3dm.py b/import_3dm/read3dm.py index 0ed6fc6..c6016f0 100644 --- a/import_3dm/read3dm.py +++ b/import_3dm/read3dm.py @@ -114,22 +114,45 @@ def install_dependencies(): try: import rhino3dm as r3d except: - print("Failed to load rhino3dm, trying to install automatically...") - try: - install_dependencies() - # let user restart Blender, reloading of rhino3dm after automated - # install doesn't always work, better to just fail clearly before - # that - raise Exception("Please restart Blender.") - except: - raise + if not sys.platform in ('darwin', 'win32'): + print("Failed to load rhino3dm, trying to install automatically...") + try: + install_dependencies() + # let user restart Blender, reloading of rhino3dm after automated + # install doesn't always work, better to just fail clearly before + # that + raise Exception("Please restart Blender.") + except: + raise from . import converters +def clear_memory(): + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete() + + bpy.ops.collection.select_all(action='SELECT') + bpy.ops.collection.delete() + + bpy.ops.material.select_all(action='SELECT') + bpy.ops.material.delete() + + bpy.ops.text.select_all(action='SELECT') + bpy.ops.text.delete() + + bpy.ops.image.select_all(action='SELECT') + bpy.ops.image.delete() -def read_3dm(context, options): + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.delete() + +def read_3dm(context, options, block_toggle): filepath = options.get("filepath", "") + if block_toggle != False: + initial_file_3dm = filepath + initial_file_blend = bpy.data.filepath + initial_import_instances = options.get("import_instances",False) model = None try: @@ -158,9 +181,10 @@ def read_3dm(context, options): import_groups = options.get("import_groups", False) import_nested_groups = options.get("import_nested_groups", False) import_instances = options.get("import_instances",False) + create_instance_files = options.get("create_instance_files", False) + overwrite = options.get("overwrite", False) update_materials = options.get("update_materials", False) - # Import Views and NamedViews if import_views: converters.handle_views(context, model, toplayer, model.Views, "Views", scale) @@ -175,8 +199,8 @@ def read_3dm(context, options): materials[converters.DEFAULT_RHINO_MATERIAL] = None #build skeletal hierarchy of instance definitions as collections (will be populated by object importer) - if import_instances: - converters.handle_instance_definitions(context, model, toplayer, "Instance Definitions") + if import_instances: #or create_instance_files: + instance_properties = converters.handle_instance_definitions(context, model, toplayer, "Instance Definitions", initial_file_3dm) # Handle objects for ob in model.Objects: @@ -252,5 +276,113 @@ def read_3dm(context, options): bpy.ops.object.shade_smooth({'selected_editable_objects': toplayer.all_objects}) except Exception: pass - + + # Create Blender files for each linked block instance + if create_instance_files and block_toggle: + for instance in instance_properties: + if str(instance['UpdateType']) != "InstanceDefinitionUpdateType.Static": + + # Skip file creation if overwrite toggle is disabled + if os.path.isfile(instance['SourceArchive'].replace("3dm", "blend")) and not overwrite: + pass + else: + filepath = instance['SourceArchive'] + options["filepath"] = filepath + options["import_instances"] = False + try: + nuke_everything(50) + read_3dm(context, options, block_toggle=False) + except Exception: + print("Failed to create file: ", filepath) + + if create_instance_files: + + # Save .blend file and delete its contents before moving on to the next + if block_toggle == False: + filepath_blend = filepath.replace('.3dm', '.blend') + bpy.ops.wm.save_as_mainfile(filepath=filepath_blend) + nuke_everything(50) + + # Import the original model + elif block_toggle == True: + # Open the original Blender file + bpy.ops.wm.open_mainfile(filepath=initial_file_blend) + options["filepath"] = initial_file_3dm + options["import_instances"] = initial_import_instances + read_3dm(context, options, block_toggle = None) + + # Link all block files after importing the original model + elif block_toggle == None: + link_all(instance_properties) + + # Only link block files + if import_instances and not create_instance_files: + link_all(instance_properties) + if block_toggle: + print("Import complete!") return {'FINISHED'} + + +# Delete all objects and collections and purge orphan data as many times as needed +# amount value is currently a workaround because I couldn't find a way to purge everything elegantly +def nuke_everything(amount): + + # Delete all world data + for world in bpy.data.worlds: + bpy.data.worlds.remove(world) + + # Create a new world shader and set it as active + new_world = bpy.data.worlds.new(name="World") + bpy.context.scene.world = new_world + + for data in range(amount): + + # Select all collections + bpy.ops.outliner.orphans_purge() + + # Get the root collection + root_collection = bpy.context.scene.collection + + # Recursively delete all collections except the root collection + for collection in root_collection.children: + bpy.data.collections.remove(collection, do_unlink=True) + + +# Locates files corresponding to Instance Definitions and populates instances through linking +def link_all(instance_properties): + failed_link_paths = [] + failed_link_collections = [] + for instance in instance_properties: + if str(instance['UpdateType']) == "InstanceDefinitionUpdateType.Linked": + source_archive = instance['SourceArchive'] + + for ob in bpy.data.objects: + if ob.name.endswith("_Instance"): + existing_collection_name = instance['Name'] + + if ob.name == existing_collection_name + "_Instance": + file_path = instance['SourceArchive'].replace(".3dm",".blend") + existing_collection = bpy.data.collections.get(existing_collection_name) + linked_collection_name = os.path.basename(instance['SourceArchive']).split('/')[-1].replace(".3dm","") + + try: + with bpy.data.libraries.load(file_path) as (data_from, data_to): + data_to.collections = [linked_collection_name] + + for collection in data_to.collections: + if existing_collection: + existing_collection.children.link(collection) + else: + bpy.context.scene.collection.children.link(collection) + + print("The Collection \"" + linked_collection_name + "\" was successfully linked!") + except Exception: + print("Failed to link collection \"" + linked_collection_name + "\"") + failed_link_paths.append(file_path) + failed_link_collections.append(linked_collection_name) + + if failed_link_paths is not None: + for i in range(len(failed_link_paths)): + print("Couldn't link collection \"" + failed_link_collections[i] + "\" at location: " + failed_link_paths[i]) + else: + print("All Collections have been successfully linked!") \ No newline at end of file