diff --git a/CHANGELOG.md b/CHANGELOG.md index 71c2f670..87a02b2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added basic support for yaw control in drone shows, including: + - multiple options for selecting a drone template before creating the swarm and the takeoff grid: sphere, cone (suitable for yaw control) or any custom selected object; + - yaw angle export into the .skyc show format. + +### Fixed + +- Minimum navigation altitude during export fixed for shows with staged landing + ## [3.0.2] - 2023-11-20 ### Fixed diff --git a/src/modules/sbstudio/plugin/constants.py b/src/modules/sbstudio/plugin/constants.py index 4d425fd8..de3661a6 100644 --- a/src/modules/sbstudio/plugin/constants.py +++ b/src/modules/sbstudio/plugin/constants.py @@ -1,10 +1,11 @@ import bpy from bpy.types import Collection +from functools import partial from typing import Callable, ClassVar, Optional from .materials import create_glowing_material -from .meshes import create_icosphere +from .meshes import create_icosphere, create_cone from .utils import ( ensure_object_exists_in_collection, get_object_in_collection, @@ -72,7 +73,7 @@ def _find( key: str, *, create: bool = True, - on_created: Optional[Callable[[bpy.types.Object], None]] = None + on_created: Optional[Callable[[bpy.types.Object], None]] = None, ): """Returns the Blender collection with the given name, and optionally creates it if it does not exist yet. @@ -88,7 +89,7 @@ def _find_in( key: str, *, create: bool = True, - on_created: Optional[Callable[[bpy.types.Object], None]] = None + on_created: Optional[Callable[[bpy.types.Object], None]] = None, ): """Returns an object from a Blender collection given its name, and optionally creates it if it does not exist yet. @@ -129,27 +130,49 @@ class Templates: """Name of the drone template object""" @classmethod - def find_drone(cls, *, create: bool = True): + def find_drone(cls, *, create: bool = True, template: str = "SPHERE"): """Returns the Blender object that serves as a template for newly - created drones, and optionally creates it if it does not exist yet. + created drones. + + Args: + template: the drone template to use. + Possible values: SPHERE, CONE, SELECTED + create: whether to create the template if it does not exist yet + """ templates = Collections.find_templates() coll = templates.objects if create: drone, _ = ensure_object_exists_in_collection( - coll, cls.DRONE, factory=cls._create_drone_template + coll, + cls.DRONE, + factory=partial(cls._create_drone_template, template=template), ) return drone else: return get_object_in_collection(coll, cls.DRONE) @staticmethod - def _create_drone_template(): - object = create_icosphere(radius=DRONE_RADIUS) - - # The icosphere is created in the current scene collection of the Blender - # context, but we don't need it there so let's remove it. - bpy.context.scene.collection.objects.unlink(object) + def _create_drone_template(template: str = "SPHERE"): + if template == "SPHERE": + object = create_icosphere(radius=DRONE_RADIUS) + elif template == "CONE": + object = create_cone(radius=DRONE_RADIUS) + elif template == "SELECTED": + objects = bpy.context.selected_objects + if not objects: + object = create_icosphere(radius=DRONE_RADIUS) + else: + object = objects[0] + else: + raise ValueError(f"Unknown drone template name: {template!r}") + + # We remove the object from all collections it is in. + for collection in bpy.data.collections: + if object.name in collection.objects: + collection.objects.unlink(object) + if object.name in bpy.context.scene.collection.objects: + bpy.context.scene.collection.objects.unlink(object) # Hide the object from the viewport and the render object.hide_viewport = True diff --git a/src/modules/sbstudio/plugin/meshes.py b/src/modules/sbstudio/plugin/meshes.py index ea05a9bc..fcb6b2dd 100644 --- a/src/modules/sbstudio/plugin/meshes.py +++ b/src/modules/sbstudio/plugin/meshes.py @@ -6,6 +6,7 @@ import bpy from contextlib import contextmanager +from math import radians from mathutils import Matrix from typing import Optional @@ -14,6 +15,7 @@ from .objects import create_object __all__ = ( + "create_cone", "create_cube", "create_icosphere", "edit_mesh", @@ -55,6 +57,45 @@ def create_cube( return _current_object_renamed_to(name) +def create_cone( + center: Coordinate3D = (0, 0, 0), radius: float = 1, *, name: Optional[str] = None +): + """Creates a Blender cone mesh object thats tip is pointing horizontally, + to be suitable for visualizing yaw controlled shows. + + Parameters: + center: the center of the cone + radius: the radius of the cone + name: the name of the mesh object; `None` to use the default name that + Blender assigns to the object + + Returns: + object: the created mesh object + """ + with use_b_mesh() as bm: + if bpy.app.version < (3, 0, 0): + raise NotImplementedError( + "Creating cone is not implemented for Blender < 3.0.0" + ) + else: + # Blender 3.0 and later. The Python API naming is consistent here. + bmesh.ops.create_cone( + bm, + cap_ends=True, + cap_tris=True, + segments=32, + radius1=radius, + depth=radius * 2, + matrix=Matrix.Rotation(radians(90), 3, "Y"), + calc_uvs=True, + ) + obj = create_object_from_bmesh(bm, name=name or "Cone") + + obj.location = center + + return obj + + def create_icosphere( center: Coordinate3D = (0, 0, 0), radius: float = 1, *, name: Optional[str] = None ): diff --git a/src/modules/sbstudio/plugin/model/settings.py b/src/modules/sbstudio/plugin/model/settings.py index febf213b..66c101de 100644 --- a/src/modules/sbstudio/plugin/model/settings.py +++ b/src/modules/sbstudio/plugin/model/settings.py @@ -38,6 +38,23 @@ class DroneShowAddonFileSpecificSettings(PropertyGroup): description="The collection that contains all the objects that are to be treated as drones", ) + drone_template = EnumProperty( + items=[ + ("SPHERE", "Sphere", "", 1), + ("CONE", "Cone", "", 2), + ("SELECTED", "Selected Object", "", 3), + ], + name="Drone template", + description=( + "Drone template object to use for all drones. " + "The SPHERE is the default simplest isotropic drone object, " + "the CONE is anisotropic for visualizing yaw control, " + "or use SELECTED for any custom object that is selected right now." + ), + default="SPHERE", + options=set(), + ) + max_acceleration = FloatProperty( name="Max acceleration", description="Maximum acceleration allowed when planning the duration of transitions between fixed points", diff --git a/src/modules/sbstudio/plugin/operators/prepare.py b/src/modules/sbstudio/plugin/operators/prepare.py index 52748f91..66092ddc 100644 --- a/src/modules/sbstudio/plugin/operators/prepare.py +++ b/src/modules/sbstudio/plugin/operators/prepare.py @@ -35,6 +35,6 @@ def execute(self, context): link_object_to_scene(templates, allow_nested=True) # Create the drone template as well - Templates.find_drone() + Templates.find_drone(template=context.scene.skybrush.settings.drone_template) return {"FINISHED"} diff --git a/src/modules/sbstudio/plugin/panels/swarm.py b/src/modules/sbstudio/plugin/panels/swarm.py index eb858439..89a4647c 100644 --- a/src/modules/sbstudio/plugin/panels/swarm.py +++ b/src/modules/sbstudio/plugin/panels/swarm.py @@ -1,5 +1,7 @@ from bpy.types import Panel +from sbstudio.plugin.constants import Collections + from sbstudio.plugin.operators import ( CreateTakeoffGridOperator, LandOperator, @@ -35,6 +37,9 @@ def draw(self, context): layout.prop(settings, "drone_collection", text="Drones") + if Collections.find_templates(create=False) is None: + layout.prop(settings, "drone_template", text="Drone") + layout.prop(settings, "max_acceleration", slider=True) layout.separator()