diff --git a/io_scene_forest/__init__.py b/io_scene_forest/__init__.py index e9b9418..f474672 100644 --- a/io_scene_forest/__init__.py +++ b/io_scene_forest/__init__.py @@ -8,7 +8,7 @@ bl_info = { "name": "BeamNG forest item (*.forest4.json)", "author": "BeamNG / dmn", - "version": (0, 1, 1), + "version": (0, 2, 0), "blender": (2, 80, 0), "location": "File > Import-Export", "description": "Import-Export forest files", @@ -30,6 +30,7 @@ ImportHelper, ExportHelper, ) +import os #this is needed to force refresh of changed file if "bpy" in locals() and "import_forest" in locals(): @@ -137,9 +138,151 @@ def execute(self, context): return {'FINISHED'} +class PROPERTIES_PG_forest_particle(bpy.types.PropertyGroup): + export_path: bpy.props.StringProperty( + name="Export file path", + description="", + subtype='FILE_PATH') + fi_name: bpy.props.StringProperty( + name="Forest item name", + description="") + +def particle_get_settings(context): + if context.particle_system: + return context.particle_system.settings + elif isinstance(context.space_data.pin_id, bpy.types.ParticleSettings): + return context.space_data.pin_id + return None + +class ExportParticleForest(bpy.types.Operator): + """Yes""" + bl_idname = "export_particle.forest" + bl_label = "Export particle forest" + + @classmethod + def poll(cls, context): + psys = context.particle_system + pset = particle_get_settings(context) + return psys is not None and pset is not None + + def execute(self, context): + degp = bpy.context.evaluated_depsgraph_get() + particle_systems = context.active_object.evaluated_get(degp).particle_systems + psys = particle_systems[context.particle_system.name] or None + pset = particle_get_settings(context) + if psys is not None and pset is not None: + with open(bpy.path.abspath(pset.forest.export_path), "w") as f: + export_forest.export_forest(f, pset.forest.fi_name, psys.particles) + return {'FINISHED'} + else: + print("psys:",psys) + print("pset:",pset) + self.report({'ERROR'}, 'ERROR : Could not get particle system or setting') + return {"CANCELLED"} + +class PANEL_PT_forest_particle(bpy.types.Panel): + bl_label = "BNG Forest Properties" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "particle" + bl_parent_id = "PARTICLE_PT_context_particles" + + @classmethod + def poll(cls, context): + psys = context.particle_system + pset = particle_get_settings(context) + return psys is not None and pset is not None + + def draw(self, context): + layout = self.layout + layout.use_property_split = True # Active single-column layout + layout.use_property_decorate = False + + # particles are always empty because ..... + #psys = context.particle_system + + #https://devtalk.blender.org/t/manipulating-particles-in-python/7552/2 + # Dependancy graph + degp = bpy.context.evaluated_depsgraph_get() + particle_systems = context.active_object.evaluated_get(degp).particle_systems + psys = particle_systems[context.particle_system.name] or None + # print("ctx name:",context.particle_system.name) + # print("psys is none : ", psys == None) + + pset = particle_get_settings(context) + + row = layout.row() + if not context.active_object.type == "MESH": + row.label(text='Non-mesh objects are not compatible with the exporter.', + icon='ERROR') + return + if not pset: + row.label(text='ParticleSetting not available', + icon='ERROR') + return + if psys and pset: + fo = None + try: + fo = pset.forest + except AttributeError: + # print("create") + bpy.types.ParticleSettings.forest = bpy.props.PointerProperty(type=PROPERTIES_PG_forest_particle) + fo = pset.forests + # print("fo :", fo) + row.prop(fo, "export_path") + row = layout.row() + row.prop(fo, "fi_name") + + error = False + + oprow = layout.row() + oprow.operator(ExportParticleForest.bl_idname, + text="Export", + icon='SCENE_DATA') + + if len(pset.forest.fi_name) == 0: + row = layout.row() + row.label(text='Forest Item cannot be an empty string', + icon='ERROR') + error = error or True + + if len(pset.forest.export_path) == 0 or os.path.isdir(bpy.path.abspath(pset.forest.export_path)): + row = layout.row() + row.label(text='Export path is invalid', + icon='ERROR') + error = error or True + + if not pset.forest.export_path.endswith(".forest4.json"): + row = layout.row() + row.label(text='Wrong file extention (*.forest4.json)', + icon='ERROR') + #error = error or True ## not critical + + if len(psys.particles)==0: + row = layout.row() + row.label(text='NO PARTICLES', + icon='ERROR') + error = error or True + + oprow.enabled = not error + + # DEBUG + # row = layout.row() + # row.label(text=f'child_particles = {len(psys.child_particles)}') + # row = layout.row() + # row.label(text=f'particles = {len(psys.particles)}') + # row = layout.row() + # row.label(text=f'targets = {len(psys.targets)}') + + + + addon_classes = [ExportForest, ImportForest, - SCENE_OT_instance + SCENE_OT_instance, + PROPERTIES_PG_forest_particle, + PANEL_PT_forest_particle, + ExportParticleForest ] # Add to a menu @@ -156,6 +299,10 @@ def register(): bpy.types.TOPBAR_MT_file_export.append(menu_func_export) bpy.types.TOPBAR_MT_file_import.append(menu_func_import) + def make_pointer(prop_type): + return bpy.props.PointerProperty(type=prop_type) + bpy.types.ParticleSettings.forest = make_pointer(PROPERTIES_PG_forest_particle) + def unregister(): for c in addon_classes: @@ -163,6 +310,7 @@ def unregister(): bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) + del bpy.types.ParticleSettings.forest if __name__ == "__main__": register() diff --git a/io_scene_forest/export_forest.py b/io_scene_forest/export_forest.py index a0e37f2..ec9af28 100644 --- a/io_scene_forest/export_forest.py +++ b/io_scene_forest/export_forest.py @@ -13,20 +13,34 @@ ###################################################### def export_forest(file, object_name, data_source): items = [] + print("exporting items :",len(data_source)) + loc = [0.0,0.0,0.0] for ob in data_source: - uniform_scale = (ob.scale[0] + ob.scale[1] + ob.scale[2]) / 3 + if isinstance(ob , bpy.types.Particle): + uniform_scale = ob.size + object_quaternion = ob.rotation.copy() + e_rot = mathutils.Euler(( 0.0, 0.0, math.radians(90.0)), 'XYZ') + object_quaternion = object_quaternion @ e_rot.to_quaternion() - # get euler and rotate 180 deg - object_euler = ob.rotation_euler.copy() - object_euler.rotate_axis('Z', math.radians(180)) - object_quaternion = object_euler.to_quaternion() + if len(ob.hair_keys) > 0: + loc = ob.hair_keys[0].co + else: + loc = ob.location - items.append('{"type":"' + object_name + '","pos":[' + str(ob.location[0]) + ',' + str(ob.location[1]) + ',' + str(ob.location[2]) + '],"quat":[' + str(object_quaternion[2]) + ',' + str(object_quaternion[1] * -1) + ',' + str(object_quaternion[0]) + "," + str(object_quaternion[3]) + '],"scale":' + str(uniform_scale) + "}") + else: + uniform_scale = (ob.scale[0] + ob.scale[1] + ob.scale[2]) / 3 + + # get euler and rotate 180 deg + object_euler = ob.rotation_euler.copy() + object_euler.rotate_axis('Z', math.radians(180)) + object_quaternion = object_euler.to_quaternion() + + loc = ob.location + + items.append('{"type":"' + object_name + '","pos":[' + str(loc[0]) + ',' + str(loc[1]) + ',' + str(loc[2]) + '],"quat":[' + str(object_quaternion[2]) + ',' + str(object_quaternion[1] * -1) + ',' + str(object_quaternion[0]) + "," + str(object_quaternion[3]) + '],"scale":' + str(uniform_scale) + "}") # write to file file.write("\n".join(items)) - file.close() - return ###################################################### @@ -49,6 +63,7 @@ def save_forest(filepath, # write forest file = open(filepath, 'w') export_forest(file, forest_item, data_source) + file.close() # forest export complete print(" done in %.4f sec." % (time.clock() - time1))