Shot Viewer: camera markers in a sidebar panel

Shots are now timeline markers bound to a camera instead of sequencer
scene strips, all in one scene. The Shot Viewer is a fixed panel in the
WTD tab instead of a separate window: choosing a shot moves the playhead
and puts the current 3D view in its camera. Adding a shot creates a
marker and camera at the end of the scene, deleting removes the marker
(and optionally the camera). Loop Shot is now off by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nero Frame 13andClaude Opus 5.5 committed 2026-10-20 15:56:28 +02:00
1 parent a83bc959d0
commit c376a92591
3 files changed
+171 -499

No files matched your search

+115 -417
View File
@@ -1,8 +1,9 @@
"""Shot viewer: a window showing the shots of the edit through their camera.
"""Shot viewer: switch, add and delete the shots of the scene from the sidebar.
Shots are the scene strips of the edit scene's sequencer. The viewer is a
separate window showing the scene of a strip through the strip's camera, with
a sidebar panel to switch shots and quickly select the character / prop rigs.
Shots are the timeline markers bound to a camera (Ctrl+B in the timeline). A
shot runs from its marker to the frame before the next camera marker, the last
one to the scene end. Showing a shot puts the 3D view the panel is used from
in that shot's camera view. The panel also selects the character / prop rigs.
"""
import os
@@ -10,100 +11,51 @@ import re
import bpy
from bpy.app.handlers import persistent
from bpy.props import BoolProperty, EnumProperty, IntProperty, PointerProperty, StringProperty
from bpy.props import BoolProperty, IntProperty, PointerProperty, StringProperty
from mathutils import Euler, Vector
from . import config
# Custom property marking the screen of a viewer window (saved with the file).
VIEWER_TAG = "wtd_shot_viewer"
VIEWER_WORKSPACE = "Shot Viewer"
# Height of the edit sequencer at the bottom of the viewer, as a part of the window.
VIEWER_TIMELINE_FACTOR = 0.25
OTHER_GROUP = "Other"
# --------------------------------------------------------------------------- #
# Shots
# --------------------------------------------------------------------------- #
def has_shots(scene):
editor = scene.sequence_editor
return editor is not None and any(strip.type == "SCENE" for strip in editor.strips_all)
def edit_scene(context):
"""The scene holding the edit: the one set in the settings, else the first with scene strips."""
settings = context.window_manager.wtd_shot_viewer
if settings.edit_scene is not None:
return settings.edit_scene
return next((scene for scene in bpy.data.scenes if has_shots(scene)), None)
def strip_range(strip):
"""(first frame, last frame + 1) of the strip in the edit."""
if hasattr(strip, "left_handle"): # Blender 5.1+ names, the old ones go in 6.0
return int(strip.left_handle), int(strip.right_handle)
return int(strip.frame_final_start), int(strip.frame_final_end)
def _content_start(strip):
return strip.content_start if hasattr(strip, "content_start") else strip.frame_start
def shots(scene):
"""Scene strips of the edit in edit order (muted strips and strips without a scene left out)."""
if scene is None or scene.sequence_editor is None:
"""Camera markers of the scene, in time order."""
if scene is None:
return []
strips = [
strip
for strip in scene.sequence_editor.strips_all
if strip.type == "SCENE" and strip.scene is not None and not strip.mute
]
return sorted(strips, key=lambda strip: (strip_range(strip)[0], strip.channel))
return sorted((m for m in scene.timeline_markers if m.camera is not None), key=lambda m: m.frame)
def shot_range(marker):
"""(first, last) frame of the shot: up to the next camera marker, the last shot up to the scene end."""
scene = marker.id_data
later = [m.frame for m in shots(scene) if m.frame > marker.frame]
end = min(later) - 1 if later else max(marker.frame, scene.frame_end)
return marker.frame, end
def shot_at(scene, frame):
"""The shot showing at the frame (the last camera marker at or before it), or None."""
return next((m for m in reversed(shots(scene)) if m.frame <= frame), None)
def current_shot(context):
"""The shot selected in the viewer, or None."""
scene = edit_scene(context)
if scene is None or scene.sequence_editor is None:
return None
strips = scene.sequence_editor.strips_all
index = context.window_manager.wtd_shot_viewer.shot_index
if not 0 <= index < len(strips):
return None
strip = strips[index]
return strip if strip in shots(scene) else None
return shot_at(context.scene, context.scene.frame_current)
def shot_camera(strip):
return strip.scene_camera or strip.scene.camera
def _marker_index(marker):
return list(marker.id_data.timeline_markers).index(marker)
def shot_frame_range(strip):
"""Frame range of the strip in its own scene (the strip content starts at the scene start)."""
start, end = strip_range(strip)
offset = strip.scene.frame_start - int(_content_start(strip))
return start + offset, end - 1 + offset
def is_viewer(screen):
return screen is not None and bool(screen.get(VIEWER_TAG))
def viewer_areas(context):
for window in context.window_manager.windows:
if is_viewer(window.screen):
for area in window.screen.areas:
if area.type == "VIEW_3D":
yield window, area
def apply_loop(context, strip):
"""With Loop Shot on, set the preview range of the shot scene to the shot so playback loops on it."""
def apply_loop(context, marker):
"""With Loop Shot on, set the preview range to the shot so playback loops on it."""
if not context.window_manager.wtd_shot_viewer.loop_shot:
return
scene = strip.scene
start, end = shot_frame_range(strip)
scene = marker.id_data
start, end = shot_range(marker)
scene.use_preview_range = True
# Setting start past the old end (or end before the old start) pushes the other one.
scene.frame_preview_start = start
@@ -111,33 +63,19 @@ def apply_loop(context, strip):
scene.frame_preview_start = start
def show_shot(context, strip):
"""Show the shot in every viewer window and move the edit playhead to it."""
scene = strip.scene
camera = shot_camera(strip)
start, end = shot_frame_range(strip)
scene.frame_current = start
apply_loop(context, strip)
strip.id_data.frame_current = strip_range(strip)[0]
for window, area in viewer_areas(context):
window.scene = scene
space = area.spaces.active
space.use_local_camera = camera is not None
space.camera = camera
def show_shot(context, marker):
"""Jump to the shot and look through its camera in the current 3D view."""
scene = marker.id_data
# The camera markers switch the camera on frame change; set it right away.
scene.camera = marker.camera
scene.frame_current = marker.frame
# Item assignment skips the update callback, which would call this again.
context.window_manager.wtd_shot_viewer["shot_index"] = _marker_index(marker)
apply_loop(context, marker)
space = context.space_data
if space is not None and space.type == "VIEW_3D":
space.use_local_camera = False
space.region_3d.view_perspective = "CAMERA"
area.tag_redraw()
def shot_at(edit, frame):
"""The shot under the frame of the edit (top channel first), or None."""
covering = [strip for strip in shots(edit) if strip_range(strip)[0] <= frame < strip_range(strip)[1]]
return max(covering, key=lambda strip: strip.channel, default=None)
def _shot_frame(strip, edit_frame):
"""Frame of the strip's scene shown at a frame of the edit."""
return edit_frame + shot_frame_range(strip)[0] - strip_range(strip)[0]
_syncing = False
@@ -145,93 +83,35 @@ _syncing = False
@persistent
def _on_frame_change(scene, _depsgraph=None):
"""Keep the shot list and the edit playhead in step with the viewer.
Scrubbing the edit selects the shot under the playhead (the workspace time
sync shows it). Playing a shot in the 3D view moves the edit playhead along.
"""
"""Keep the shot list (and the loop range) on the shot under the playhead."""
global _syncing
context = bpy.context
wm = context.window_manager
if _syncing or wm is None or not any(is_viewer(window.screen) for window in wm.windows):
if _syncing or wm is None:
return
marker = shot_at(scene, scene.frame_current)
settings = wm.wtd_shot_viewer
edit = settings.edit_scene
if edit is None or edit.sequence_editor is None:
if marker is None or _marker_index(marker) == settings.shot_index:
return
_syncing = True
try:
# Edit scrubbed (or played): the shot under the playhead, already shown
# by the workspace time sync, becomes the selected one.
under = shot_at(edit, edit.frame_current)
if under is not None and (scene == edit or _shot_frame(under, edit.frame_current) == scene.frame_current):
index = list(edit.sequence_editor.strips_all).index(under)
if index != settings.shot_index:
settings["shot_index"] = index
apply_loop(context, under)
else:
# Shot played in the 3D view: the edit playhead follows.
strip = current_shot(context)
if strip is not None and strip.scene == scene:
start, end = strip_range(strip)
frame = start + scene.frame_current - shot_frame_range(strip)[0]
edit.frame_current = max(start, min(end - 1, frame))
for _window, area in viewer_areas(context):
area.tag_redraw()
settings["shot_index"] = _marker_index(marker)
apply_loop(context, marker)
finally:
_syncing = False
def select_shot(context, strip):
"""Make the strip the selected shot and show it."""
index = list(strip.id_data.sequence_editor.strips_all).index(strip)
# Item assignment skips the update callback: the shot is shown just below,
# also when the index does not change (e.g. after a deletion).
context.window_manager.wtd_shot_viewer["shot_index"] = index
show_shot(context, strip)
def next_shot_name(edit):
"""Next free shot name (config SHOT_NAME_FORMAT), after the highest one of the strips and scenes."""
def next_shot_name(scene):
"""Next free shot name (config SHOT_NAME_FORMAT), after the highest one of the markers."""
head, _sep, rest = config.SHOT_NAME_FORMAT.partition("{")
pattern = re.compile(re.escape(head) + r"(\d+)" + re.escape(rest.partition("}")[2]))
names = [scene.name for scene in bpy.data.scenes]
if edit.sequence_editor is not None:
names += [strip.name for strip in edit.sequence_editor.strips_all]
numbers = [int(match.group(1)) for name in names if (match := pattern.search(name))]
numbers = [int(match.group(1)) for m in scene.timeline_markers if (match := pattern.search(m.name))]
step = config.SHOT_NUMBER_STEP
return config.SHOT_NAME_FORMAT.format((max(numbers, default=0) // step + 1) * step)
def _new_shot_scene(name, edit, source, duration):
"""New shot scene: a linked copy of ``source`` (same collections) or, without source, an empty
scene with the format of the edit."""
if source is not None:
scene = source.copy()
scene.name = name
scene.sequence_editor_clear()
# The cameras of the source shot are not shared.
for ob in [ob for ob in scene.collection.objects if ob.type == "CAMERA"]:
scene.collection.objects.unlink(ob)
else:
scene = bpy.data.scenes.new(name)
for attr in (
"fps",
"fps_base",
"resolution_x",
"resolution_y",
"resolution_percentage",
"pixel_aspect_x",
"pixel_aspect_y",
):
setattr(scene.render, attr, getattr(edit.render, attr))
scene.frame_start = config.SHOT_FRAME_START
scene.frame_end = config.SHOT_FRAME_START + duration - 1
return scene
def _new_camera(scene, name, source=None):
"""New camera in the scene's own collection, copying ``source`` (lens and placement) when given."""
"""New camera in the scene collection, copying ``source`` (lens and placement) when given."""
camera_name = config.OBJECT_PREFIXES.get("CAMERA", "") + name
if source is not None and source.type == "CAMERA":
data = source.data.copy()
@@ -246,32 +126,9 @@ def _new_camera(scene, name, source=None):
camera.location = Vector((0.0, -10.0, 1.6))
camera.rotation_euler = Euler((1.5708, 0.0, 0.0))
scene.collection.objects.link(camera)
if scene.camera is None:
scene.camera = camera
return camera
def _viewed_camera(context):
space = context.space_data
if space is not None and space.type == "VIEW_3D" and space.use_local_camera and space.camera:
return space.camera
return context.scene.camera
def _move(strip, delta):
if hasattr(strip, "content_start"): # Blender 5.1+
strip.content_start += delta
else:
strip.frame_start += delta
def _close_gap(editor, start, end):
"""Move the strips starting after a removed one back by its length (all channels)."""
later = [strip for strip in editor.strips if strip_range(strip)[0] >= end]
for strip in sorted(later, key=lambda strip: strip_range(strip)[0]):
_move(strip, start - end)
# --------------------------------------------------------------------------- #
# Rigs
# --------------------------------------------------------------------------- #
@@ -305,206 +162,65 @@ def rig_groups(view_layer):
# Settings / operators
# --------------------------------------------------------------------------- #
def _shot_index_update(self, context):
scene = edit_scene(context)
if scene is None or not 0 <= self.shot_index < len(scene.sequence_editor.strips_all):
return
strip = scene.sequence_editor.strips_all[self.shot_index]
if strip.type == "SCENE" and strip.scene is not None:
show_shot(context, strip)
markers = context.scene.timeline_markers
if 0 <= self.shot_index < len(markers) and markers[self.shot_index].camera is not None:
show_shot(context, markers[self.shot_index])
def _loop_shot_update(self, context):
marker = current_shot(context)
if self.loop_shot:
strip = current_shot(context)
if strip is not None:
apply_loop(context, strip)
if marker is not None:
apply_loop(context, marker)
else:
for strip in shots(edit_scene(context)):
strip.scene.use_preview_range = False
context.scene.use_preview_range = False
class WTD_ShotViewerSettings(bpy.types.PropertyGroup):
edit_scene: PointerProperty(
name="Edit",
description="Scene whose sequencer holds the shots (scene strips)",
type=bpy.types.Scene,
)
shot_index: IntProperty(name="Shot", update=_shot_index_update)
loop_shot: BoolProperty(
name="Loop Shot",
description="Set the preview range of the shot scene to the shot, so playback loops on it",
default=True,
description="Set the preview range to the shot under the playhead, so playback loops on it",
default=False,
update=_loop_shot_update,
)
def _setup_viewer_screen(window, edit):
"""Lay out the viewer screen: the 3D view in camera view, the edit's sequencer at the bottom.
Returns False while the window is not ready yet (its sidebar is not drawn)."""
screen = window.screen
view3d = max((a for a in screen.areas if a.type == "VIEW_3D"), key=lambda a: a.width * a.height, default=None)
if view3d is None:
view3d = max(screen.areas, key=lambda a: a.width * a.height)
view3d.type = "VIEW_3D"
keep = {view3d.as_pointer()}
sequencer = next((a for a in screen.areas if a.type == "SEQUENCE_EDITOR"), None)
if sequencer is not None:
keep.add(sequencer.as_pointer())
for other in [a for a in screen.areas if a.as_pointer() not in keep]:
with bpy.context.temp_override(window=window, screen=screen, area=other):
bpy.ops.screen.area_close()
if sequencer is None:
with bpy.context.temp_override(window=window, screen=screen, area=view3d):
bpy.ops.screen.area_split(direction="HORIZONTAL", factor=VIEWER_TIMELINE_FACTOR)
sequencer = min(screen.areas, key=lambda a: a.y)
sequencer.type = "SEQUENCE_EDITOR"
view3d = max(screen.areas, key=lambda a: a.y)
sequencer.spaces.active.view_type = "SEQUENCER"
sequencer.spaces.active.show_seconds = False # frames, like the shot list
screen[VIEWER_TAG] = True
view3d.spaces.active.show_region_ui = True
try:
ui = next(r for r in view3d.regions if r.type == "UI")
ui.active_panel_category = "WTD"
except (AttributeError, StopIteration, TypeError):
return False
strip = current_shot(bpy.context)
if strip is not None:
show_shot(bpy.context, strip)
if view3d.spaces.active.region_3d.view_perspective == "CAMERA":
region = next(r for r in view3d.regions if r.type == "WINDOW")
with bpy.context.temp_override(window=window, screen=screen, area=view3d, region=region):
bpy.ops.view3d.view_center_camera()
region = next(r for r in sequencer.regions if r.type == "WINDOW")
with bpy.context.temp_override(window=window, screen=screen, area=sequencer, region=region):
try:
bpy.ops.sequencer.view_all()
except RuntimeError: # framing only, the sequencer may not be drawn yet
pass
return True
def _finish_viewer(window, previous_workspace, edit, tries=30):
"""Finish the viewer once Blender has switched its workspace (done on the next event loop)."""
def finish():
nonlocal tries
tries -= 1
try:
workspace = window.workspace
if workspace.as_pointer() == previous_workspace:
return 0.1 if tries > 0 else None
workspace.name = VIEWER_WORKSPACE
workspace.sequencer_scene = edit
# Scrubbing the edit in the sequencer shows the shot under the playhead.
workspace.use_scene_time_sync = True
ready = _setup_viewer_screen(window, edit)
except ReferenceError: # window closed meanwhile
return None
return None if ready or tries <= 0 else 0.1
bpy.app.timers.register(finish, first_interval=0.1)
class WTD_OT_shot_viewer_open(bpy.types.Operator):
bl_idname = "wtd_tools.shot_viewer_open"
bl_label = "Shot Viewer"
bl_description = "Open a window showing the shots of the edit through their camera, with the edit below"
@classmethod
def poll(cls, context):
return context.area is not None
def execute(self, context):
# Without shots anywhere, the current scene becomes the edit.
scene = edit_scene(context) or context.scene
scene.sequence_editor_create()
settings = context.window_manager.wtd_shot_viewer
settings.edit_scene = scene
# A main window, not a child one (screen.area_dupli): child windows
# always show the scene of their parent.
windows = context.window_manager.windows
before = {window.as_pointer() for window in windows}
bpy.ops.wm.window_new_main()
window = next((window for window in windows if window.as_pointer() not in before), None)
if window is None:
self.report({"ERROR"}, "Could not open the viewer window")
return {"CANCELLED"}
# The viewer gets its own workspace: the scene time sync is a workspace
# setting, and the window's current layout must stay untouched as other
# windows may reuse it. Both switches happen on the next event loop.
previous = window.workspace.as_pointer()
workspace = bpy.data.workspaces.get(VIEWER_WORKSPACE)
if workspace is not None:
window.workspace = workspace
else:
with context.temp_override(window=window, screen=window.screen, area=window.screen.areas[0]):
bpy.ops.workspace.duplicate()
# Start on the shot under the edit playhead.
ordered = shots(scene)
strip = next(
(s for s in ordered if strip_range(s)[0] <= scene.frame_current < strip_range(s)[1]),
ordered[0] if ordered else None,
)
if strip is not None:
index = list(scene.sequence_editor.strips_all).index(strip)
settings["shot_index"] = index
_finish_viewer(window, previous, scene)
return {"FINISHED"}
class WTD_OT_shot_viewer_step(bpy.types.Operator):
bl_idname = "wtd_tools.shot_viewer_step"
bl_label = "Previous / Next Shot"
bl_description = "Show the previous or next shot of the edit"
bl_description = "Show the previous or next shot"
step: IntProperty(default=1)
def execute(self, context):
ordered = shots(edit_scene(context))
ordered = shots(context.scene)
if not ordered:
return {"CANCELLED"}
current = current_shot(context)
position = ordered.index(current) if current is not None else -1
position = max(0, min(len(ordered) - 1, position + self.step))
select_shot(context, ordered[position])
show_shot(context, ordered[position])
return {"FINISHED"}
class WTD_OT_shot_viewer_new_shot(bpy.types.Operator):
bl_idname = "wtd_tools.shot_viewer_new_shot"
bl_label = "New Shot"
bl_description = "Add a shot (scene strip) with its own camera at the end of the edit"
bl_description = "Add a shot (camera marker) at the end of the scene"
bl_options = {"REGISTER", "UNDO"}
name: StringProperty(name="Name", description="Name of the strip and of the new scene")
scene_mode: EnumProperty(
name="Scene",
items=[
("SHARED", "New, Shared Content", "New scene using the same collections as the scene shown in the viewer (linked copy)"),
("EMPTY", "New, Empty", "New empty scene with the format of the edit"),
("EXISTING", "Existing", "Use an existing scene, for its whole frame range"),
],
)
scene_name: StringProperty(name="Scene Name")
name: StringProperty(name="Name", description="Name of the marker")
duration: IntProperty(name="Duration", description="Length of the shot in frames", min=1, default=48)
new_camera: BoolProperty(
name="New Camera",
description="Add a camera for this shot (otherwise the scene camera is used)",
description="Add a camera for this shot, copying the current one. Otherwise pick an existing camera",
default=True,
)
@classmethod
def poll(cls, context):
return edit_scene(context) is not None
camera_name: StringProperty(name="Camera")
def invoke(self, context, event):
self.name = next_shot_name(edit_scene(context))
self.name = next_shot_name(context.scene)
self.duration = config.SHOT_DEFAULT_DURATION
return context.window_manager.invoke_props_dialog(self)
@@ -513,53 +229,45 @@ class WTD_OT_shot_viewer_new_shot(bpy.types.Operator):
layout.use_property_split = True
layout.use_property_decorate = False
layout.prop(self, "name")
layout.prop(self, "scene_mode")
if self.scene_mode == "EXISTING":
layout.prop_search(self, "scene_name", bpy.data, "scenes")
layout.prop(self, "new_camera")
else:
layout.prop(self, "duration")
layout.prop(self, "duration")
layout.prop(self, "new_camera")
if not self.new_camera:
layout.prop_search(self, "camera_name", context.scene, "objects", icon="CAMERA_DATA")
def execute(self, context):
edit = edit_scene(context)
name = self.name.strip() or next_shot_name(edit)
source_camera = _viewed_camera(context)
if self.scene_mode == "EXISTING":
scene = bpy.data.scenes.get(self.scene_name)
if scene is None or scene == edit:
self.report({"WARNING"}, "Pick an existing scene other than the edit")
return {"CANCELLED"}
scene = context.scene
name = self.name.strip() or next_shot_name(scene)
if self.new_camera:
camera = _new_camera(scene, name, scene.camera)
else:
source = context.scene if self.scene_mode == "SHARED" else None
scene = _new_shot_scene(name, edit, source, self.duration)
camera = None
if self.scene_mode != "EXISTING" or self.new_camera or scene.camera is None:
camera = _new_camera(scene, name, source_camera)
camera = scene.objects.get(self.camera_name)
if camera is None or camera.type != "CAMERA":
self.report({"WARNING"}, "Pick a camera of the scene, or tick New Camera")
return {"CANCELLED"}
editor = edit.sequence_editor_create()
start = max((strip_range(strip)[1] for strip in editor.strips), default=edit.frame_start)
strip = editor.strips.new_scene(name, scene, channel=1, frame_start=start)
if camera is not None:
strip.scene_camera = camera
edit.frame_end = max(edit.frame_end, strip_range(strip)[1] - 1)
select_shot(context, strip)
self.report({"INFO"}, f"Shot '{strip.name}' added (scene '{scene.name}')")
ordered = shots(scene)
if ordered:
start = shot_range(ordered[-1])[1] + 1
else:
start = config.SHOT_FRAME_START
scene.frame_start = start
marker = scene.timeline_markers.new(name, frame=start)
marker.camera = camera
scene.frame_end = start + self.duration - 1
show_shot(context, marker)
self.report({"INFO"}, f"Shot '{marker.name}' added at frame {start} ({camera.name})")
return {"FINISHED"}
class WTD_OT_shot_viewer_delete_shot(bpy.types.Operator):
bl_idname = "wtd_tools.shot_viewer_delete_shot"
bl_label = "Delete Shot"
bl_description = "Remove the selected shot from the edit"
bl_description = "Remove the shot under the playhead (its camera marker)"
bl_options = {"REGISTER", "UNDO"}
close_gap: BoolProperty(name="Close Gap", description="Move the following strips back", default=True)
delete_camera: BoolProperty(
name="Delete Camera", description="Also delete its camera when no other shot uses it"
)
delete_scene: BoolProperty(
name="Delete Scene", description="Also delete its scene when no other shot uses it"
)
@classmethod
def poll(cls, context):
@@ -570,46 +278,37 @@ class WTD_OT_shot_viewer_delete_shot(bpy.types.Operator):
def draw(self, context):
layout = self.layout
layout.label(text=f"Delete shot '{current_shot(context).name}'?", icon="ERROR")
layout.prop(self, "close_gap")
marker = current_shot(context)
layout.label(text=f"Delete shot '{marker.name}'?", icon="ERROR")
if marker != shots(context.scene)[-1]:
layout.label(text="Its frames go to the previous shot, the animation is not moved.")
layout.prop(self, "delete_camera")
layout.prop(self, "delete_scene")
def execute(self, context):
strip = current_shot(context)
edit = strip.id_data
editor = edit.sequence_editor
name, scene, camera = strip.name, strip.scene, shot_camera(strip)
position = shots(edit).index(strip)
start, end = strip_range(strip)
scene = context.scene
marker = current_shot(context)
ordered = shots(scene)
position = ordered.index(marker)
name, camera = marker.name, marker.camera
start = marker.frame
is_last = position == len(ordered) - 1
editor.strips.remove(strip)
if self.close_gap:
_close_gap(editor, start, end)
remaining = shots(edit)
if remaining:
select_shot(context, remaining[min(position, len(remaining) - 1)])
else:
for window, _area in viewer_areas(context):
window.scene = edit
others = [s for s in editor.strips_all if s.type == "SCENE" and s.scene is not None]
deleted = []
if self.delete_camera and camera is not None and not any(shot_camera(s) == camera for s in others):
scene.timeline_markers.remove(marker)
if is_last and position > 0:
# The frames were only this shot's.
scene.frame_end = start - 1
deleted = ""
if self.delete_camera and not any(m.camera == camera for m in scene.timeline_markers):
data = camera.data
bpy.data.objects.remove(camera)
if data.users == 0:
bpy.data.cameras.remove(data)
deleted.append("camera")
if self.delete_scene and scene != edit and not any(s.scene == scene for s in others):
for window in context.window_manager.windows:
if window.scene == scene:
window.scene = edit
bpy.data.scenes.remove(scene)
deleted.append("scene")
extra = f" with its {' and '.join(deleted)}" if deleted else ""
self.report({"INFO"}, f"Shot '{name}' deleted{extra}")
deleted = " with its camera"
remaining = shots(scene)
if remaining:
show_shot(context, remaining[min(position, len(remaining) - 1)])
self.report({"INFO"}, f"Shot '{name}' deleted{deleted}")
return {"FINISHED"}
@@ -648,7 +347,6 @@ class WTD_OT_shot_viewer_select_rig(bpy.types.Operator):
classes = (
WTD_ShotViewerSettings,
WTD_OT_shot_viewer_open,
WTD_OT_shot_viewer_step,
WTD_OT_shot_viewer_new_shot,
WTD_OT_shot_viewer_delete_shot,