From 34722df1c3f146744a87fb8dde589e5184b55682 Mon Sep 17 00:00:00 2001 From: Gabor Vasarhelyi Date: Tue, 2 Sep 2025 15:07:16 +0200 Subject: [PATCH 01/14] feat: speedup light effects by using shared material of drones use a single common material for all drones and animate drone.color instead, with injecting that into the shader node trees of objects BREAKING CHANGE: old files are not compatible with this change yet --- src/modules/sbstudio/plugin/colors.py | 71 +++++++++++++++++++ src/modules/sbstudio/plugin/materials.py | 20 ++++-- .../sbstudio/plugin/operators/apply_color.py | 14 +--- .../plugin/operators/create_takeoff_grid.py | 18 ++--- .../detach_materials_from_template.py | 8 +-- .../sbstudio/plugin/tasks/light_effects.py | 6 +- src/modules/sbstudio/plugin/utils/sampling.py | 8 +-- 7 files changed, 102 insertions(+), 43 deletions(-) create mode 100644 src/modules/sbstudio/plugin/colors.py diff --git a/src/modules/sbstudio/plugin/colors.py b/src/modules/sbstudio/plugin/colors.py new file mode 100644 index 00000000..effb049f --- /dev/null +++ b/src/modules/sbstudio/plugin/colors.py @@ -0,0 +1,71 @@ +import bpy + +from sbstudio.model.types import RGBAColor, RGBAColorLike +from sbstudio.plugin.actions import ensure_action_exists_for_object +from sbstudio.plugin.keyframes import set_keyframes + +__all__ = ( + "create_keyframe_for_color_of_drone", + "get_color_of_drone", + "set_color_of_drone", +) + +is_blender_4 = bpy.app.version >= (4, 0, 0) + + +def create_keyframe_for_color_of_drone( + drone, + color: tuple[float, float, float] | tuple[float, float, float, float] | RGBAColor, + *, + frame: int | None = None, + step: bool = False, +): + """Creates color keyframes for the given drone to set + in the given frame. + + Parameters: + drone: the drone object to modify + color: the RGB color to use for the color of the drone + frame: the frame to apply the color on; `None` means the + current frame + step: whether to insert an additional keyframe in the preceding frame to + ensure an abrupt transition + """ + if frame is None: + frame = bpy.context.scene.frame_current + + ensure_action_exists_for_object(drone, name=f"Color of {drone.name}") + + if hasattr(color, "r"): + color_as_rgba = color.r, color.g, color.b, 1.0 + else: + color_as_rgba = color[0], color[1], color[2], 1.0 + + keyframes = [(frame, color_as_rgba)] + if step and frame > bpy.context.scene.frame_start: + keyframes.insert(0, (frame - 1, None)) + + set_keyframes(drone, "color", keyframes, interpolation="LINEAR") + + +def get_color_of_drone(drone) -> RGBAColor: + """Returns the color of the LED light on the given drone. + + Parameters: + drone: the drone to query + color: the color to apply to the LED light of the drone + """ + if drone.color is not None: + return drone.color + + return (0.0, 0.0, 0.0, 0.0) + + +def set_color_of_drone(drone, color: RGBAColorLike): + """Sets the color of the LED light on the given drone. + + Parameters: + drone: the drone to update + color: the color to apply to the LED light of the drone + """ + drone.color = color diff --git a/src/modules/sbstudio/plugin/materials.py b/src/modules/sbstudio/plugin/materials.py index 2952a4a8..611bd3d5 100644 --- a/src/modules/sbstudio/plugin/materials.py +++ b/src/modules/sbstudio/plugin/materials.py @@ -12,9 +12,12 @@ __all__ = ( "create_colored_material", "create_glowing_material", + "create_keyframe_for_diffuse_color_of_material", "get_material_for_led_light_color", "get_material_for_pyro", - "set_led_light_color", + "get_led_light_color_from_material", + "set_emission_strength_of_material", + "set_led_light_color_to_material", ) is_blender_4 = bpy.app.version >= (4, 0, 0) @@ -96,11 +99,17 @@ def create_glowing_material( links.clear() nodes.clear() + object_info_node = nodes.new("ShaderNodeObjectInfo") + object_info_node.location = (-300, 0) + emission_node = nodes.new("ShaderNodeEmission") emission_node.inputs["Strength"].default_value = strength + emission_node.location = (0, 0) output_node = nodes.new("ShaderNodeOutputMaterial") + output_node.location = (300, 0) + links.new(object_info_node.outputs["Color"], emission_node.inputs["Color"]) links.new(emission_node.outputs["Emission"], output_node.inputs["Surface"]) _set_diffuse_color_of_material(mat, color) @@ -140,13 +149,12 @@ def get_material_for_pyro(drone) -> Optional[Material]: def _get_diffuse_color_of_material(material) -> RGBAColor: - """Returns the diffuse color of the given material to the given value. + """Returns the diffuse color of the given material. The material must use a principled BSDF or an emission shader. Parameters: - material: the Blender material to update - color: the color to apply to the material + material: the Blender material to get the color from """ if material.use_nodes: # Material is using shader nodes so we need to adjust the diffuse @@ -238,7 +246,7 @@ def create_keyframe_for_diffuse_color_of_material( set_keyframes(node_tree, data_path, keyframes, interpolation="LINEAR") -def get_led_light_color(drone) -> RGBAColor: +def get_led_light_color_from_material(drone) -> RGBAColor: """Returns the color of the LED light on the given drone. Parameters: @@ -252,7 +260,7 @@ def get_led_light_color(drone) -> RGBAColor: return (0.0, 0.0, 0.0, 0.0) -def set_led_light_color(drone, color: RGBAColorLike): +def set_led_light_color_to_material(drone, color: RGBAColorLike): """Sets the color of the LED light on the given drone. Parameters: diff --git a/src/modules/sbstudio/plugin/operators/apply_color.py b/src/modules/sbstudio/plugin/operators/apply_color.py index 00f93af6..7e72ad8b 100644 --- a/src/modules/sbstudio/plugin/operators/apply_color.py +++ b/src/modules/sbstudio/plugin/operators/apply_color.py @@ -2,10 +2,7 @@ from bpy.types import Operator from random import shuffle -from sbstudio.plugin.materials import ( - create_keyframe_for_diffuse_color_of_material, - get_material_for_led_light_color, -) +from sbstudio.plugin.colors import create_keyframe_for_color_of_drone from sbstudio.plugin.props import ColorProperty from sbstudio.plugin.selection import get_selected_drones from sbstudio.plugin.utils.evaluator import create_position_evaluator @@ -158,11 +155,6 @@ def _apply_color_to_single_drone(self, drone, frame: int, index: int, ratio: flo # Gradient color = self.primary_color * (1 - ratio) + self.secondary_color * ratio - material = get_material_for_led_light_color(drone) - if not material: - # Drone does not have an LED light - return - - create_keyframe_for_diffuse_color_of_material( - material, color, frame=frame, step=not self.fade + create_keyframe_for_color_of_drone( + drone, color, frame=frame, step=not self.fade ) diff --git a/src/modules/sbstudio/plugin/operators/create_takeoff_grid.py b/src/modules/sbstudio/plugin/operators/create_takeoff_grid.py index 26be0a46..0f77c69b 100644 --- a/src/modules/sbstudio/plugin/operators/create_takeoff_grid.py +++ b/src/modules/sbstudio/plugin/operators/create_takeoff_grid.py @@ -7,15 +7,11 @@ from sbstudio.model.types import Coordinate3D from sbstudio.plugin.constants import Collections, Formations, Templates -from sbstudio.plugin.materials import ( - get_material_for_led_light_color, - get_material_for_pyro, - create_keyframe_for_diffuse_color_of_material, -) +from sbstudio.plugin.colors import create_keyframe_for_color_of_drone +from sbstudio.plugin.materials import get_material_for_pyro from sbstudio.plugin.model.formation import add_points_to_formation, create_formation from sbstudio.plugin.model.storyboard import StoryboardEntryPurpose, get_storyboard from sbstudio.plugin.operators.detach_materials_from_template import ( - detach_led_light_material_from_drone_template, detach_pyro_material_from_drone_template, ) from sbstudio.plugin.selection import select_only @@ -355,7 +351,6 @@ def _run(self, context): ) drone_collection = Collections.find_drones() - led_light_template_material = get_material_for_led_light_color(drone_template) pyro_template_material = get_material_for_pyro(drone_template) drones = [] @@ -367,16 +362,13 @@ def _run(self, context): template=drone_template, collection=drone_collection, ) - material = detach_led_light_material_from_drone_template( - drone, template_material=led_light_template_material - ) # The next line is needed for light effects to work properly - create_keyframe_for_diffuse_color_of_material( - material, (1.0, 1.0, 1.0), frame=context.scene.frame_start, step=True + create_keyframe_for_color_of_drone( + drone, (1.0, 1.0, 1.0), frame=context.scene.frame_start, step=True ) - _pyro_material = detach_pyro_material_from_drone_template( + detach_pyro_material_from_drone_template( drone, template_material=pyro_template_material ) diff --git a/src/modules/sbstudio/plugin/operators/detach_materials_from_template.py b/src/modules/sbstudio/plugin/operators/detach_materials_from_template.py index e9cf23f4..683c4bd4 100644 --- a/src/modules/sbstudio/plugin/operators/detach_materials_from_template.py +++ b/src/modules/sbstudio/plugin/operators/detach_materials_from_template.py @@ -11,6 +11,8 @@ __all__ = ("DetachMaterialsFromDroneTemplateOperator",) +# TODO: deprecated function, not used since drones use a +# shared template material def detach_led_light_material_from_drone_template( drone, template_material: Optional[Material] = None ) -> None: @@ -74,16 +76,10 @@ class DetachMaterialsFromDroneTemplateOperator(Operator): def execute(self, context): template = Templates.find_drone(create=False) - led_light_template_material = ( - get_material_for_led_light_color(template) if template else None - ) pyro_template_material = get_material_for_pyro(template) if template else None drones = Collections.find_drones() for drone in drones.objects: - detach_led_light_material_from_drone_template( - drone, template_material=led_light_template_material - ) detach_pyro_material_from_drone_template( drone, template_material=pyro_template_material ) diff --git a/src/modules/sbstudio/plugin/tasks/light_effects.py b/src/modules/sbstudio/plugin/tasks/light_effects.py index 95884f22..707f099b 100644 --- a/src/modules/sbstudio/plugin/tasks/light_effects.py +++ b/src/modules/sbstudio/plugin/tasks/light_effects.py @@ -12,7 +12,7 @@ from sbstudio.model.types import MutableRGBAColor, RGBAColor from sbstudio.plugin.constants import Collections -from sbstudio.plugin.materials import get_led_light_color, set_led_light_color +from sbstudio.plugin.colors import get_color_of_drone, set_color_of_drone from sbstudio.plugin.utils.evaluator import get_position_of_object if TYPE_CHECKING: @@ -80,7 +80,7 @@ def update_light_effects(scene: Scene, depsgraph: Depsgraph): # the base color cache in parallel to the colors list colors: list[MutableRGBAColor] = [] for drone in drones: - color = list(get_led_light_color(drone)) + color = list(get_color_of_drone(drone)) colors.append(color) _base_color_cache[id(drone)] = color else: @@ -116,7 +116,7 @@ def update_light_effects(scene: Scene, depsgraph: Depsgraph): if changed: assert drones is not None for drone, color in zip(drones, colors): - set_led_light_color(drone, color) + set_color_of_drone(drone, color) @contextmanager diff --git a/src/modules/sbstudio/plugin/utils/sampling.py b/src/modules/sbstudio/plugin/utils/sampling.py index 14039e81..700c704f 100644 --- a/src/modules/sbstudio/plugin/utils/sampling.py +++ b/src/modules/sbstudio/plugin/utils/sampling.py @@ -9,7 +9,7 @@ from sbstudio.model.point import Point4D from sbstudio.model.trajectory import Trajectory from sbstudio.model.yaw import YawSetpoint, YawSetpointList -from sbstudio.plugin.materials import get_led_light_color +from sbstudio.plugin.colors import get_color_of_drone from sbstudio.plugin.utils.evaluator import ( get_position_of_object, get_xyz_euler_rotation_of_object, @@ -217,7 +217,7 @@ def sample_colors_of_objects( for _, time in each_frame_in(frames, context=context, redraw=redraw): for obj in objects: key = obj.name if by_name else obj - color = get_led_light_color(obj) + color = get_color_of_drone(obj) lights[key].append( Color4D( time, @@ -272,7 +272,7 @@ def sample_positions_and_colors_of_objects( for obj in objects: key = obj.name if by_name else obj pos = get_position_of_object(obj) - color = get_led_light_color(obj) + color = get_color_of_drone(obj) trajectories[key].append(Point4D(time, *pos)) lights[key].append( Color4D( @@ -335,7 +335,7 @@ def sample_positions_colors_and_yaw_of_objects( for obj in objects: key = obj.name if by_name else obj pos = get_position_of_object(obj) - color = get_led_light_color(obj) + color = get_color_of_drone(obj) rotation = get_xyz_euler_rotation_of_object(obj) trajectories[key].append(Point4D(time, *pos)) lights[key].append( From 69d63cbf30edf0ba965007e5b9aeb9490832ffd7 Mon Sep 17 00:00:00 2001 From: Gabor Vasarhelyi Date: Wed, 3 Sep 2025 06:08:43 +0200 Subject: [PATCH 02/14] fix: fix find_all_f_curves_for_data_path() to return curves sorted by array index (and also fix typing) --- src/modules/sbstudio/plugin/actions.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/modules/sbstudio/plugin/actions.py b/src/modules/sbstudio/plugin/actions.py index 1e3019ab..cb75cea9 100644 --- a/src/modules/sbstudio/plugin/actions.py +++ b/src/modules/sbstudio/plugin/actions.py @@ -146,9 +146,7 @@ def find_f_curve_for_data_path_and_index( return None -def find_all_f_curves_for_data_path( - object_or_action, data_path: str -) -> Optional[FCurve]: +def find_all_f_curves_for_data_path(object_or_action, data_path: str) -> list[FCurve]: """Finds all F-curves in the F-curves of the action whose data path matches the given property, sorted by the array index of the curves. @@ -166,9 +164,10 @@ def find_all_f_curves_for_data_path( else: action = object_or_action - # TODO(ntamas): sort by array index! - result = [curve for curve in action.fcurves if curve.data_path == data_path] - return result + return sorted( + [curve for curve in action.fcurves if curve.data_path == data_path], + key=lambda c: c.array_index, + ) def cleanup_actions_for_object(object): From 1d4c33cb6f1c240fb1b1b5e7b25fc2f3567fcb51 Mon Sep 17 00:00:00 2001 From: Gabor Vasarhelyi Date: Wed, 3 Sep 2025 06:13:30 +0200 Subject: [PATCH 03/14] feat: add migration operator to convert old blender files with one material per drone to new style with shared material --- src/addons/ui_skybrush_studio.py | 2 + src/modules/sbstudio/plugin/colors.py | 2 +- src/modules/sbstudio/plugin/keyframes.py | 35 +++++++- .../sbstudio/plugin/operators/__init__.py | 4 + .../use_common_material_for_all_drones.py | 80 +++++++++++++++++++ 5 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py diff --git a/src/addons/ui_skybrush_studio.py b/src/addons/ui_skybrush_studio.py index 02dd0b7c..52694c9e 100644 --- a/src/addons/ui_skybrush_studio.py +++ b/src/addons/ui_skybrush_studio.py @@ -122,6 +122,7 @@ UpdateFrameRangeFromStoryboardOperator, UpdateTimeMarkersFromStoryboardOperator, UseSelectedVertexGroupForFormationOperator, + UseSharedMaterialForAllDronesMigrationOperator, ValidateTrajectoriesOperator, VVIZExportOperator, ) @@ -246,6 +247,7 @@ AddMarkersFromQRCodeOperator, RefreshFileFormatsOperator, RunFullProximityCheckOperator, + UseSharedMaterialForAllDronesMigrationOperator, ) #: List widgets in this addon. diff --git a/src/modules/sbstudio/plugin/colors.py b/src/modules/sbstudio/plugin/colors.py index effb049f..aa26579a 100644 --- a/src/modules/sbstudio/plugin/colors.py +++ b/src/modules/sbstudio/plugin/colors.py @@ -34,7 +34,7 @@ def create_keyframe_for_color_of_drone( if frame is None: frame = bpy.context.scene.frame_current - ensure_action_exists_for_object(drone, name=f"Color of {drone.name}") + ensure_action_exists_for_object(drone) if hasattr(color, "r"): color_as_rgba = color.r, color.g, color.b, 1.0 diff --git a/src/modules/sbstudio/plugin/keyframes.py b/src/modules/sbstudio/plugin/keyframes.py index 29be13c2..94e89272 100644 --- a/src/modules/sbstudio/plugin/keyframes.py +++ b/src/modules/sbstudio/plugin/keyframes.py @@ -1,6 +1,7 @@ """Functions related to the handling of keyframes in animation actions.""" from bpy.types import Action, FCurve +from collections import defaultdict from typing import Callable, Optional, Sequence, Tuple, Union from .actions import ( @@ -9,7 +10,7 @@ get_action_for_object, ) -__all__ = ("clear_keyframes", "set_keyframes") +__all__ = ("clear_keyframes", "get_keyframes", "set_keyframes") def clear_keyframes( @@ -67,6 +68,38 @@ def clear_keyframes( points.remove(points[index]) +def get_keyframes( + object, + data_path: str, +) -> list[tuple[float, float | list[float]]]: + """Gets the values of all keyframes of an object at the given data path. + + Parameters: + object: the object on which the keyframes are to be retrieved + data_path: the data path to use + + Returns: + the keyframes of the object at the given data path + """ + source, sep, prop = data_path.rpartition(".") + source = object.path_resolve(source) if sep else object + + fcurves = find_all_f_curves_for_data_path(object, data_path) + + match len(fcurves): + case 0: + return [] + case 1: + return [(p.co.x, p.co.y) for p in fcurves[0].keyframe_points] + case _: + frames_dict = defaultdict(lambda: [0.0] * len(fcurves)) + for curve in fcurves: + if curve.data_path == data_path: + for point in curve.keyframe_points: + frames_dict[int(point.co.x)][curve.array_index] = point.co.y + return sorted(frames_dict.items()) + + def set_keyframes( object, data_path: str, diff --git a/src/modules/sbstudio/plugin/operators/__init__.py b/src/modules/sbstudio/plugin/operators/__init__.py index 976af6af..0f47645c 100644 --- a/src/modules/sbstudio/plugin/operators/__init__.py +++ b/src/modules/sbstudio/plugin/operators/__init__.py @@ -30,6 +30,9 @@ from .get_formation_stats import GetFormationStatisticsOperator from .import_light_effects import ImportLightEffectsOperator from .land import LandOperator +from .migrations.use_common_material_for_all_drones import ( + UseSharedMaterialForAllDronesMigrationOperator, +) from .move_light_effect import ( MoveLightEffectDownOperator, MoveLightEffectUpOperator, @@ -116,6 +119,7 @@ "UpdateFrameRangeFromStoryboardOperator", "UpdateTimeMarkersFromStoryboardOperator", "UseSelectedVertexGroupForFormationOperator", + "UseSharedMaterialForAllDronesMigrationOperator", "ValidateTrajectoriesOperator", "VVIZExportOperator", ) diff --git a/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py b/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py new file mode 100644 index 00000000..19b50f10 --- /dev/null +++ b/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py @@ -0,0 +1,80 @@ +import bpy +from bpy.types import Operator + +from sbstudio.plugin.actions import ensure_action_exists_for_object +from sbstudio.plugin.constants import Collections, Templates +from sbstudio.plugin.errors import SkybrushStudioAddonError +from sbstudio.plugin.keyframes import get_keyframes, set_keyframes +from sbstudio.plugin.materials import ( + get_material_for_led_light_color, + _get_shader_node_and_input_for_diffuse_color_of_material, +) + +__all__ = ("UseSharedMaterialForAllDronesMigrationOperator",) + + +def add_object_info_to_shader_node_tree_of_drone_template() -> None: + """Checks drone template and if it seems to be old, upgrade + it to contain object info shader node as drone color source.""" + template = Templates.find_drone(create=False) + template_material = get_material_for_led_light_color(template) + if template_material is not None: + _, input = _get_shader_node_and_input_for_diffuse_color_of_material( + template_material + ) + if not input.is_linked: + nodes = template_material.node_tree.nodes + links = template_material.node_tree.links + object_info_node = nodes.new("ShaderNodeObjectInfo") + links.new(object_info_node.outputs["Color"], input) + elif input.links[0].from_node.name != "Object Info": + raise SkybrushStudioAddonError("Template drone shader node tree mismatch") + + +def upgrade_drone_color_animations_and_drone_materials() -> None: + """Moves drone color animation from shader nodes to object colors + and replaces drone material with template material.""" + template = Templates.find_drone(create=False) + template_material = get_material_for_led_light_color(template) + drones = Collections.find_drones() + for drone in drones.objects: + material = get_material_for_led_light_color(drone) + if material and material.name == f"LED color of {drone.name}": + # copy color animation from shader node to drone + node_tree = material.node_tree + if node_tree.animation_data: + node, input = _get_shader_node_and_input_for_diffuse_color_of_material( + material + ) + index = node.inputs.find(input.name) + data_path = f'nodes["{node.name}"].inputs[{index}].default_value' + keyframes = get_keyframes(node_tree, data_path) + ensure_action_exists_for_object(drone) + set_keyframes(drone, "color", keyframes, interpolation="LINEAR") + # reset drone's material to the template material + drone.material_slots[0].material = template_material + + +def set_solid_shading_color_type_to_object() -> None: + """Sets the object color source of the solid shading mode of the + 3D Viewport to use object color.""" + shading = bpy.context.space_data.shading + shading.type = "SOLID" + shading.color_type = "OBJECT" + + +class UseSharedMaterialForAllDronesMigrationOperator(Operator): + """Upgrades old Blender file content (<=3.13.2) that uses + a different material for all drone objects to store color animation to + new version in which all drones share a common material. + """ + + bl_idname = "skybrush.use_shared_material_for_all_drones_migration" + bl_label = "Use Shared Material For All Drones Migration" + + def execute(self, context): + add_object_info_to_shader_node_tree_of_drone_template() + upgrade_drone_color_animations_and_drone_materials() + set_solid_shading_color_type_to_object() + + return {"FINISHED"} From a92784e02e097d8732a4b27be6c10021e25485b5 Mon Sep 17 00:00:00 2001 From: Gabor Vasarhelyi Date: Wed, 3 Sep 2025 06:37:15 +0200 Subject: [PATCH 04/14] fix: set both solid and wireframe color to object color in all views of the 3D viewport on migration to new color-handling --- .../operators/migrations/use_common_material_for_all_drones.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py b/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py index 19b50f10..2c3554f9 100644 --- a/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py +++ b/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py @@ -59,8 +59,8 @@ def set_solid_shading_color_type_to_object() -> None: """Sets the object color source of the solid shading mode of the 3D Viewport to use object color.""" shading = bpy.context.space_data.shading - shading.type = "SOLID" shading.color_type = "OBJECT" + shading.wireframe_color_type = "OBJECT" class UseSharedMaterialForAllDronesMigrationOperator(Operator): From b5125933e1be67c837a562e824ef8e20b81cfd9a Mon Sep 17 00:00:00 2001 From: Gabor Vasarhelyi Date: Wed, 3 Sep 2025 06:50:58 +0200 Subject: [PATCH 05/14] fixup! fix: set both solid and wireframe color to object color in all views of the 3D viewport on migration to new color-handling --- .../migrations/use_common_material_for_all_drones.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py b/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py index 2c3554f9..2191bf12 100644 --- a/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py +++ b/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py @@ -55,7 +55,7 @@ def upgrade_drone_color_animations_and_drone_materials() -> None: drone.material_slots[0].material = template_material -def set_solid_shading_color_type_to_object() -> None: +def set_all_shading_color_types_to_object() -> None: """Sets the object color source of the solid shading mode of the 3D Viewport to use object color.""" shading = bpy.context.space_data.shading @@ -75,6 +75,6 @@ class UseSharedMaterialForAllDronesMigrationOperator(Operator): def execute(self, context): add_object_info_to_shader_node_tree_of_drone_template() upgrade_drone_color_animations_and_drone_materials() - set_solid_shading_color_type_to_object() + set_all_shading_color_types_to_object() return {"FINISHED"} From 827a44fa887012b7641d765938fbffc7dd75b1fc Mon Sep 17 00:00:00 2001 From: Gabor Vasarhelyi Date: Wed, 3 Sep 2025 08:56:20 +0200 Subject: [PATCH 06/14] fix: add logging to migration operator --- .../use_common_material_for_all_drones.py | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py b/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py index 2191bf12..440168ef 100644 --- a/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py +++ b/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py @@ -1,5 +1,8 @@ import bpy +import logging + from bpy.types import Operator +from time import time from sbstudio.plugin.actions import ensure_action_exists_for_object from sbstudio.plugin.constants import Collections, Templates @@ -12,6 +15,8 @@ __all__ = ("UseSharedMaterialForAllDronesMigrationOperator",) +log = logging.getLogger(__name__) + def add_object_info_to_shader_node_tree_of_drone_template() -> None: """Checks drone template and if it seems to be old, upgrade @@ -37,7 +42,9 @@ def upgrade_drone_color_animations_and_drone_materials() -> None: template = Templates.find_drone(create=False) template_material = get_material_for_led_light_color(template) drones = Collections.find_drones() - for drone in drones.objects: + num_drones = len(drones.objects) + last_log = time() + for i, drone in enumerate(drones.objects): material = get_material_for_led_light_color(drone) if material and material.name == f"LED color of {drone.name}": # copy color animation from shader node to drone @@ -53,12 +60,25 @@ def upgrade_drone_color_animations_and_drone_materials() -> None: set_keyframes(drone, "color", keyframes, interpolation="LINEAR") # reset drone's material to the template material drone.material_slots[0].material = template_material + if time() - last_log > 10 or i == num_drones - 1: + log.info( + f" {(i + 1) / num_drones * 100:.1f}% ready, " + f"{i + 1}/{num_drones} drones converted" + ) + last_log = time() def set_all_shading_color_types_to_object() -> None: - """Sets the object color source of the solid shading mode of the - 3D Viewport to use object color.""" - shading = bpy.context.space_data.shading + """Sets the object color source of the solid and wireframe + shading types of the 3D Viewport to use object color.""" + shading = getattr(bpy.context.space_data, "shading", None) + if shading is None: + log.warning( + "Space data does not have 'shading' property. " + "Call again with 3D Viewport open!" + ) + return + shading.color_type = "OBJECT" shading.wireframe_color_type = "OBJECT" @@ -73,8 +93,17 @@ class UseSharedMaterialForAllDronesMigrationOperator(Operator): bl_label = "Use Shared Material For All Drones Migration" def execute(self, context): + log.info("Upgrade started.") + + log.info("Modifying shader node tree of drone template...") add_object_info_to_shader_node_tree_of_drone_template() + + log.info("Simplifying drone color animation storage...") upgrade_drone_color_animations_and_drone_materials() + + log.info("Changing 3D Viewport shader color types to 'OBJECT'...") set_all_shading_color_types_to_object() + log.info("Upgrade successful.") + return {"FINISHED"} From 5a1bf1a9b90fcf9ff13f7fc3e41ba8ae63a7eb7d Mon Sep 17 00:00:00 2001 From: Gabor Vasarhelyi Date: Wed, 3 Sep 2025 10:13:12 +0200 Subject: [PATCH 07/14] fix: configure logging explicitly as in Blender 4.5 default log level is warn, not info --- src/modules/sbstudio/plugin/tasks/initialization.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/modules/sbstudio/plugin/tasks/initialization.py b/src/modules/sbstudio/plugin/tasks/initialization.py index 1ab2c702..f3553174 100644 --- a/src/modules/sbstudio/plugin/tasks/initialization.py +++ b/src/modules/sbstudio/plugin/tasks/initialization.py @@ -71,6 +71,16 @@ def update_pyro_particles_of_drones(*args): update_pyro_particles_of_object(drone) +def config_logging(*args): + import logging + + logging.basicConfig( + format="%(asctime)s.%(msecs)03d %(levelname)s: %(message)s", + level=logging.INFO, + datefmt="%H:%M:%S", + ) + + class InitializationTask(Task): """Background task that is called every time a new file is loaded.""" @@ -81,5 +91,6 @@ class InitializationTask(Task): remove_legacy_formation_constraints, setup_random_seed, update_pyro_particles_of_drones, + config_logging, ] } From f435f3be1c457e93b96f641edc210e9a558e8e68 Mon Sep 17 00:00:00 2001 From: Gabor Vasarhelyi Date: Wed, 3 Sep 2025 11:20:27 +0200 Subject: [PATCH 08/14] feat: add MigrationOperator for automatic user-confirmed migration operators --- src/modules/sbstudio/plugin/operators/base.py | 31 ++++++++ .../use_common_material_for_all_drones.py | 75 ++++++++++++++----- .../sbstudio/plugin/tasks/initialization.py | 5 ++ 3 files changed, 91 insertions(+), 20 deletions(-) diff --git a/src/modules/sbstudio/plugin/operators/base.py b/src/modules/sbstudio/plugin/operators/base.py index 55cc2d72..0f95fe79 100644 --- a/src/modules/sbstudio/plugin/operators/base.py +++ b/src/modules/sbstudio/plugin/operators/base.py @@ -496,3 +496,34 @@ def _propose_marker_count(self, context: Context) -> int: else: num_existing_markers = 0 return max(0, num_drones - num_existing_markers) + + +class MigrationOperator(Operator): + """Operator mixin for migrations/upgrades for files created in earlier + versions of the Skybrush Studio for Blender plugin.""" + + @classmethod + def poll(cls, context: Context): + return context.scene.skybrush + + def execute(self, context: Context): + if self.needs_migration(): + return self.execute_migration(context) + + return {"FINISHED"} + + def invoke(self, context: Context, event): + if self.needs_migration(): + return context.window_manager.invoke_confirm( + self, event, title=self.bl_label, message=self.bl_description + ) + + return {"CANCELLED"} + + def needs_migration(self) -> bool: + """Returns whether the current Blender content needs migration.""" + raise NotImplementedError + + def execute_migration(self, context: Context): + """Executes the migration/upgrade on the current Blender content.""" + raise NotImplementedError diff --git a/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py b/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py index 440168ef..f71f3674 100644 --- a/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py +++ b/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py @@ -1,7 +1,6 @@ import bpy import logging -from bpy.types import Operator from time import time from sbstudio.plugin.actions import ensure_action_exists_for_object @@ -12,6 +11,7 @@ get_material_for_led_light_color, _get_shader_node_and_input_for_diffuse_color_of_material, ) +from sbstudio.plugin.operators.base import MigrationOperator __all__ = ("UseSharedMaterialForAllDronesMigrationOperator",) @@ -36,6 +36,22 @@ def add_object_info_to_shader_node_tree_of_drone_template() -> None: raise SkybrushStudioAddonError("Template drone shader node tree mismatch") +def set_all_shading_color_types_to_object() -> None: + """Sets the object color source of the solid and wireframe + shading types of the 3D Viewport to use object color.""" + shading = getattr(bpy.context.space_data, "shading", None) + if shading is None: + log.warning( + "Space data does not have a 'shading' property. " + "Set Object Color and Wireframe color to Object " + "in your 3D Viewport's Viewport Shading properties manually!" + ) + return + + shading.color_type = "OBJECT" + shading.wireframe_color_type = "OBJECT" + + def upgrade_drone_color_animations_and_drone_materials() -> None: """Moves drone color animation from shader nodes to object colors and replaces drone material with template material.""" @@ -62,37 +78,52 @@ def upgrade_drone_color_animations_and_drone_materials() -> None: drone.material_slots[0].material = template_material if time() - last_log > 10 or i == num_drones - 1: log.info( - f" {(i + 1) / num_drones * 100:.1f}% ready, " + f"{(i + 1) / num_drones * 100:.1f}% ready, " f"{i + 1}/{num_drones} drones converted" ) last_log = time() -def set_all_shading_color_types_to_object() -> None: - """Sets the object color source of the solid and wireframe - shading types of the 3D Viewport to use object color.""" - shading = getattr(bpy.context.space_data, "shading", None) - if shading is None: - log.warning( - "Space data does not have 'shading' property. " - "Call again with 3D Viewport open!" - ) - return +def needs_migration(): + """Returns whether the current Blender content needs migration.""" + # TODO: what should be the optimal method to check if file + # needs migration or not? We check the template material now + # as it is most probably not modified by the users frequently + try: + template = Templates.find_drone(create=False) + except (KeyError, ValueError): + return False - shading.color_type = "OBJECT" - shading.wireframe_color_type = "OBJECT" + template_material = get_material_for_led_light_color(template) + if template_material is None: + return False + + _, input = _get_shader_node_and_input_for_diffuse_color_of_material( + template_material + ) + return not input.is_linked -class UseSharedMaterialForAllDronesMigrationOperator(Operator): - """Upgrades old Blender file content (<=3.13.2) that uses - a different material for all drone objects to store color animation to - new version in which all drones share a common material. + +class UseSharedMaterialForAllDronesMigrationOperator(MigrationOperator): + """Upgrades old Skybrush Studio for Blender file content (<=3.13.2) + that uses a separate material for all drone objects to a new version + in which all drones share a common material. This speeds up light effect + handling substantially. """ bl_idname = "skybrush.use_shared_material_for_all_drones_migration" - bl_label = "Use Shared Material For All Drones Migration" + bl_label = "Update file content to speed up light effect rendering" + bl_description = ( + "Upgrade your old (<=3.13.2) Skybrush Studio for Blender file content\n" + "to speed up light effect playback and show export, by replacing all\n" + "drone object materials to a shared template material, modifying its shader\n" + "node tree and storing color animations in the drone object's 'color' property\n" + ) + + def execute_migration(self, context): + """Executes the migration/upgrade on the current Blender content.""" - def execute(self, context): log.info("Upgrade started.") log.info("Modifying shader node tree of drone template...") @@ -107,3 +138,7 @@ def execute(self, context): log.info("Upgrade successful.") return {"FINISHED"} + + def needs_migration(self) -> bool: + """Returns whether the current Blender content needs migration.""" + return needs_migration() diff --git a/src/modules/sbstudio/plugin/tasks/initialization.py b/src/modules/sbstudio/plugin/tasks/initialization.py index f3553174..64bea2a4 100644 --- a/src/modules/sbstudio/plugin/tasks/initialization.py +++ b/src/modules/sbstudio/plugin/tasks/initialization.py @@ -81,6 +81,10 @@ def config_logging(*args): ) +def perform_migrations(*args): + bpy.ops.skybrush.use_shared_material_for_all_drones_migration("INVOKE_DEFAULT") + + class InitializationTask(Task): """Background task that is called every time a new file is loaded.""" @@ -92,5 +96,6 @@ class InitializationTask(Task): setup_random_seed, update_pyro_particles_of_drones, config_logging, + perform_migrations, ] } From addb96f556f57eac9417df78c8dfe9768d624ce0 Mon Sep 17 00:00:00 2001 From: Gabor Vasarhelyi Date: Fri, 5 Sep 2025 11:00:16 +0100 Subject: [PATCH 09/14] feat: add internal version number for automating migration handling --- src/modules/sbstudio/plugin/model/show.py | 11 ++++- src/modules/sbstudio/plugin/operators/base.py | 41 +++++++++++++++---- .../use_common_material_for_all_drones.py | 13 ++++-- .../sbstudio/plugin/tasks/initialization.py | 1 + 4 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/modules/sbstudio/plugin/model/show.py b/src/modules/sbstudio/plugin/model/show.py index adad15ff..d2f6f7bf 100644 --- a/src/modules/sbstudio/plugin/model/show.py +++ b/src/modules/sbstudio/plugin/model/show.py @@ -1,4 +1,4 @@ -from bpy.props import PointerProperty +from bpy.props import IntProperty, PointerProperty from bpy.types import PropertyGroup from .formations_panel import FormationsPanelProperties @@ -35,3 +35,12 @@ class DroneShowAddonProperties(PropertyGroup): type=DroneShowAddonFileSpecificSettings ) storyboard: Storyboard = PointerProperty(type=Storyboard) + version: IntProperty = IntProperty( + name="Version", + description=( + "Current version of the show content stored in Blender. " + "Version 1 is the initial version (plugin version <= 3.13.2). " + "Version 2 uses a shared material for all drones to speed up light effects." + ), + default=1, + ) diff --git a/src/modules/sbstudio/plugin/operators/base.py b/src/modules/sbstudio/plugin/operators/base.py index 0f95fe79..65677a16 100644 --- a/src/modules/sbstudio/plugin/operators/base.py +++ b/src/modules/sbstudio/plugin/operators/base.py @@ -10,7 +10,7 @@ from numpy.typing import NDArray from typing import Any, Optional -from bpy.props import BoolProperty, EnumProperty +from bpy.props import BoolProperty, EnumProperty, IntProperty from bpy.types import Collection, Context, Object, Operator from bpy_extras.io_utils import ExportHelper @@ -502,28 +502,53 @@ class MigrationOperator(Operator): """Operator mixin for migrations/upgrades for files created in earlier versions of the Skybrush Studio for Blender plugin.""" + version_from = IntProperty(name="Input format version", options={"HIDDEN"}) + version_to = IntProperty(name="Output format version", options={"HIDDEN"}) + @classmethod def poll(cls, context: Context): return context.scene.skybrush def execute(self, context: Context): - if self.needs_migration(): - return self.execute_migration(context) + if context.scene.skybrush.version < self.version_from: + raise RuntimeError( + f"Input format version should be {self.version_from}, " + f"not {context.scene.skybrush.version}" + ) + elif context.scene.skybrush.version == self.version_from: + retval = ( + self.execute_migration(context) + if self.needs_migration() + else {"FINISHED"} + ) + if retval == {"FINISHED"}: + context.scene.skybrush.version = self.version_to + + return retval return {"FINISHED"} def invoke(self, context: Context, event): + self.initialize_migration() + + if context.scene.skybrush.version >= self.version_to: + return {"CANCELLED"} + if self.needs_migration(): return context.window_manager.invoke_confirm( self, event, title=self.bl_label, message=self.bl_description ) + else: + return self.execute(context) - return {"CANCELLED"} + def execute_migration(self, context: Context): + """Executes the migration/upgrade on the current Blender content.""" + raise NotImplementedError - def needs_migration(self) -> bool: - """Returns whether the current Blender content needs migration.""" + def initialize_migration(self) -> None: + """Initializes the operator by setting up the from/to versions.""" raise NotImplementedError - def execute_migration(self, context: Context): - """Executes the migration/upgrade on the current Blender content.""" + def needs_migration(self) -> bool: + """Returns whether the current Blender content needs migration.""" raise NotImplementedError diff --git a/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py b/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py index f71f3674..6bf6b027 100644 --- a/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py +++ b/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py @@ -121,6 +121,15 @@ class UseSharedMaterialForAllDronesMigrationOperator(MigrationOperator): "node tree and storing color animations in the drone object's 'color' property\n" ) + def initialize_migration(self) -> None: + """Initializes the operator by setting up the from/to versions.""" + self.version_from = 1 + self.version_to = 2 + + def needs_migration(self) -> bool: + """Returns whether the current Blender content needs migration.""" + return needs_migration() + def execute_migration(self, context): """Executes the migration/upgrade on the current Blender content.""" @@ -138,7 +147,3 @@ def execute_migration(self, context): log.info("Upgrade successful.") return {"FINISHED"} - - def needs_migration(self) -> bool: - """Returns whether the current Blender content needs migration.""" - return needs_migration() diff --git a/src/modules/sbstudio/plugin/tasks/initialization.py b/src/modules/sbstudio/plugin/tasks/initialization.py index 64bea2a4..eff1512f 100644 --- a/src/modules/sbstudio/plugin/tasks/initialization.py +++ b/src/modules/sbstudio/plugin/tasks/initialization.py @@ -82,6 +82,7 @@ def config_logging(*args): def perform_migrations(*args): + # version 1 -> 2 bpy.ops.skybrush.use_shared_material_for_all_drones_migration("INVOKE_DEFAULT") From 67e43cf9a92fac38e4c8139094bb4ed7ee8416a1 Mon Sep 17 00:00:00 2001 From: Gabor Vasarhelyi Date: Sat, 6 Sep 2025 16:36:22 +0100 Subject: [PATCH 10/14] fix: do not mess around with logging as a default init call --- src/modules/sbstudio/plugin/tasks/initialization.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/sbstudio/plugin/tasks/initialization.py b/src/modules/sbstudio/plugin/tasks/initialization.py index eff1512f..3cd4722c 100644 --- a/src/modules/sbstudio/plugin/tasks/initialization.py +++ b/src/modules/sbstudio/plugin/tasks/initialization.py @@ -71,7 +71,7 @@ def update_pyro_particles_of_drones(*args): update_pyro_particles_of_object(drone) -def config_logging(*args): +def _config_logging(*args): import logging logging.basicConfig( @@ -96,7 +96,8 @@ class InitializationTask(Task): remove_legacy_formation_constraints, setup_random_seed, update_pyro_particles_of_drones, - config_logging, + # enable this below for neat logs + # _config_logging, perform_migrations, ] } From a6ed36f84dbfcadb746482abd025ca3cfa2745ee Mon Sep 17 00:00:00 2001 From: Gabor Vasarhelyi Date: Sat, 6 Sep 2025 16:48:14 +0100 Subject: [PATCH 11/14] fix: update viewport object and wireframe colors properly on all active 3D viewports --- .../use_common_material_for_all_drones.py | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py b/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py index 6bf6b027..ccd4b64f 100644 --- a/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py +++ b/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py @@ -12,6 +12,7 @@ _get_shader_node_and_input_for_diffuse_color_of_material, ) from sbstudio.plugin.operators.base import MigrationOperator +from sbstudio.plugin.views import find_all_3d_views __all__ = ("UseSharedMaterialForAllDronesMigrationOperator",) @@ -39,17 +40,9 @@ def add_object_info_to_shader_node_tree_of_drone_template() -> None: def set_all_shading_color_types_to_object() -> None: """Sets the object color source of the solid and wireframe shading types of the 3D Viewport to use object color.""" - shading = getattr(bpy.context.space_data, "shading", None) - if shading is None: - log.warning( - "Space data does not have a 'shading' property. " - "Set Object Color and Wireframe color to Object " - "in your 3D Viewport's Viewport Shading properties manually!" - ) - return - - shading.color_type = "OBJECT" - shading.wireframe_color_type = "OBJECT" + for space in find_all_3d_views(): + space.shading.color_type = "OBJECT" + space.shading.wireframe_color_type = "OBJECT" def upgrade_drone_color_animations_and_drone_materials() -> None: @@ -115,10 +108,11 @@ class UseSharedMaterialForAllDronesMigrationOperator(MigrationOperator): bl_idname = "skybrush.use_shared_material_for_all_drones_migration" bl_label = "Update file content to speed up light effect rendering" bl_description = ( - "Upgrade your old (<=3.13.2) Skybrush Studio for Blender file content\n" + "Upgrade your old (<4.0) Skybrush Studio for Blender file content\n" "to speed up light effect playback and show export, by replacing all\n" "drone object materials to a shared template material, modifying its shader\n" - "node tree and storing color animations in the drone object's 'color' property\n" + "node tree and storing color animations in the drone object's 'color' property.\n" + "The upgrade also changes active 3D Viewport wireframe and object color to 'OBJECT'.\n" ) def initialize_migration(self) -> None: From 3e226590db96673c410d64f959aa1c108b409f0c Mon Sep 17 00:00:00 2001 From: Gabor Vasarhelyi Date: Sun, 7 Sep 2025 09:59:17 +0100 Subject: [PATCH 12/14] feat: add warnings to panels on version, formation size or shading color mismatch --- src/modules/sbstudio/plugin/model/show.py | 7 ++ .../sbstudio/plugin/panels/formations.py | 31 +++----- .../sbstudio/plugin/panels/led_control.py | 7 ++ src/modules/sbstudio/plugin/utils/warnings.py | 70 +++++++++++++++++++ 4 files changed, 94 insertions(+), 21 deletions(-) create mode 100644 src/modules/sbstudio/plugin/utils/warnings.py diff --git a/src/modules/sbstudio/plugin/model/show.py b/src/modules/sbstudio/plugin/model/show.py index d2f6f7bf..a9837762 100644 --- a/src/modules/sbstudio/plugin/model/show.py +++ b/src/modules/sbstudio/plugin/model/show.py @@ -42,5 +42,12 @@ class DroneShowAddonProperties(PropertyGroup): "Version 1 is the initial version (plugin version <= 3.13.2). " "Version 2 uses a shared material for all drones to speed up light effects." ), + min=1, + max=2, default=1, ) + + @property + def latest_version(self) -> int: + """Returns the latest plugin file format version available.""" + return self.__annotations__["version"].keywords["max"] diff --git a/src/modules/sbstudio/plugin/panels/formations.py b/src/modules/sbstudio/plugin/panels/formations.py index 1b3fa2b5..9fdc258d 100644 --- a/src/modules/sbstudio/plugin/panels/formations.py +++ b/src/modules/sbstudio/plugin/panels/formations.py @@ -1,7 +1,6 @@ from bpy.types import Panel from sbstudio.plugin.menus import GenerateMarkersMenu -from sbstudio.plugin.model.formation import count_markers_in_formation from sbstudio.plugin.operators import ( CreateFormationOperator, CreateTakeoffGridOperator, @@ -16,7 +15,11 @@ UpdateFormationOperator, AppendFormationToStoryboardOperator, ) -from sbstudio.plugin.stats import get_drone_count +from sbstudio.plugin.utils.warnings import ( + draw_bad_shader_color_source_warning, + draw_formation_size_warning, + draw_version_warning, +) __all__ = ("FormationsPanel",) @@ -40,14 +43,15 @@ def poll(cls, context): return context.scene.skybrush.formations def draw(self, context): - scene = context.scene - formations = scene.skybrush.formations + formations = context.scene.skybrush.formations if not formations: return - selected_formation = formations.selected layout = self.layout + draw_version_warning(context, layout) + draw_bad_shader_color_source_warning(context, layout) + layout.operator(CreateTakeoffGridOperator.bl_idname, icon="ADD") row = layout.row(align=True) @@ -67,22 +71,7 @@ def draw(self, context): row.operator(DeselectFormationOperator.bl_idname, text="Deselect") row.operator(GetFormationStatisticsOperator.bl_idname, text="Stats") - if selected_formation: - num_drones = get_drone_count() - num_markers = count_markers_in_formation(formations.selected) - - # If the number of markers in the formation is different from the - # number of drones, show a warning as we won't know what to do with - # the extra or missing drones - if num_markers != num_drones: - row = layout.box() - row.alert = False - row.label( - text=f"Formation size: {num_markers} " - f"{'<' if num_markers < num_drones else '>'} " - f"{num_drones}", - icon="ERROR", - ) + draw_formation_size_warning(context, layout) row = layout.row(align=True) row.menu( diff --git a/src/modules/sbstudio/plugin/panels/led_control.py b/src/modules/sbstudio/plugin/panels/led_control.py index 91d7d528..9d3412d9 100644 --- a/src/modules/sbstudio/plugin/panels/led_control.py +++ b/src/modules/sbstudio/plugin/panels/led_control.py @@ -4,6 +4,10 @@ ApplyColorsToSelectedDronesOperator as ApplyColors, ) from sbstudio.plugin.utils.bloom import bloom_effect_supported +from sbstudio.plugin.utils.warnings import ( + draw_bad_shader_color_source_warning, + draw_version_warning, +) class LEDControlPanel(Panel): @@ -32,6 +36,9 @@ def draw(self, context): layout = self.layout + draw_version_warning(context, layout) + draw_bad_shader_color_source_warning(context, layout) + row = layout.row() col = row.column() col.prop(led_control, "primary_color", text="Primary", icon="COLOR") diff --git a/src/modules/sbstudio/plugin/utils/warnings.py b/src/modules/sbstudio/plugin/utils/warnings.py new file mode 100644 index 00000000..ba7a1b6c --- /dev/null +++ b/src/modules/sbstudio/plugin/utils/warnings.py @@ -0,0 +1,70 @@ +from bpy.types import Context + +from sbstudio.plugin.model.formation import count_markers_in_formation +from sbstudio.plugin.stats import get_drone_count + +__all__ = ( + "draw_bad_shader_color_source_warning", + "draw_formation_size_warning", + "draw_version_warning", +) + + +def _draw_warning(layout, text: str) -> None: + row = layout.box() + row.alert = False + row.label(text=text, icon="ERROR") + + +def draw_bad_shader_color_source_warning(context: Context, layout) -> None: + """Draw a bad shader color source warning to a layout, if needed.""" + label = ( + "Set shader Wireframe Color to 'OBJECT'" + if ( + context.space_data.shading.type == "WIREFRAME" + and context.space_data.shading.wireframe_color_type != "OBJECT" + ) + else "Set shader Object Color to 'OBJECT'" + if ( + context.space_data.shading.type == "SOLID" + and context.space_data.shading.color_type != "OBJECT" + ) + else None + ) + if label: + _draw_warning(layout, text=label) + + +def draw_formation_size_warning(context: Context, layout) -> None: + """Draw a formation size warning to a layout, if needed.""" + formations = context.scene.skybrush.formations + if not formations: + return + + selected_formation = formations.selected + if not selected_formation: + return + + num_drones = get_drone_count() + num_markers = count_markers_in_formation(selected_formation) + + # If the number of markers in the formation is different from the + # number of drones, show a warning as we won't know what to do with + # the extra or missing drones + if num_markers != num_drones: + _draw_warning( + layout, + text=f"Formation size: {num_markers} " + f"{'<' if num_markers < num_drones else '>'} " + f"{num_drones}", + ) + + +def draw_version_warning(context: Context, layout) -> None: + """Draw a version warning to a layout, if needed.""" + skybrush = context.scene.skybrush + if skybrush.version < skybrush.latest_version: + _draw_warning( + layout, + text=f"File format is old (version {skybrush.version} < {skybrush.latest_version})", + ) From a391b29f76d945d3822d1cf31d63c903ed8b6cbe Mon Sep 17 00:00:00 2001 From: Gabor Vasarhelyi Date: Mon, 29 Sep 2025 14:57:14 +0200 Subject: [PATCH 13/14] fix: do not use __annotations__ when getting latest version --- src/modules/sbstudio/plugin/constants.py | 3 +++ src/modules/sbstudio/plugin/model/show.py | 9 +++------ src/modules/sbstudio/plugin/utils/warnings.py | 5 +++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/modules/sbstudio/plugin/constants.py b/src/modules/sbstudio/plugin/constants.py index 74fe2a6d..80eaa06d 100644 --- a/src/modules/sbstudio/plugin/constants.py +++ b/src/modules/sbstudio/plugin/constants.py @@ -46,6 +46,9 @@ DEFAULT_OUTDOOR_DRONE_RADIUS = 0.5 """Default outdoor drone radius""" +LATEST_SKYBRUSH_PLUGIN_VERSION = 2 +"""The latest (current) plugin version.""" + NUM_PYRO_CHANNELS = 6 """The number of pyro channels that we support.""" diff --git a/src/modules/sbstudio/plugin/model/show.py b/src/modules/sbstudio/plugin/model/show.py index a9837762..14c85dbf 100644 --- a/src/modules/sbstudio/plugin/model/show.py +++ b/src/modules/sbstudio/plugin/model/show.py @@ -1,6 +1,8 @@ from bpy.props import IntProperty, PointerProperty from bpy.types import PropertyGroup +from sbstudio.plugin.constants import LATEST_SKYBRUSH_PLUGIN_VERSION + from .formations_panel import FormationsPanelProperties from .led_control import LEDControlPanelProperties from .light_effects import LightEffectCollection @@ -43,11 +45,6 @@ class DroneShowAddonProperties(PropertyGroup): "Version 2 uses a shared material for all drones to speed up light effects." ), min=1, - max=2, + max=LATEST_SKYBRUSH_PLUGIN_VERSION, default=1, ) - - @property - def latest_version(self) -> int: - """Returns the latest plugin file format version available.""" - return self.__annotations__["version"].keywords["max"] diff --git a/src/modules/sbstudio/plugin/utils/warnings.py b/src/modules/sbstudio/plugin/utils/warnings.py index ba7a1b6c..456ea715 100644 --- a/src/modules/sbstudio/plugin/utils/warnings.py +++ b/src/modules/sbstudio/plugin/utils/warnings.py @@ -1,5 +1,6 @@ from bpy.types import Context +from sbstudio.plugin.constants import LATEST_SKYBRUSH_PLUGIN_VERSION from sbstudio.plugin.model.formation import count_markers_in_formation from sbstudio.plugin.stats import get_drone_count @@ -63,8 +64,8 @@ def draw_formation_size_warning(context: Context, layout) -> None: def draw_version_warning(context: Context, layout) -> None: """Draw a version warning to a layout, if needed.""" skybrush = context.scene.skybrush - if skybrush.version < skybrush.latest_version: + if skybrush.version < LATEST_SKYBRUSH_PLUGIN_VERSION: _draw_warning( layout, - text=f"File format is old (version {skybrush.version} < {skybrush.latest_version})", + text=f"File format is old (version {skybrush.version} < {LATEST_SKYBRUSH_PLUGIN_VERSION})", ) From 77a6a85da8761b6b7be13bb2bcce636ede27bf4d Mon Sep 17 00:00:00 2001 From: Gabor Vasarhelyi Date: Mon, 29 Sep 2025 15:08:33 +0200 Subject: [PATCH 14/14] fix: several small fixes based on review from @ntamas --- src/modules/sbstudio/plugin/operators/base.py | 5 ++++- .../migrations/use_common_material_for_all_drones.py | 12 +++++++++--- src/modules/sbstudio/plugin/utils/warnings.py | 11 +++-------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/modules/sbstudio/plugin/operators/base.py b/src/modules/sbstudio/plugin/operators/base.py index 65677a16..57baa79c 100644 --- a/src/modules/sbstudio/plugin/operators/base.py +++ b/src/modules/sbstudio/plugin/operators/base.py @@ -550,5 +550,8 @@ def initialize_migration(self) -> None: raise NotImplementedError def needs_migration(self) -> bool: - """Returns whether the current Blender content needs migration.""" + """Returns whether the current Blender content needs migration. + + Note that return value is checked based on actual content, + irrespective of the current plugin version.""" raise NotImplementedError diff --git a/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py b/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py index ccd4b64f..2327c1fc 100644 --- a/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py +++ b/src/modules/sbstudio/plugin/operators/migrations/use_common_material_for_all_drones.py @@ -55,7 +55,7 @@ def upgrade_drone_color_animations_and_drone_materials() -> None: last_log = time() for i, drone in enumerate(drones.objects): material = get_material_for_led_light_color(drone) - if material and material.name == f"LED color of {drone.name}": + if material: # copy color animation from shader node to drone node_tree = material.node_tree if node_tree.animation_data: @@ -78,7 +78,10 @@ def upgrade_drone_color_animations_and_drone_materials() -> None: def needs_migration(): - """Returns whether the current Blender content needs migration.""" + """Returns whether the current Blender content needs migration. + + Note that return value is checked based on actual content, + irrespective of the current plugin version.""" # TODO: what should be the optimal method to check if file # needs migration or not? We check the template material now # as it is most probably not modified by the users frequently @@ -121,7 +124,10 @@ def initialize_migration(self) -> None: self.version_to = 2 def needs_migration(self) -> bool: - """Returns whether the current Blender content needs migration.""" + """Returns whether the current Blender content needs migration. + + Note that return value is checked based on actual content, + irrespective of the current plugin version.""" return needs_migration() def execute_migration(self, context): diff --git a/src/modules/sbstudio/plugin/utils/warnings.py b/src/modules/sbstudio/plugin/utils/warnings.py index 456ea715..1b098230 100644 --- a/src/modules/sbstudio/plugin/utils/warnings.py +++ b/src/modules/sbstudio/plugin/utils/warnings.py @@ -19,17 +19,12 @@ def _draw_warning(layout, text: str) -> None: def draw_bad_shader_color_source_warning(context: Context, layout) -> None: """Draw a bad shader color source warning to a layout, if needed.""" + shading = context.space_data.shading label = ( "Set shader Wireframe Color to 'OBJECT'" - if ( - context.space_data.shading.type == "WIREFRAME" - and context.space_data.shading.wireframe_color_type != "OBJECT" - ) + if (shading.type == "WIREFRAME" and shading.wireframe_color_type != "OBJECT") else "Set shader Object Color to 'OBJECT'" - if ( - context.space_data.shading.type == "SOLID" - and context.space_data.shading.color_type != "OBJECT" - ) + if (shading.type == "SOLID" and shading.color_type != "OBJECT") else None ) if label: