Skip to content

Commit

Permalink
feat(collection export): support to use blender built-in collection …
Browse files Browse the repository at this point in the history
…export (#221)

* feat(export): support to use blender built-in collection export

This will change the export part to accommodate having individual exporters per collection, essentially enabling people to model several .i3d files in one .blend project in the same scene. This still supports using the normal file-browser export as well.

The work is inspired by the glTF exporter.
  • Loading branch information
NMC-TBone authored Jan 11, 2025
1 parent 059bc9d commit 7e63185
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 185 deletions.
42 changes: 28 additions & 14 deletions addon/i3dio/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@

BINARIZER_TIMEOUT_IN_SECONDS = 30

def export_blend_to_i3d(filepath: str, axis_forward, axis_up) -> dict:

def export_blend_to_i3d(operator, filepath: str, axis_forward, axis_up, settings) -> dict:
export_data = {}

if bpy.context.scene.i3dio.log_to_file:
if operator.log_to_file:
# Remove the file ending from path and append log specific naming
filename = filepath[0:len(filepath) - len(xml_i3d.file_ending)] + debugging.export_log_file_ending
log_file_handler = logging.FileHandler(filename, mode='w')
Expand All @@ -47,7 +48,7 @@ def export_blend_to_i3d(filepath: str, axis_forward, axis_up) -> dict:
logger.info(f"Exported using '{xml_i3d.xml_current_library}'")
logger.info(f"Exporting to {filepath}")

if bpy.context.scene.i3dio.verbose_output:
if operator.verbose_output:
debugging.addon_console_handler.setLevel(logging.DEBUG)
else:
debugging.addon_console_handler.setLevel(debugging.addon_console_handler_default_level)
Expand All @@ -62,26 +63,39 @@ def export_blend_to_i3d(filepath: str, axis_forward, axis_up) -> dict:
i3d = I3D(name=bpy.path.display_name_from_filepath(filepath),
i3d_file_path=filepath,
conversion_matrix=axis_conversion(to_forward=axis_forward, to_up=axis_up, ).to_4x4(),
depsgraph=depsgraph)
depsgraph=depsgraph,
settings=settings)

# Log export settings
logger.info("Exporter settings:")
for setting, value in i3d.settings.items():
logger.info(f" {setting}: {value}")

export_selection = bpy.context.scene.i3dio.selection
if export_selection == 'ALL':
_export_active_scene_master_collection(i3d)
elif export_selection == 'ACTIVE_COLLECTION':
_export_active_collection(i3d)
elif export_selection == 'ACTIVE_OBJECT':
_export_active_object(i3d)
elif export_selection == 'SELECTED_OBJECTS':
_export_selected_objects(i3d)
# Handle case when export is triggered from a collection
source_collection = None
if operator.collection:
source_collection = bpy.data.collections.get(operator.collection)
if not source_collection:
operator.report({'ERROR'}, f"Collection '{operator.collection}' was not found")
return None

if source_collection:
logger.info(f"Exporting using Blender's collection export feature. Collection: '{source_collection.name}'")
_export_collection_content(i3d, source_collection)
else:
match operator.selection:
case 'ALL':
_export_active_scene_master_collection(i3d)
case 'ACTIVE_COLLECTION':
_export_active_collection(i3d)
case 'ACTIVE_OBJECT':
_export_active_object(i3d)
case 'SELECTED_OBJECTS':
_export_selected_objects(i3d)

i3d.export_to_i3d_file()

