diff --git a/__init__.py b/__init__.py index 5863c07..b540b24 100644 --- a/__init__.py +++ b/__init__.py @@ -23,7 +23,9 @@ from .thruster import ( ThrusterProperties, OBJECT_OT_add_thruster, - OBJECT_PT_thruster_panel + OBJECT_PT_thruster_panel, + OBJECT_PT_thruster_panel_control, + OBJECT_PT_thruster_panel_offset ) from .engine import ( EngineProperties, @@ -43,7 +45,11 @@ # 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, 5), "blender": (4, 50, 0), + "location": "Add Menu > KSA folder, and File > Export > KSA Part", "category": ["Add Mesh", "Import-Export"], } @@ -54,6 +60,8 @@ OBJECT_OT_add_thruster, OBJECT_OT_add_engine, OBJECT_PT_thruster_panel, + OBJECT_PT_thruster_panel_control, + OBJECT_PT_thruster_panel_offset, OBJECT_PT_engine_panel, OBJECT_OT_export_ksa_metadata, OBJECT_OT_export_glb_with_meta, diff --git a/blender_manifest.toml b/blender_manifest.toml index a4b89c5..471bc7a 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.4" +version = "0.0.5" 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 29441b4..11335b6 100644 --- a/engine.py +++ b/engine.py @@ -19,7 +19,7 @@ import bpy import xml.etree.ElementTree as ET import math -from .utils import _round_coordinate, _indent_xml +from .utils import _round_coordinate, _indent_xml, prop_with_unit def _engine_dict_to_xml_element(parent, engine_data, decimal_places=3): @@ -91,42 +91,42 @@ def engines_list_to_xml_str(list_of_meta): class EngineProperties(bpy.types.PropertyGroup): """Holds editable parameters for an 'Engine' object that will be used by the exporter.""" thrust_kn: bpy.props.FloatProperty( - name="Thrust kN", - description="Engine thrust in kilonewtons", - default=650.0, + name="Thrust", + description="The force provided by the thruster firing in Kilonewtons.", + default=850.0, min=0.00, ) specific_impulse_seconds: bpy.props.FloatProperty( - name="Specific Impulse Seconds", - description="Specific impulse in seconds", - default=10000.0, + name="Specific Impulse", + description="Specific impulse (Isp): \n Engine thrust divided by propellant weight (not mass) flowrate. \n Unit: [lbf]/([lbm]/[s]) = [s]·g0 = [s]", + default=350.0, min=0.00, ) minimum_throttle: bpy.props.FloatProperty( name="Minimum Throttle", description="Minimum throttle value (0-1)", - default=0.05, + default=0.10, min=0.00, max=1.00, ) volumetric_exhaust_id: bpy.props.StringProperty( - name="VolumetricExhaust_id", - description="", + name="Volumetric exhaust", + description="Volumetric exhaust effect to be used by the thurster when firing.", default="ApolloCSM" ) sound_event_action_on: bpy.props.StringProperty( - name="SoundEventAction_On", - description="", + name="Sound", + description="Sound effect to be used by the thurster when firing.", default="DefaultEngineSoundBehavior" ) exportable: bpy.props.BoolProperty( name="Export", - description="Include this object in custom exports", + description="Include this object in custom exports.", default=True, ) @@ -152,9 +152,11 @@ def execute(self, context): # This visually represents the engine exhaust direction try: obj.empty_display_type = 'CONE' - obj.empty_display_size = 0.5 + obj.empty_display_size = 4 # makes scale more appropriate + # Rotate the cone to point along +X (engine exhaust direction) - obj.rotation_euler = (0, -math.pi / 2, 0) + obj.rotation_euler = (0, 0, math.pi / 2) # align engine thrust with -X direction + obj.scale = (0.5, 2, 0.5) # better proportions except Exception: pass @@ -184,14 +186,14 @@ class OBJECT_PT_engine_panel(bpy.types.Panel): bl_idname = "OBJECT_PT_engine_panel" bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' - bl_context = 'object' + bl_context = 'data' # more intuitive location @classmethod def poll(cls, context): obj = getattr(context, 'object', None) if obj is None: return False - # Only show the panel for objects that are marked as engines + # 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): @@ -202,9 +204,9 @@ def draw(self, context): props = obj.engine_props col = layout.column() - col.prop(props, "thrust_kn") - col.prop(props, "specific_impulse_seconds") - col.prop(props, "minimum_throttle") + 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.prop(props, "volumetric_exhaust_id") col.prop(props, "sound_event_action_on") col.prop(props, "exportable") diff --git a/thruster.py b/thruster.py index 320ab6d..4ff3ad9 100644 --- a/thruster.py +++ b/thruster.py @@ -19,7 +19,7 @@ import bpy import xml.etree.ElementTree as ET import math -from .utils import _round_coordinate, _safe_vector_to_list, _indent_xml +from .utils import _round_coordinate, _safe_vector_to_list, _indent_xml, prop_with_unit def _thruster_dict_to_xml_element(parent, thruster_data, decimal_places=3): @@ -121,60 +121,65 @@ def thrusters_list_to_xml_str(list_of_meta): class ThrusterProperties(bpy.types.PropertyGroup): """Holds editable parameters for a 'Kitten' object that will be used by the exporter.""" - fx_location: bpy.props.FloatVectorProperty( - name="FxLocation", - description="Origin of the thruster effect", - default=(0.0, 0.0, 0.0), - ) + thrust_n: bpy.props.FloatProperty( - name="Thrust N", - description="?", - default=40, + name="Thrust", + description="The force provided by the thruster firing in Newtons.", + default=100, min=0.00, ) specific_impulse_seconds: bpy.props.FloatProperty( - name="Specific impulse seconds", - description="?", - default=0.0, + name="Specific impulse", + description="Specific impulse (Isp): \n Engine thrust divided by propellant weight (not mass) flowrate. \n Unit: [lbf]/([lbm]/[s]) = [s]·g0 = [s] ", + default=280.0, min=0.00, ) minimum_pulse_time_seconds: bpy.props.FloatProperty( - name="Minimum pulse time seconds", - description="?", - default=0.0, + name="Minimum pulse time", + description="Shortest thruster firing time in seconds", + default=0.5, min=0.00, ) volumetric_exhaust_id: bpy.props.StringProperty( name="VolumetricExhaust_id", - description="", + description="Volumetric exhaust effect to be used by the thurster when firing.", default="ApolloRCS" ) sound_event_on: bpy.props.StringProperty( - name="Sound event on", - description="", + name="Sound effect", + description="Sound effect to be used by the thurster when firing.", default="DefaultRcsThruster" ) control_map_translation: bpy.props.BoolVectorProperty( name="control_map_translation", - description="Set if thruster should fire on translation input. [TranslateForward, TranslateBackward, TranslateLeft, TranslateRight, TranslateUp, TranslateDown]", + description="Set if thruster should fire on translation input. Do not select both option for the same direction.", default=[False, False, False, False, False, False], size=6 ) control_map_rotation: bpy.props.BoolVectorProperty( name="control_map_rotation", - description="Set if thruster should fire on rotation input. [PitchUp, PitchDown, RollLeft, RollRight, YawLeft, YawRight]", + description="Set if thruster should fire on rotation input. Do not select both option for the same direction.", default=[False, False, False, False, False, False], size=6 ) + fx_location: bpy.props.FloatVectorProperty( + name="FxLocation", + description="Offset of the thruster effect.", + default=(0.0, 0.0, 0.0), + size=3, # 3D vector + subtype='TRANSLATION', # <-- This gives X/Y/Z, use subtype='XYZ' if you only want generic XYZ fields + unit='LENGTH' # <-- Uses scene length units (m, cm, etc.) + ) + exportable: bpy.props.BoolProperty( name="Export", - description="Include this object in custom exports", + description="Include this object in custom exports.", default=True, ) @@ -202,7 +207,7 @@ def execute(self, context): # This visually represents the thruster direction try: obj.empty_display_type = 'SINGLE_ARROW' - obj.empty_display_size = 0.3 + obj.empty_display_size = 2 # Rotate the arrow to point along +X (thruster exhaust direction) # Default arrow points along +Z, so rotate -90° around Y axis obj.rotation_euler = (0, -math.pi / 2, 0) @@ -236,7 +241,7 @@ class OBJECT_PT_thruster_panel(bpy.types.Panel): bl_idname = "OBJECT_PT_thruster_panel" bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' - bl_context = 'object' + bl_context = 'data' # more intuitive location @classmethod def poll(cls, context): @@ -256,28 +261,89 @@ def draw(self, context): col = layout.column() # Basic properties - col.prop(props, "fx_location") - col.prop(props, "thrust_n") - col.prop(props, "specific_impulse_seconds") - col.prop(props, "minimum_pulse_time_seconds") + 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() + col.prop(props, "volumetric_exhaust_id") col.prop(props, "sound_event_on") - # Translation control mapping with labels - col.separator() - box = col.box() - box.label(text="Translation Control Map:") + +class OBJECT_PT_thruster_panel_control(bpy.types.Panel): + bl_label = "Thruster control map" + bl_idname = "OBJECT_PT_thruster_panel_control" + 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() + + # ------------------------------------------ + # SIDE-BY-SIDE BOXES + # ------------------------------------------ + row = col.row(align=True) + + # --- Left Box: Translation --- + col_left = row.column(align=True) + box = col_left.box() + box.label(text="Translation") translation_labels = ["Forward", "Backward", "Left", "Right", "Up", "Down"] for i, label in enumerate(translation_labels): box.prop(props, "control_map_translation", index=i, text=label) - # Rotation control mapping with labels - col.separator() - box = col.box() - box.label(text="Rotation Control Map:") + # --- Right Box: Rotation --- + col_right = row.column(align=True) + box = col_right.box() + box.label(text="Rotation") rotation_labels = ["Pitch Up", "Pitch Down", "Roll Left", "Roll Right", "Yaw Left", "Yaw Right"] for i, label in enumerate(rotation_labels): box.prop(props, "control_map_rotation", index=i, text=label) + +class OBJECT_PT_thruster_panel_offset(bpy.types.Panel): + bl_label = "Thruster effect offset" + 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.prop(props, "fx_location") + col.separator() col.prop(props, "exportable") diff --git a/utils.py b/utils.py index a3b3e7d..e492903 100644 --- a/utils.py +++ b/utils.py @@ -240,3 +240,16 @@ def _indent_xml(elem, level=0): else: if not elem.text or not elem.text.strip(): elem.text = '' + + +def prop_with_unit(layout, props, prop_name, unit, factor_edit=0.92): # layout for units after the numbers + + prop_rna = props.bl_rna.properties[prop_name] + label = prop_rna.name + + split = layout.split(factor=factor_edit, align=True) + col_left = split.column(align=True) + col_right = split.column(align=True) + + col_left.prop(props, prop_name, text=label) + col_right.label(text=unit)