Skip to content

Commit

Permalink
feat: added drone template selector for easy visualization of yaw ang…
Browse files Browse the repository at this point in the history
…les during yaw control
  • Loading branch information
vasarhelyi committed Jan 16, 2024
1 parent 6bc9f10 commit ccf02b9
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 13 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 35 additions & 12 deletions src/modules/sbstudio/plugin/constants.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions src/modules/sbstudio/plugin/meshes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import bpy

from contextlib import contextmanager
from math import radians
from mathutils import Matrix
from typing import Optional

Expand All @@ -14,6 +15,7 @@
from .objects import create_object

__all__ = (
"create_cone",
"create_cube",
"create_icosphere",
"edit_mesh",
Expand Down Expand Up @@ -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
):
Expand Down
17 changes: 17 additions & 0 deletions src/modules/sbstudio/plugin/model/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/modules/sbstudio/plugin/operators/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
5 changes: 5 additions & 0 deletions src/modules/sbstudio/plugin/panels/swarm.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from bpy.types import Panel

from sbstudio.plugin.constants import Collections

from sbstudio.plugin.operators import (
CreateTakeoffGridOperator,
LandOperator,
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit ccf02b9

Please sign in to comment.