diff --git a/io_scene_ase/__init__.py b/io_scene_ase/__init__.py index 152323c..1688a56 100644 --- a/io_scene_ase/__init__.py +++ b/io_scene_ase/__init__.py @@ -4,6 +4,7 @@ if 'builder' in locals(): importlib.reload(builder) if 'writer' in locals(): importlib.reload(writer) if 'exporter' in locals(): importlib.reload(exporter) + if 'dfs' in locals(): importlib.reload(dfs) import bpy import bpy.utils.previews @@ -11,6 +12,7 @@ from . import builder from . import writer from . import exporter +from . import dfs classes = exporter.classes diff --git a/io_scene_ase/builder.py b/io_scene_ase/builder.py index 7bd3b82..c8ce772 100644 --- a/io_scene_ase/builder.py +++ b/io_scene_ase/builder.py @@ -28,6 +28,36 @@ def __init__(self): self.should_invert_normals = False self.should_export_visible_only = True self.scale = 1.0 + self.forward_axis = 'X' + self.up_axis = 'Z' + + +def get_vector_from_axis_identifier(axis_identifier: str) -> Vector: + match axis_identifier: + case 'X': + return Vector((1.0, 0.0, 0.0)) + case 'Y': + return Vector((0.0, 1.0, 0.0)) + case 'Z': + return Vector((0.0, 0.0, 1.0)) + case '-X': + return Vector((-1.0, 0.0, 0.0)) + case '-Y': + return Vector((0.0, -1.0, 0.0)) + case '-Z': + return Vector((0.0, 0.0, -1.0)) + + +def get_coordinate_system_transform(forward_axis: str = 'X', up_axis: str = 'Z') -> Matrix: + forward = get_vector_from_axis_identifier(forward_axis) + up = get_vector_from_axis_identifier(up_axis) + left = up.cross(forward) + return Matrix(( + (forward.x, forward.y, forward.z, 0.0), + (left.x, left.y, left.z, 0.0), + (up.x, up.y, up.z, 0.0), + (0.0, 0.0, 0.0, 1.0) + )) def build_ase(context: Context, options: ASEBuildOptions, dfs_objects: Iterable[DfsObject]) -> ASE: @@ -46,6 +76,8 @@ def build_ase(context: Context, options: ASEBuildOptions, dfs_objects: Iterable[ mesh_data = cast(Mesh, dfs_object.obj.data) max_uv_layers = max(max_uv_layers, len(mesh_data.uv_layers)) + coordinate_system_transform = get_coordinate_system_transform(options.forward_axis, options.up_axis) + for object_index, dfs_object in enumerate(dfs_objects): obj = dfs_object.obj matrix_world = dfs_object.matrix_world @@ -92,7 +124,9 @@ def build_ase(context: Context, options: ASEBuildOptions, dfs_objects: Iterable[ vertex_transform = Matrix.Rotation(math.pi, 4, 'Z') @ Matrix.Scale(options.scale, 4) @ matrix_world for vertex_index, vertex in enumerate(mesh_data.vertices): - geometry_object.vertices.append(vertex_transform @ vertex.co) + vertex = vertex_transform @ vertex.co + vertex = coordinate_system_transform @ vertex + geometry_object.vertices.append(vertex) material_indices = [] if not geometry_object.is_collision: diff --git a/io_scene_ase/dfs.py b/io_scene_ase/dfs.py index d3c51ff..d6068bb 100644 --- a/io_scene_ase/dfs.py +++ b/io_scene_ase/dfs.py @@ -7,7 +7,7 @@ from typing import Optional, Set, Iterable, List -from bpy.types import Collection, Object, ViewLayer, LayerCollection +from bpy.types import Collection, Object, ViewLayer, LayerCollection, Context from mathutils import Matrix @@ -133,22 +133,12 @@ def dfs_view_layer_objects(view_layer: ViewLayer) -> Iterable[DfsObject]: @param view_layer: The view layer to inspect. @return: An iterable of tuples containing the object, the instance objects, and the world matrix. ''' - def layer_collection_objects_recursive(layer_collection: LayerCollection): + def layer_collection_objects_recursive(layer_collection: LayerCollection, visited: Set[Object]=None): + if visited is None: + visited = set() for child in layer_collection.children: - yield from layer_collection_objects_recursive(child) + yield from layer_collection_objects_recursive(child, visited=visited) # Iterate only the top-level objects in this collection first. - yield from _dfs_collection_objects_recursive(layer_collection.collection) + yield from _dfs_collection_objects_recursive(layer_collection.collection, visited=visited) yield from layer_collection_objects_recursive(view_layer.layer_collection) - - -def _is_dfs_object_visible(obj: Object, instance_objects: List[Object]) -> bool: - ''' - Check if a DFS object is visible. - @param obj: The object. - @param instance_objects: The instance objects. - @return: True if the object is visible, False otherwise. - ''' - if instance_objects: - return instance_objects[-1].visible_get() - return obj.visible_get() diff --git a/io_scene_ase/exporter.py b/io_scene_ase/exporter.py index 1036c6b..aea0ab4 100644 --- a/io_scene_ase/exporter.py +++ b/io_scene_ase/exporter.py @@ -275,6 +275,36 @@ def execute(self, context): return {'FINISHED'} +empty_set = set() +axis_identifiers = ('X', 'Y', 'Z', '-X', '-Y', '-Z') +forward_items = ( + ('X', 'X Forward', ''), + ('Y', 'Y Forward', ''), + ('Z', 'Z Forward', ''), + ('-X', '-X Forward', ''), + ('-Y', '-Y Forward', ''), + ('-Z', '-Z Forward', ''), +) + +up_items = ( + ('X', 'X Up', ''), + ('Y', 'Y Up', ''), + ('Z', 'Z Up', ''), + ('-X', '-X Up', ''), + ('-Y', '-Y Up', ''), + ('-Z', '-Z Up', ''), +) + +def forward_axis_update(self, _context: Context): + if self.forward_axis == self.up_axis: + self.up_axis = next((axis for axis in axis_identifiers if axis != self.forward_axis), 'Z') + + +def up_axis_update(self, _context: Context): + if self.up_axis == self.forward_axis: + self.forward_axis = next((axis for axis in axis_identifiers if axis != self.up_axis), 'X') + + class ASE_OT_export(Operator, ExportHelper): bl_idname = 'io_scene_ase.ase_export' bl_label = 'Export ASE' @@ -283,6 +313,8 @@ class ASE_OT_export(Operator, ExportHelper): bl_description = 'Export selected objects to ASE' filename_ext = '.ase' filter_glob: StringProperty(default="*.ase", options={'HIDDEN'}, maxlen=255) + + # TODO: why are these not part of the ASE_PG_export property group? object_eval_state: EnumProperty( items=object_eval_state_items, name='Data', @@ -290,11 +322,13 @@ class ASE_OT_export(Operator, ExportHelper): ) should_export_visible_only: BoolProperty(name='Visible Only', default=False, description='Export only visible objects') scale: FloatProperty(name='Scale', default=1.0, min=0.0001, soft_max=1000.0, description='Scale factor to apply to the exported geometry') + forward_axis: EnumProperty(name='Forward', items=forward_items, default='X', update=forward_axis_update) + up_axis: EnumProperty(name='Up', items=up_items, default='Z', update=up_axis_update) @classmethod def poll(cls, context): - if not any(x.type == 'MESH' for x in context.selected_objects): - cls.poll_message_set('At least one mesh must be selected') + if not any(x.type == 'MESH' or (x.type == 'EMPTY' and x.instance_collection is not None) for x in context.selected_objects): + cls.poll_message_set('At least one mesh or instanced collection must be selected') return False return True @@ -305,8 +339,6 @@ def draw(self, context): flow = layout.grid_flow() flow.use_property_split = True flow.use_property_decorate = False - flow.prop(self, 'should_export_visible_only') - flow.prop(self, 'scale') materials_header, materials_panel = layout.panel('Materials', default_closed=False) materials_header.label(text='Materials') @@ -334,6 +366,16 @@ def draw(self, context): else: vertex_colors_panel.label(text='No vertex color attributes found') + transform_header, transform_panel = layout.panel('Transform', default_closed=True) + transform_header.label(text='Transform') + + if transform_panel: + transform_panel.use_property_split = True + transform_panel.use_property_decorate = False + transform_panel.prop(self, 'scale') + transform_panel.prop(self, 'forward_axis') + transform_panel.prop(self, 'up_axis') + advanced_header, advanced_panel = layout.panel('Advanced', default_closed=True) advanced_header.label(text='Advanced') @@ -344,7 +386,13 @@ def draw(self, context): advanced_panel.prop(pg, 'should_invert_normals') def invoke(self, context: 'Context', event: 'Event' ) -> Union[Set[str], Set[int]]: - mesh_objects = [x[0] for x in get_mesh_objects(context.selected_objects)] + from .dfs import dfs_view_layer_objects + + mesh_objects = list(map(lambda x: x.obj, filter(lambda x: x.is_selected and x.obj.type == 'MESH', dfs_view_layer_objects(context.view_layer)))) + + if len(mesh_objects) == 0: + self.report({'ERROR'}, 'No mesh objects selected') + return {'CANCELLED'} pg = getattr(context.scene, 'ase_export') @@ -373,10 +421,12 @@ def execute(self, context): options.should_invert_normals = pg.should_invert_normals options.should_export_visible_only = self.should_export_visible_only options.scale = self.scale + options.forward_axis = self.forward_axis + options.up_axis = self.up_axis from .dfs import dfs_view_layer_objects - dfs_objects = list(filter(lambda x: x.obj.type == 'MESH', dfs_view_layer_objects(context.view_layer))) + dfs_objects = list(filter(lambda x: x.is_selected and x.obj.type == 'MESH', dfs_view_layer_objects(context.view_layer))) try: ase = build_ase(context, options, dfs_objects) @@ -425,6 +475,8 @@ class ASE_OT_export_collection(Operator, ExportHelper): export_space: EnumProperty(name='Export Space', items=export_space_items, default='INSTANCE') should_export_visible_only: BoolProperty(name='Visible Only', default=False, description='Export only visible objects') scale: FloatProperty(name='Scale', default=1.0, min=0.0001, soft_max=1000.0, description='Scale factor to apply to the exported geometry') + forward_axis: EnumProperty(name='Forward', items=forward_items, default='X', update=forward_axis_update) + up_axis: EnumProperty(name='Up', items=up_items, default='Z', update=up_axis_update) def draw(self, context): layout = self.layout @@ -433,7 +485,6 @@ def draw(self, context): flow.use_property_split = True flow.use_property_decorate = False flow.prop(self, 'should_export_visible_only') - flow.prop(self, 'scale') materials_header, materials_panel = layout.panel('Materials', default_closed=True) materials_header.label(text='Materials') @@ -450,6 +501,16 @@ def draw(self, context): col.separator() col.operator(ASE_OT_populate_material_order_list.bl_idname, icon='FILE_REFRESH', text='') + transform_header, transform_panel = layout.panel('Transform', default_closed=True) + transform_header.label(text='Transform') + + if transform_panel: + transform_panel.use_property_split = True + transform_panel.use_property_decorate = False + transform_panel.prop(self, 'scale') + transform_panel.prop(self, 'forward_axis') + transform_panel.prop(self, 'up_axis') + advanced_header, advanced_panel = layout.panel('Advanced', default_closed=True) advanced_header.label(text='Advanced') @@ -465,6 +526,8 @@ def execute(self, context): options = ASEBuildOptions() options.object_eval_state = self.object_eval_state options.scale = self.scale + options.forward_axis = self.forward_axis + options.up_axis = self.up_axis match self.export_space: case 'WORLD':