diff --git a/__init__.py b/__init__.py index e6c89ef..acd8555 100644 --- a/__init__.py +++ b/__init__.py @@ -21,6 +21,7 @@ # Import modules with all classes and functions from . import utils from .thruster import ( + OBJECT_PT_thruster_panel_export, ThrusterProperties, OBJECT_OT_add_thruster, OBJECT_PT_thruster_panel, @@ -31,6 +32,7 @@ EngineProperties, OBJECT_OT_add_engine, OBJECT_PT_engine_panel, + OBJECT_PT_engine_panel_export, ) from .export import ( OBJECT_OT_export_ksa_metadata, @@ -42,6 +44,16 @@ menu_func_export, ) +# Addon metadata +bl_info = { + "name": "Kitten export", + "description": "A Blender addon for exporting spacecraft models to Kitten Space Agency (KSA) format with support for thrusters, engines, meshes, and materials.", + "author": "Marcus Zuber", + "version": (0, 0, 6), + "blender": (4, 50, 0), + "location": "Add Menu > KSA folder, and File > Export > KSA Part", + "category": ["Add Mesh", "Import-Export"], +} # List of all classes to register classes = ( @@ -52,7 +64,9 @@ OBJECT_PT_thruster_panel, OBJECT_PT_thruster_panel_control, OBJECT_PT_thruster_panel_offset, + OBJECT_PT_thruster_panel_export, OBJECT_PT_engine_panel, + OBJECT_PT_engine_panel_export, OBJECT_OT_export_ksa_metadata, OBJECT_OT_export_glb_with_meta, VIEW3D_MT_ksa_add, diff --git a/blender_manifest.toml b/blender_manifest.toml index 471bc7a..ce4b776 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -3,7 +3,7 @@ schema_version = "1.0.0" # Example of manifest file for a Blender extension # Change the values according to your extension id = "kittenExport" -version = "0.0.5" +version = "0.0.6" name = "Kitten Space Agency Exporter" tagline = "Export your models to Kitten Space Agency format" maintainer = "Marcus Zuber" diff --git a/engine.py b/engine.py index 579fa1d..bc1d191 100644 --- a/engine.py +++ b/engine.py @@ -52,6 +52,12 @@ def _engine_dict_to_xml_element(parent, engine_data, decimal_places=3): else: ex_dir = [1.0, 0.0, 0.0] # default forward (+X) + + if engine_data.get('sound_on'): # switchable sound export on/off + engine_sound_on_off = 'On' + else: + engine_sound_on_off = 'Off' + rounded_ex_dir = [_round_coordinate(v, decimal_places) for v in ex_dir] ET.SubElement(engine, 'ExhaustDirection', X=str(rounded_ex_dir[0]), Y=str(rounded_ex_dir[1]), Z=str(rounded_ex_dir[2])) @@ -74,8 +80,8 @@ def _engine_dict_to_xml_element(parent, engine_data, decimal_places=3): ET.SubElement(engine, 'VolumetricExhaust', Id=exhaust_id) # SoundEvent with Action and SoundId attributes - sound_id = engine_data.get('sound_event_action_on', 'DefaultEngineSoundBehavior') - ET.SubElement(engine, 'SoundEvent', Action='On', SoundId=sound_id) + sound_id = engine_data.get('sound_effect', 'DefaultEngineSoundBehavior') + ET.SubElement(engine, 'SoundEvent', Action=engine_sound_on_off, SoundId=sound_id) def engines_list_to_xml_str(list_of_meta): @@ -113,17 +119,24 @@ class EngineProperties(bpy.types.PropertyGroup): ) volumetric_exhaust_id: bpy.props.StringProperty( - name="Volumetric exhaust", + name="Exhaust effect", description="Volumetric exhaust effect to be used by the thurster when firing.", default="ApolloCSM" ) - sound_event_action_on: bpy.props.StringProperty( - name="Sound", + sound_effect: bpy.props.StringProperty( + name="Sound effect", description="Sound effect to be used by the thurster when firing.", default="DefaultEngineSoundBehavior" ) + sound_on: bpy.props.BoolProperty( + name="Sound on", + description="Sound effect on/off.", + default=True, + ) + + exportable: bpy.props.BoolProperty( name="Export", description="Include this object in custom exports.", @@ -207,6 +220,44 @@ def draw(self, context): prop_with_unit(col, props, "thrust_kn", "kN") prop_with_unit(col, props, "specific_impulse_seconds", "s") prop_with_unit(col, props, "minimum_throttle", "%") + + col.separator() + + # Exhaust effect col.prop(props, "volumetric_exhaust_id") - col.prop(props, "sound_event_action_on") + col.separator() + + # Sound effect + col.prop(props, "sound_effect") + col.prop(props, "sound_on") # Sound on/ff + col.separator() + + +class OBJECT_PT_engine_panel_export(bpy.types.Panel): + """Display engine properties in the properties panel.""" + bl_label = "Engine export" + bl_idname = "OBJECT_PT_engine_panel_export" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = 'data' # more intuitive location + + @classmethod + def poll(cls, context): + obj = getattr(context, 'object', None) + if obj is None: + return False + # Only show panel for objects that are marked as engines + return obj.get('_is_engine') is not None or obj.get('_engine_meta') is not None or obj.name.startswith('Engine') + + def draw(self, context): + layout = self.layout + obj = context.object + + # Access engine_props + props = obj.engine_props + + col = layout.column() + + col.separator() col.prop(props, "exportable") + col.separator() \ No newline at end of file diff --git a/export.py b/export.py index 7e3cf5b..dc5d3df 100644 --- a/export.py +++ b/export.py @@ -54,7 +54,7 @@ class OBJECT_OT_export_ksa_metadata(bpy.types.Operator): coordinate_decimal_places: bpy.props.IntProperty( name="Coordinate Decimal Places", - description="Number of decimal places for coordinates in the XML export (e.g., 3 for 0.001 precision)", + description="Number of decimal places for coordinates in the XML export (e.g., 3 for 0.001 precision). \n Bigger does not equal better. Recommended to not increase above 4. This is a soltuion for floating point errors for now.", default=3, min=0, max=10, @@ -98,7 +98,7 @@ def execute(self, context): 'specific_impulse_seconds': tp.specific_impulse_seconds, 'minimum_pulse_time_seconds': tp.minimum_pulse_time_seconds, 'volumetric_exhaust_id': tp.volumetric_exhaust_id, - 'sound_event_on': tp.sound_event_on, + 'sound_effect': tp.sound_effect, 'control_map_translation': _safe_vector_to_list(tp.control_map_translation), 'control_map_rotation': _safe_vector_to_list(tp.control_map_rotation), 'exportable': tp.exportable, @@ -121,7 +121,7 @@ def execute(self, context): 'specific_impulse_seconds': ep.specific_impulse_seconds, 'minimum_throttle': ep.minimum_throttle, 'volumetric_exhaust_id': ep.volumetric_exhaust_id, - 'sound_event_action_on': ep.sound_event_action_on, + 'sound_effect': ep.sound_effect, 'exportable': ep.exportable, } engines.append(entry) @@ -406,7 +406,7 @@ def execute(self, context): 'specific_impulse_seconds': kp.specific_impulse_seconds, 'minimum_pulse_time_seconds': kp.minimum_pulse_time_seconds, 'volumetric_exhaust_id': kp.volumetric_exhaust_id, - 'sound_event_on': kp.sound_event_on, + 'sound_effect': kp.sound_effect, 'control_map_translation': _safe_vector_to_list(kp.control_map_translation), 'control_map_rotation': _safe_vector_to_list(kp.control_map_rotation), 'exportable': kp.exportable, diff --git a/thruster.py b/thruster.py index 044637f..823968d 100644 --- a/thruster.py +++ b/thruster.py @@ -83,6 +83,11 @@ def _thruster_dict_to_xml_element(parent, thruster_data, decimal_places=3): if enabled and i < len(rot_labels): csv_parts.append(rot_labels[i]) + if thruster_data.get('sound_on'): # switchable sound export on/off + thruster_sound_on_off = 'On' + else: + thruster_sound_on_off = 'Off' + # Always add the ControlMap element (even if empty) csv_value = ','.join(csv_parts) if csv_parts else '' ET.SubElement(thruster, 'ControlMap', CSV=csv_value) @@ -104,8 +109,8 @@ def _thruster_dict_to_xml_element(parent, thruster_data, decimal_places=3): ET.SubElement(thruster, 'VolumetricExhaust', Id=exhaust_id) # SoundEvent with Action and SoundId attributes - sound_id = thruster_data.get('sound_event_on', 'DefaultRcsThruster') - ET.SubElement(thruster, 'SoundEvent', Action='On', SoundId=sound_id) + sound_id = thruster_data.get('sound_effect', 'DefaultRcsThruster') + ET.SubElement(thruster, 'SoundEvent', Action=thruster_sound_on_off, SoundId=sound_id) def thrusters_list_to_xml_str(list_of_meta): @@ -142,17 +147,23 @@ class ThrusterProperties(bpy.types.PropertyGroup): ) volumetric_exhaust_id: bpy.props.StringProperty( - name="VolumetricExhaust_id", + name="Exhaust effect", description="Volumetric exhaust effect to be used by the thurster when firing.", default="ApolloRCS" ) - sound_event_on: bpy.props.StringProperty( + sound_effect: bpy.props.StringProperty( name="Sound effect", - description="Sound effect to be used by the thurster when firing.", + description="Sound effect to be played when firing the thurster.", default="DefaultRcsThruster" ) + sound_on: bpy.props.BoolProperty( + name="Sound on", + description="Sound effect on/off.", + default=True, + ) + control_map_translation: bpy.props.BoolVectorProperty( name="control_map_translation", description="Set if thruster should fire on translation input. Do not select both option for the same direction.", @@ -168,7 +179,7 @@ class ThrusterProperties(bpy.types.PropertyGroup): ) fx_location: bpy.props.FloatVectorProperty( - name="FxLocation", + name="Offset", description="Offset of the thruster effect.", default=(0.0, 0.0, 0.0), size=3, # 3D vector @@ -263,11 +274,15 @@ def draw(self, context): prop_with_unit(col, props, "thrust_n", "N") prop_with_unit(col, props, "specific_impulse_seconds", "s") prop_with_unit(col, props, "minimum_pulse_time_seconds", "s") - col.separator() + # Exhaust effect col.prop(props, "volumetric_exhaust_id") - col.prop(props, "sound_event_on") + col.separator() + + # Sound effect + col.prop(props, "sound_effect") + col.prop(props, "sound_on") class OBJECT_PT_thruster_panel_control(bpy.types.Panel): @@ -343,6 +358,34 @@ def draw(self, context): col = layout.column() col.prop(props, "fx_location") + col.separator() + + +class OBJECT_PT_thruster_panel_export(bpy.types.Panel): + bl_label = "Thruster export" + bl_idname = "OBJECT_PT_thruster_panel_offset" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = 'data' + + @classmethod + def poll(cls, context): + obj = getattr(context, 'object', None) + if obj is None: + return False + # Only show panel for objects that are marked as thrusters + return obj.get('_is_thruster') is not None or obj.get('_thruster_meta') is not None or obj.name.startswith( + 'Thruster') + + def draw(self, context): + layout = self.layout + obj = context.object + + # Access thruster_props + props = obj.thruster_props + + col = layout.column() col.separator() col.prop(props, "exportable") + col.separator() \ No newline at end of file