diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index bd5a45f9b8..aa9656c76e 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -538,6 +538,19 @@ class Blender(bonsai.core.tool.Blender): cls.handlers.clear() cls.is_installed = False + def draw_batch(self, shader_type, content_pos, color, indices=None): + """Submit a GPU batch through ``self.line_shader`` (for ``"LINES"``) + or ``self.shader`` (for any other primitive). Skips empty batches + via ``validate_shader_batch_data`` so Blender 4.4+ doesn't crash on + empty ``indices``. Subclasses bind both shaders in their draw method + before calling this helper.""" + if not Blender.validate_shader_batch_data(content_pos, indices): + return + shader = self.line_shader if shader_type == "LINES" else self.shader + batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices) + shader.uniform_float("color", color) + batch.draw(shader) + @staticmethod def _lookup_active_instance(gizmo_cls: type, context: bpy.types.Context) -> Optional[Any]: """Return the live ``GizmoGroup`` instance registered under @@ -2391,6 +2404,13 @@ class Blender(bonsai.core.tool.Blender): return False return True + @staticmethod + def transparent_color(color: Iterable[float], alpha: float = 0.1) -> list[float]: + """Copy an RGBA color with its alpha channel overridden.""" + out = [c for c in color] + out[3] = alpha + return out + @classmethod def draw_bmesh_face_tris( cls, diff --git a/src/bonsai/test/tool/test_blender.py b/src/bonsai/test/tool/test_blender.py index e532bacc02..5f96886e97 100644 --- a/src/bonsai/test/tool/test_blender.py +++ b/src/bonsai/test/tool/test_blender.py @@ -42,6 +42,46 @@ class TestImplementsTool(NewFile): assert isinstance(subject(), bonsai.core.tool.Blender) +class TestTransparentColor(NewFile): + def test_default_alpha_overrides_to_zero_one(self): + assert subject.transparent_color([1.0, 0.5, 0.25, 1.0]) == [1.0, 0.5, 0.25, 0.1] + + def test_explicit_alpha_is_applied(self): + assert subject.transparent_color([1.0, 0.5, 0.25, 1.0], alpha=0.5) == [1.0, 0.5, 0.25, 0.5] + + def test_does_not_mutate_input(self): + original = [1.0, 0.5, 0.25, 1.0] + subject.transparent_color(original) + assert original == [1.0, 0.5, 0.25, 1.0] + + def test_returns_new_list_instance(self): + original = [1.0, 0.5, 0.25, 1.0] + result = subject.transparent_color(original) + assert result is not original + + +class TestViewportDecoratorDrawBatch(NewFile): + def test_empty_content_pos_skips_shader_calls(self): + from unittest.mock import MagicMock + + decorator = subject.ViewportDecorator() + decorator.line_shader = MagicMock() + decorator.shader = MagicMock() + decorator.draw_batch("LINES", [], (1.0, 1.0, 1.0, 1.0)) + decorator.line_shader.uniform_float.assert_not_called() + decorator.shader.uniform_float.assert_not_called() + + def test_empty_indices_skips_shader_calls(self): + from unittest.mock import MagicMock + + decorator = subject.ViewportDecorator() + decorator.line_shader = MagicMock() + decorator.shader = MagicMock() + decorator.draw_batch("LINES", [(0.0, 0.0, 0.0), (1.0, 0.0, 0.0)], (1.0, 1.0, 1.0, 1.0), indices=[]) + decorator.line_shader.uniform_float.assert_not_called() + decorator.shader.uniform_float.assert_not_called() + + class TestCopyNodeGraph(NewFile): def test_run(self): material_to = bpy.data.materials.new("material_to")