if bpy.context.scene.i3dio.binarize_i3d == True:
if operator.binarize_i3d:
logger.info(f'Starting binarization of "{filepath}"')
try:
i3d_binarize_path = PurePath(None if (path := bpy.context.preferences.addons['i3dio'].preferences.i3d_converter_path) == "" else path)
Expand Down
7 changes: 2 additions & 5 deletions addon/i3dio/i3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
class I3D:
"""A special node which is the root node for the entire I3D file. It essentially represents the i3d file"""
def __init__(self, name: str, i3d_file_path: str, conversion_matrix: mathutils.Matrix,
depsgraph: bpy.types.Depsgraph):
depsgraph: bpy.types.Depsgraph, settings: Dict):
self.logger = debugging.ObjectNameAdapter(logging.getLogger(f"{__name__}.{type(self).__name__}"),
{'object_name': name})
self._ids = {
Expand Down Expand Up @@ -46,10 +46,7 @@ def __init__(self, name: str, i3d_file_path: str, conversion_matrix: mathutils.M

self.i3d_mapping: List[SceneGraphNode] = []

# Save all settings for the current run unto the I3D to abstract it from the nodes themselves
self.settings = {}
for setting in bpy.context.scene.i3dio.__annotations__.keys():
self.settings[setting] = getattr(bpy.context.scene.i3dio, setting)
self.settings = settings

self.depsgraph = depsgraph

Expand Down
48 changes: 25 additions & 23 deletions addon/i3dio/node_classes/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def _resolve_filepath(self):

if filepath_relative_to_fs[0] == '$':
self.resolved_path = filepath_relative_to_fs
elif bpy.context.scene.i3dio.copy_files:
elif self.i3d.settings.get('copy_files', False):
self._copy_file()
else:
self.resolved_path = filepath_relative_to_fs
Expand All @@ -71,36 +71,38 @@ def _copy_file(self):
resolved_directory = ""
write_directory = self.i3d.paths['i3d_folder']
self.logger.info(f"is not an FS builtin and will be copied")
file_structure = bpy.context.scene.i3dio.file_structure
if file_structure == 'FLAT':
self.logger.debug(f"will be copied using the 'FLAT' hierarchy structure")
elif file_structure == 'MODHUB':
self.logger.debug(f"will be copied using the 'MODHUB' hierarchy structure")
resolved_directory = type(self).MODHUB_FOLDER
write_directory += '\\' + resolved_directory
elif file_structure == 'BLENDER':
self.logger.debug(f"'will be copied using the 'BLENDER' hierarchy structure")
# TODO: Rewrite this to make it more than three levels above the blend file but allow deeper nesting
# ,since current code just counts number of slashes
blender_relative_distance_limit = 3 # Limits the distance a file can be from the blend file
# relative steps to avoid copying entire folder structures ny mistake. Defaults to an absolute path.
if self.blender_path.count("..\\") <= blender_relative_distance_limit:
# Remove blender relative notation and filename
resolved_directory = self.blender_path[2:self.blender_path.rfind('\\')]

match self.i3d.settings.get('file_structure', 'MODHUB'):
case 'FLAT':
self.logger.debug(f"will be copied using the 'FLAT' hierarchy structure")
case 'MODHUB':
self.logger.debug(f"will be copied using the 'MODHUB' hierarchy structure")
resolved_directory = type(self).MODHUB_FOLDER
write_directory += '\\' + resolved_directory
else:
self.logger.debug(f"'exists more than {blender_relative_distance_limit} folders away "
f"from .blend file. Defaulting to absolute path and no copying.")
self.resolved_path = bpy.path.abspath(self.blender_path)
return
case 'BLENDER':
self.logger.debug(f"'will be copied using the 'BLENDER' hierarchy structure")
# TODO: Rewrite this to make it more than three levels above the blend file but allow deeper nesting
# ,since current code just counts number of slashes
blender_relative_distance_limit = 3 # Limits the distance a file can be from the blend file
# relative steps to avoid copying entire folder structures ny mistake. Defaults to an absolute path.
if self.blender_path.count("..\\") <= blender_relative_distance_limit:
# Remove blender relative notation and filename
resolved_directory = self.blender_path[2:self.blender_path.rfind('\\')]
write_directory += '\\' + resolved_directory
else:
self.logger.debug(f"'exists more than {blender_relative_distance_limit} folders away "
f"from .blend file. Defaulting to absolute path and no copying.")
self.resolved_path = bpy.path.abspath(self.blender_path)
return

self.resolved_path = resolved_directory + '\\' + self.file_name + self.file_extension

if self.resolved_path != bpy.path.abspath(self.blender_path): # Check to make sure not to overwrite the file

# We write the file if it either doesn't exists or if it exists, but we are allowed to overwrite.
write_path_full = write_directory + '\\' + self.file_name + self.file_extension
if bpy.context.scene.i3dio.overwrite_files or not os.path.exists(write_path_full):
overwrite_files = self.i3d.settings.get('overwrite_files', False)
if overwrite_files or not os.path.exists(write_path_full):
os.makedirs(write_directory, exist_ok=True)
try:
shutil.copy(bpy.path.abspath(self.blender_path), write_directory)
Expand Down
4 changes: 3 additions & 1 deletion addon/i3dio/node_classes/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ def __init__(self, id_: int,
self.xml_elements: Dict[str, Union[xml_i3d.XML_Element, None]] = {'Node': None}

self._name = self.blender_object.name
if (prefix:= bpy.context.scene.i3dio.object_sorting_prefix) != "" and (prefix_index := self._name.find(prefix)) != -1 and prefix_index < (len(self._name) - 1):

prefix = i3d.settings.get('object_sorting_prefix', "")
if prefix and (prefix_index := self._name.find(prefix)) > -1 and prefix_index < len(self._name) - 1:
self._name = self._name[prefix_index + 1:]

super().__init__(id_, i3d, parent)
Expand Down
Loading

0 comments on commit 7e63185

Please sign in to comment.