Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions src/bonsai/bonsai/tool/blender.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,16 @@ class Blender(bonsai.core.tool.Blender):

@classmethod
def activate_camera(cls, obj: bpy.types.Object) -> None:

area = cls.get_view3d_area()
assert area
assert isinstance((space := area.spaces[0]), bpy.types.SpaceView3D)
assert isinstance((space := area.spaces.active), bpy.types.SpaceView3D)
is_local_view = space.local_view is not None

assert bpy.context.screen and bpy.context.scene
previous_camera = bpy.context.scene.camera
if previous_camera and previous_camera != obj:
# Other camera views would otherwise follow the global scene camera change.
cls.pin_scene_camera_to_other_viewports(previous_camera, space)
if is_local_view:
# Turn off local view before activating drawing, and then turn it on again.
for a in bpy.context.screen.areas:
Expand Down Expand Up @@ -373,12 +376,40 @@ def message_ui(self, context):

@classmethod
def get_view3d_area(cls) -> Union[bpy.types.Area, None]:
if (area := bpy.context.area) and area.type == "VIEW_3D":
return area
assert (wm := bpy.context.window_manager)
for window in wm.windows:
for area in window.screen.areas:
if area.type == "VIEW_3D":
return area

@classmethod
def get_view3d_spaces(cls) -> Iterator[bpy.types.SpaceView3D]:
assert (wm := bpy.context.window_manager)
scene = bpy.context.scene
for window in wm.windows:
if window.scene != scene:
continue
for area in window.screen.areas:
if area.type != "VIEW_3D":
continue
space = area.spaces.active
assert isinstance(space, bpy.types.SpaceView3D)
yield space

@classmethod
def pin_scene_camera_to_other_viewports(
cls, camera: bpy.types.Object, active_space: bpy.types.SpaceView3D
) -> None:
for space in cls.get_view3d_spaces():
if space == active_space or space.use_local_camera:
continue
if not (region_3d := space.region_3d) or region_3d.view_perspective != "CAMERA":
continue
space.camera = camera
space.use_local_camera = True

@classmethod
def operator_idname_to_py(cls, idname: str) -> str:
"""Convert a Blender internal operator idname to its Python equivalent.
Expand Down
60 changes: 60 additions & 0 deletions src/bonsai/test/tool/test_blender.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
# This file was modified with the assistance of an AI coding tool.

import tempfile
import types
from pathlib import Path
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -60,6 +61,65 @@ def test_returns_new_list_instance(self):
assert result is not original


class TestPinSceneCameraToOtherViewports(NewFile):
def test_camera_activation_pins_the_previous_camera_before_switching_scene_camera(self, monkeypatch):
assert bpy.context.scene
previous_camera = bpy.data.objects.new("Previous Camera", bpy.data.cameras.new("Previous Camera"))
new_camera = bpy.data.objects.new("New Camera", bpy.data.cameras.new("New Camera"))
bpy.context.scene.collection.objects.link(previous_camera)
bpy.context.scene.collection.objects.link(new_camera)
bpy.context.scene.camera = previous_camera
calls = []
monkeypatch.setattr(
subject,
"pin_scene_camera_to_other_viewports",
classmethod(lambda cls, camera, active_space: calls.append((camera, active_space))),
)

subject.activate_camera(new_camera)
subject.activate_camera(new_camera)

assert bpy.context.scene.camera == new_camera
assert calls == [(previous_camera, subject.get_view3d_space())]

def test_pins_only_other_camera_views_that_follow_the_scene_camera(self, monkeypatch):
previous_camera = object()
active_space = types.SimpleNamespace(
camera=None,
region_3d=types.SimpleNamespace(view_perspective="CAMERA"),
use_local_camera=False,
)
following_space = types.SimpleNamespace(
camera=None,
region_3d=types.SimpleNamespace(view_perspective="CAMERA"),
use_local_camera=False,
)
local_camera = object()
local_space = types.SimpleNamespace(
camera=local_camera,
region_3d=types.SimpleNamespace(view_perspective="CAMERA"),
use_local_camera=True,
)
perspective_space = types.SimpleNamespace(
camera=None,
region_3d=types.SimpleNamespace(view_perspective="PERSP"),
use_local_camera=False,
)
spaces = [active_space, following_space, local_space, perspective_space]
monkeypatch.setattr(subject, "get_view3d_spaces", classmethod(lambda cls: iter(spaces)))

subject.pin_scene_camera_to_other_viewports(previous_camera, active_space)

assert active_space.camera is None
assert active_space.use_local_camera is False
assert following_space.camera is previous_camera
assert following_space.use_local_camera is True
assert local_space.camera is local_camera
assert local_space.use_local_camera is True
assert perspective_space.camera is None
assert perspective_space.use_local_camera is False


class TestViewportDecoratorDrawBatch(NewFile):
def test_empty_content_pos_skips_shader_calls(self):
from unittest.mock import MagicMock
Expand Down