Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"],
}

Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion blender_manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
42 changes: 22 additions & 20 deletions engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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,
)

Expand All @@ -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

Expand Down Expand Up @@ -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):
Expand All @@ -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")
136 changes: 101 additions & 35 deletions thruster.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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,
)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand All @@ -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")
13 changes: 13 additions & 0 deletions utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)