From 1316e27c8fbc7d0d85f657b9ed82a54c788049e8 Mon Sep 17 00:00:00 2001 From: Artem30801 Date: Sun, 28 Jul 2019 12:25:46 +0300 Subject: [PATCH] Initial commit for updated addon --- blender-addon/__init__.py | 44 +++ blender-addon/addon.py | 213 -------------- blender-addon/operators/drone_manipulation.py | 0 blender-addon/operators/exporter.py | 278 ++++++++++++++++++ blender-addon/operators/general_functions.py | 48 +++ blender-addon/ui/panels.py | 0 6 files changed, 370 insertions(+), 213 deletions(-) create mode 100644 blender-addon/__init__.py delete mode 100644 blender-addon/addon.py create mode 100644 blender-addon/operators/drone_manipulation.py create mode 100644 blender-addon/operators/exporter.py create mode 100644 blender-addon/operators/general_functions.py create mode 100644 blender-addon/ui/panels.py diff --git a/blender-addon/__init__.py b/blender-addon/__init__.py new file mode 100644 index 0000000..c2c18be --- /dev/null +++ b/blender-addon/__init__.py @@ -0,0 +1,44 @@ +import bpy +from bpy.utils import register_class, unregister_class + +from . operators.exporter import * + +bl_info = { + "name": "Export > CSV Drone Swarm Animation Exporter (.csv)", + "author": "Artem Vasiunik", + "version": (0, 4, 4), + "blender": (2, 80, 0), + "location": "File > Export > CSV Drone Swarm Animation Exporter (.csv)", + "description": "Export > CSV Drone Swarm Animation Exporter (.csv)", + "warning": "", + "wiki_url": "https://github.com/artem30801/blender-csv-animation/blob/master/README.md", + "tracker_url": "https://github.com/artem30801/blender-csv-animation/issues", + "category": "Import-Export" +} + +classes = (ExportCsv, ) + + +def menu_func(self, context): + self.layout.operator( + ExportCsv.bl_idname, + text="CSV Drone Swarm Animation Exporter (.csv)" + ) + + +def register(): + for cls in classes: + register_class(cls) + + bpy.types.TOPBAR_MT_file_export.append(menu_func) + + +def unregister(): + for cls in reversed(classes): + unregister_class(cls) + + bpy.types.TOPBAR_MT_file_export.remove(menu_func) + + +if __name__ == "__main__": + register() diff --git a/blender-addon/addon.py b/blender-addon/addon.py deleted file mode 100644 index 1a9d527..0000000 --- a/blender-addon/addon.py +++ /dev/null @@ -1,213 +0,0 @@ -import os -import csv -import math - -import bpy -from bpy_extras.io_utils import ExportHelper -from bpy.types import Operator -from bpy.props import StringProperty, BoolProperty, FloatProperty, IntProperty - -bl_info = { - "name": "Export > CSV Drone Swarm Animation Exporter (.csv)", - "author": "Artem Vasiunik", - "version": (0, 4, 0), - "blender": (2, 80, 0), - #"api": 36079, - "location": "File > Export > CSV Drone Swarm Animation Exporter (.csv)", - "description": "Export > CSV Drone Swarm Animation Exporter (.csv)", - "warning": "", - "wiki_url": "https://github.com/artem30801/blender-csv-animation/blob/master/README.md", - "tracker_url": "https://github.com/artem30801/blender-csv-animation/issues", - "category": "Import-Export" -} - - -class ExportCsv(Operator, ExportHelper): - bl_idname = "export_swarm_anim.folder" - bl_label = "Export Drone Swarm animation" - filename_ext = '' - use_filter_folder = True - - use_namefilter: bpy.props.BoolProperty( - name="Use name filter for objects", - default=True, - ) - - drones_name: bpy.props.StringProperty( - name="Name identifier", - description="Name identifier for all drone objects", - default="copter" - ) - - show_warnings: bpy.props.BoolProperty( - name="Show detailed animation warnings", - default=False, - ) - - speed_warning_limit: bpy.props.FloatProperty( - name="Speed limit", - description="Limit of drone movement speed (m/s)", - unit='VELOCITY', - default=3, - min=0, - ) - drone_distance_limit: bpy.props.FloatProperty( - name="Distance limit", - description="Closest possible distance between drones (m)", - unit='LENGTH', - default=1.5, - min=0, - ) - - filepath: StringProperty( - name="File Path", - description="File path used for exporting CSV files", - maxlen=1024, - subtype='DIR_PATH', - default="" - ) - - def execute(self, context): - - create_folder_if_does_not_exist(self.filepath) - scene = context.scene - objects = context.visible_objects - - drone_objects = [] - if self.use_namefilter: - for drone_obj in objects: - if self.drones_name.lower() in drone_obj.name.lower(): - drone_objects.append(drone_obj) - else: - drone_objects = objects - - frame_start = scene.frame_start - frame_end = scene.frame_end - - for drone_obj in drone_objects: - with open(os.path.join(self.filepath, '{}.csv'.format(drone_obj.name.lower())), 'w') as csv_file: - animation_file_writer = csv.writer( - csv_file, - delimiter=',', - quotechar='|', - quoting=csv.QUOTE_MINIMAL - ) - speed_exeeded = False - distance_exeeded = False - - prev_x, prev_y, prev_z = 0, 0, 0 - - animation_file_writer.writerow([ - os.path.splitext(bpy.path.basename(bpy.data.filepath))[0] - ]) - - for frame_number in range(frame_start, frame_end + 1): - scene.frame_set(frame_number) - rgb = get_rgb_from_object(drone_obj) - x, y, z = drone_obj.matrix_world.to_translation() - rot_z = drone_obj.matrix_world.to_euler('XYZ')[2] - - speed = calc_speed((x, y, z), (prev_x, prev_y, prev_z)) if frame_number != frame_start else 1 - prev_x, prev_y, prev_z = x, y, z - - if speed > self.speed_warning_limit: - speed_exeeded = True - if self.show_warnings: - self.report({'WARNING'}, - "Speed of drone '%s' is greater than %s m/s (%s m/s) on frame %s" % - (drone_obj.name, round(self.speed_warning_limit, 5), round(speed, 5), frame_number)) - - for second_drone_obj in drone_objects: - if second_drone_obj is not drone_obj: - x2, y2, z2 = second_drone_obj.matrix_world.to_translation() - distance = calc_distance((x, y, z), (x2, y2, z2)) - if distance < self.drone_distance_limit: - distance_exeeded = True - if self.show_warnings: - self.report({'WARNING'}, - "Distance beteween drones '%s' and '%s' is less than %s m (%s m) on frame %s" % - (drone_obj.name, second_drone_obj.name, - round(self.drone_distance_limit, 5), round(distance, 5), frame_number)) - - animation_file_writer.writerow([ - str(frame_number), - round(x, 5), round(y, 5), round(z, 5), - round(rot_z, 5), - *rgb, - ]) - - - - if speed_exeeded: - self.report({'WARNING'}, "Drone '%s' speed limits exeeded" % drone_obj.name) - if distance_exeeded: - self.report({'WARNING'}, "Drone '%s' distance limits exeeded" % drone_obj.name) - self.report({'WARNING'}, "Animation file exported for drone '%s'" % drone_obj.name) - return {'FINISHED'} - - -def create_folder_if_does_not_exist(folder_path): - if os.path.isdir(folder_path): - return - os.mkdir(folder_path) - - -def get_rgb_from_object(obj): - rgb = [0, 0, 0] - try: - if len(obj.material_slots) > 0: - print('material slots true') - for slot in obj.material_slots: - if "led_color" in slot.name.lower(): - print('led color') - if slot.material.use_nodes: - for node in slot.material.node_tree.nodes: - if node.type in ('EMISSION', 'BSDF_DIFFUSE'): - alpha = node.inputs[0].default_value[3] - for component in range(3): - rgb[component] = int(node.inputs[0].default_value[component] * alpha * 255) - else: - print('no led color') - for component in range(3): - rgb[component] = int(slot.material.diffuse_color[component] * 255) - - except AttributeError: - pass - finally: - return rgb - - -def calc_speed(start_point, end_point): - time_delta = 0.1 - distance = calc_distance(start_point, end_point) - return distance / time_delta - - -def calc_distance(start_point, end_point): - distance = math.sqrt( - (start_point[0] - end_point[0]) ** 2 + - (start_point[1] - end_point[1]) ** 2 + - (start_point[2] - end_point[2]) ** 2 - ) - return distance - - -def menu_func(self, context): - self.layout.operator( - ExportCsv.bl_idname, - text="CSV Drone Swarm Animation Exporter (.csv)" - ) - - -def register(): - bpy.utils.register_class(ExportCsv) - bpy.types.TOPBAR_MT_file_export.append(menu_func) - - -def unregister(): - bpy.utils.unregister_class(ExportCsv) - bpy.types.TOPBAR_MT_file_export.remove(menu_func) - - -if __name__ == "__main__": - register() diff --git a/blender-addon/operators/drone_manipulation.py b/blender-addon/operators/drone_manipulation.py new file mode 100644 index 0000000..e69de29 diff --git a/blender-addon/operators/exporter.py b/blender-addon/operators/exporter.py new file mode 100644 index 0000000..84763f6 --- /dev/null +++ b/blender-addon/operators/exporter.py @@ -0,0 +1,278 @@ +import os +import sys + +import csv +import json + +import bpy +from bpy_extras.io_utils import ExportHelper +from bpy.types import Operator +from bpy.props import StringProperty, BoolProperty, FloatProperty, IntProperty + + +from . general_functions import * + + +class ExportCsv(Operator, ExportHelper): + bl_idname = "export_swarm_anim.folder" + bl_label = "Export Drone Swarm animation" + filename_ext = '' + use_filter_folder = True + ''' + + filter_obj: bpy.props.BoolProperty( + name="Use name filter for objects", + default=True, + ) + ''' + filter_obj: bpy.props.EnumProperty( + name="Filter objects:", + description="", + items=[('all', "No filter (all objects)", ""), + ('selected', "Only selected", ""), + ('name', "By object name", ""), + ('prop', "By object property", ""), + ], + default="name" + ) + + drones_name: bpy.props.StringProperty( + name="Name identifier", + description="Name identifier for all drone objects", + default="drone" + ) + + show_warnings: bpy.props.BoolProperty( + name="Show detailed animation warnings", + default=False, + ) + + speed_warning_limit: bpy.props.FloatProperty( + name="Speed limit", + description="Limit of drone movement speed (m/s)", + unit='VELOCITY', + default=3, + min=0, + ) + + drone_distance_limit: bpy.props.FloatProperty( + name="Distance limit", + description="Closest possible distance between drones (m)", + unit='LENGTH', + default=1.5, + min=0, + ) + + filepath: StringProperty( + name="File Path", + description="File path used for exporting CSV files", + maxlen=1024, + subtype='DIR_PATH', + default="" + ) + + def draw(self, context): + layout = self.layout + col = layout.column() + col.label(text="Filtering properties") + col.prop(self, "filter_obj") + if self.filter_obj == "name": + col.prop(self, "drones_name") + col.separator() + + col = layout.column() + col.label(text="Limitation and warning properties") + col.prop(self, "show_warnings") + col.prop(self, "speed_warning_limit") + col.prop(self, "drone_distance_limit") + # TODO check button (operator) + + def get_drone_objects(self, context): + if self.filter_obj == "all": + return context.visible_objects + + if self.filter_obj == "selected": + return context.selected_objects + + if self.filter_obj == "name": + objects = context.visible_objects + return list(filter(lambda x: self.drones_name.lower() in x.name.lower(), objects)) + + if self.filter_obj == "prop": + objects = context.visible_objects + return list(filter(lambda x: x.get("is_drone", False), objects)) + + print("Invalid input") + + def execute(self, context): + create_missing_dir(self.filepath) + + drone_objects = self.get_drone_objects(context) + + frame_start = context.scene.frame_start + frame_end = context.scene.frame_end + + for drone_obj in drone_objects: + + speed_exceeded = False + distance_exceeded = False + + context.scene.frame_set(frame_start) + prev_point = get_position(drone_obj) + + anim_frames = [] + + for frame_num in range(frame_start, frame_end + 1): + context.scene.frame_set(frame_num) + + rgb = get_rgb(drone_obj) + point = drone_obj.matrix_world.to_translation() + rot_z = drone_obj.matrix_world.to_euler('XYZ')[2] + props = get_drone_properties(drone_obj) + + speed = calc_speed(point, prev_point) + speed_exceeded += self.check_speed(drone_obj, speed, frame_num) + distance_exceeded += self.check_distances(drone_obj, drone_objects, frame_num) + + row = ( + int(frame_num), + round(point[0], 5), round(point[1], 5), round(point[2], 5), + round(rot_z, 5), + rgb[0], rgb[1], rgb[2], + form_props(props), + ) + anim_frames.append(row) + + prev_point = point + + if speed_exceeded: + self.report({'WARNING'}, "Drone '{}' speed limits exceeded".format(drone_obj.name)) + if distance_exceeded: + self.report({'WARNING'}, "Drone '{}' distance limits exceeded".format(drone_obj.name)) + + header = form_header({"name": drone_obj.name.lower(), + "file": os.path.splitext(bpy.path.basename(bpy.data.filepath))[0], + "fps": context.scene.render.fps, + "version": get_addon_version(), + }) + + self.write_csv(anim_frames, header, drone_obj.name.lower()) + + self.report({'WARNING'}, "Animation file exported for drone '{}'".format(drone_obj.name)) + + return {'FINISHED'} + + def check_speed(self, drone_obj, speed, frame="Not specified"): # TODO extract from class, add decorator + if speed > self.speed_warning_limit: + if self.show_warnings: + self.report({'WARNING'}, + "Speed of drone '{}' is greater than limit of {.3f} m/s ({.3f} m/s) on frame {}".format( + drone_obj.name, + self.speed_warning_limit, speed, + frame, + )) + return True + return False + + def check_distances(self, drone, drone_objects: list, frame=0): + _drone_objects = drone_objects.copy() + if drone in _drone_objects: + _drone_objects.remove(drone) + + close_drones = filter(lambda drone2: + get_distance(drone, drone2) < self.drone_distance_limit, + _drone_objects) + + if close_drones: + if self.show_warnings: + for err_drone in close_drones: + distance = calc_distance(get_position(drone), get_position(err_drone)) + self.report({'WARNING'}, + "Distance between drones '{}' and '{}'" + " is less than {.3f} m ({.3f} m) on frame {.3f}".format( + drone.name, err_drone.name, + self.drone_distance_limit, distance, + frame + )) + return True + return False + + def write_csv(self, contents, header, name): + with open(os.path.join(self.filepath, '{}.csv'.format(name)), 'w') as csv_file: + anim_writer = csv.writer( + csv_file, + delimiter=',', + quotechar='|', + quoting=csv.QUOTE_MINIMAL + ) + anim_writer.writerow([header]) + anim_writer.writerows(contents) + + +def create_missing_dir(folder_path): + if not os.path.isdir(folder_path): + os.mkdir(folder_path) + + +def form_header(d: dict): + header = json.dumps(d) + return header + + +def form_props(d: dict): + props = json.dumps(d) + return props + + +def get_rgb(drone): + rgb = [0, 0, 0] + try: + slot = next(filter(lambda x: "led_color" in x.name.lower(), + drone.material_slots)) + except StopIteration: + print("No matching slots") + pass + else: + try: + material = slot.material + if material.use_nodes: + print('Node led color') + value = get_node_color(material) + + else: + print('Material led color') + value = material.diffuse_color + + alpha = value[3] + rgb = [int(value[component] * alpha * 255) for component in range(3)] + except AttributeError: + print("Missing attributes") + pass + + finally: + return rgb + + +def get_node_color(material): + try: + node = next(filter(lambda x: x.type in ('EMISSION', 'BSDF_DIFFUSE', "Principled BSDF"), + material.node_tree.nodes)) + except StopIteration: + print("No matching nodes") + raise AttributeError("No matching nodes") + else: + return node.inputs[0].default_value + + +def get_addon_version(): + mod = sys.modules["blender-csv-animation"] + return mod.bl_info.get('version', (-1, -1, -1)) + + + + + + + + + diff --git a/blender-addon/operators/general_functions.py b/blender-addon/operators/general_functions.py new file mode 100644 index 0000000..2ae26db --- /dev/null +++ b/blender-addon/operators/general_functions.py @@ -0,0 +1,48 @@ +import math + + +def calc_distance(start_point, end_point): + distance = math.sqrt( + (start_point[0] - end_point[0]) ** 2 + + (start_point[1] - end_point[1]) ** 2 + + (start_point[2] - end_point[2]) ** 2 + ) + return distance + + +def calc_speed(start_point, end_point, fps=10): + time_delta = 1/fps + distance = calc_distance(start_point, end_point) + return distance / time_delta + + +def get_position(drone): + return drone.matrix_world.to_translation() + + +def get_distance(drone1, drone2): + point1 = get_position(drone1) + point2 = get_position(drone2) + + return calc_distance(point1, point2) + + +def get_drone_properties(drone): + return dict(filter(lambda x: x[0].lower().startswith("drone_"), drone.items())) + + +def add_bool_property(obj, name, description="bool property"): + rna_ui = obj.get('_RNA_UI') + if rna_ui is None: + rna_ui = obj['_RNA_UI'] = {} + obj[name] = 0 + + rna_ui[name] = {"description": description, + "default": False, + "min": 0, + "max": 1, + "soft_min": 0, + "soft_max": 1} + + # def insert_prop_keyframe(obj, prop_path: str, value): + # obj.keyframe_insert(data_path='["prop"]') \ No newline at end of file diff --git a/blender-addon/ui/panels.py b/blender-addon/ui/panels.py new file mode 100644 index 0000000..e69de29