Added functionality in Shot Manager to create all cameras for a Kitsu Sequence
This commit is contained in:
1 parent
4161ec4ad5
commit
8dcd4daad1
3 files changed
+271
-18
No files matched your search
+242
-18
@@ -3,7 +3,8 @@
|
||||
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.
|
||||
in that shot's camera view. The panel also selects the character / prop rigs,
|
||||
and builds the shots of a Kitsu sequence (Libreflow, through the Scene Manager).
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -11,9 +12,11 @@ import re
|
||||
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
from bpy.props import BoolProperty, IntProperty, PointerProperty, StringProperty
|
||||
from bpy.props import BoolProperty, EnumProperty, IntProperty, PointerProperty, StringProperty
|
||||
|
||||
from . import config
|
||||
from .scene_manager import ipc
|
||||
from .scene_manager import operators as sm
|
||||
|
||||
OTHER_GROUP = "Other"
|
||||
|
||||
@@ -113,10 +116,17 @@ def sequence_name(scene):
|
||||
return config.SEQUENCE_NAME_FORMAT.format(scene.wtd_sequence)
|
||||
|
||||
|
||||
def camera_name(scene, shot):
|
||||
"""Name of a new shot camera (config SHOT_CAMERA_NAME_FORMAT)."""
|
||||
def sequence_number(name):
|
||||
"""Number of a sequence name written with SEQUENCE_NAME_FORMAT, else None ("sq0040" -> 40)."""
|
||||
head, _sep, rest = config.SEQUENCE_NAME_FORMAT.partition("{")
|
||||
match = re.fullmatch(re.escape(head) + r"(\d+)" + re.escape(rest.partition("}")[2]), name)
|
||||
return int(match.group(1)) if match else None
|
||||
|
||||
|
||||
def camera_name(scene, shot, sequence=None):
|
||||
"""Name of a new shot camera (config SHOT_CAMERA_NAME_FORMAT), in the scene's sequence by default."""
|
||||
return config.SHOT_CAMERA_NAME_FORMAT.format(
|
||||
prefix=config.OBJECT_PREFIXES.get("CAMERA", ""), sequence=sequence_name(scene), shot=shot
|
||||
prefix=config.OBJECT_PREFIXES.get("CAMERA", ""), sequence=sequence or sequence_name(scene), shot=shot
|
||||
)
|
||||
|
||||
|
||||
@@ -125,7 +135,7 @@ def camera_rig_available():
|
||||
return hasattr(bpy.types, "OBJECT_OT_build_camera_rig")
|
||||
|
||||
|
||||
def _new_camera_rig(context, shot, is_3d):
|
||||
def _new_camera_rig(context, shot, is_3d, sequence=None):
|
||||
"""Camera rig built by the Add Camera Rigs add-on at the 3D cursor, named for the shot.
|
||||
Returns the camera object."""
|
||||
scene = context.scene
|
||||
@@ -138,15 +148,99 @@ def _new_camera_rig(context, shot, is_3d):
|
||||
camera = next((ob for ob in new if ob.type == "CAMERA"), None)
|
||||
if camera is None:
|
||||
raise RuntimeError("build_camera_rig created no camera")
|
||||
camera.name = camera.data.name = camera_name(scene, shot)
|
||||
camera.name = camera.data.name = camera_name(scene, shot, sequence)
|
||||
rig = camera.parent
|
||||
if rig is not None and rig in new:
|
||||
rig.name = rig.data.name = config.SHOT_CAMERA_RIG_NAME_FORMAT.format(
|
||||
prefix=config.OBJECT_PREFIXES.get("ARMATURE", ""), sequence=sequence_name(scene), shot=shot
|
||||
prefix=config.OBJECT_PREFIXES.get("ARMATURE", ""), sequence=sequence or sequence_name(scene), shot=shot
|
||||
)
|
||||
return camera
|
||||
|
||||
|
||||
def append_shot(context, name, camera, duration):
|
||||
"""Camera marker for the shot after the last one (at SHOT_FRAME_START in an empty scene),
|
||||
the scene end moved to its last frame. A marker of that name without camera is reused."""
|
||||
scene = context.scene
|
||||
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.get(name)
|
||||
if marker is None or marker.camera is not None:
|
||||
marker = scene.timeline_markers.new(name, frame=start)
|
||||
marker.frame = start
|
||||
marker.camera = camera
|
||||
scene.frame_end = start + duration - 1
|
||||
return marker
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Kitsu sequence
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Shots of the sequence picked for Shots from Kitsu (wtd_get_sequence_shots result).
|
||||
KITSU = {}
|
||||
|
||||
|
||||
def kitsu_sequence_shots(uid, oid):
|
||||
"""wtd_get_sequence_shots for the sequence of the oid. With a wtdcmds too old to have it,
|
||||
built from wtd_list_shots and the casting of every shot, without frame counts: the shots
|
||||
Kitsu does not know are left out."""
|
||||
try:
|
||||
return ipc.call("wtd_get_sequence_shots", uid, oid=oid)
|
||||
except ipc.LibreflowError as exc:
|
||||
if exc.status != 404:
|
||||
raise
|
||||
context = ipc.call("wtd_get_context", uid, oid=oid)
|
||||
result = {key: context.get(key) for key in ("film", "sequence", "sequence_oid")}
|
||||
result["shots"] = []
|
||||
for shot in ipc.call("wtd_list_shots", uid, oid=oid, film=context.get("film"), sequence=context.get("sequence")):
|
||||
try:
|
||||
casting = ipc.call("wtd_get_casting", uid, oid=shot["oid"])
|
||||
except ipc.LibreflowError as exc:
|
||||
if exc.status is None:
|
||||
raise
|
||||
continue
|
||||
result["shots"].append(
|
||||
{
|
||||
"shot": shot["shot"],
|
||||
"oid": shot["oid"],
|
||||
"nb_frames": None,
|
||||
"casting": [
|
||||
{"name": a["name"], "asset_type": a["asset_type"], "nb_occurences": a["nb_occurences"]}
|
||||
for a in casting["assets"]
|
||||
],
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def kitsu_shot_state(scene, shot, only_cast):
|
||||
""""EXISTS" (a camera marker has its name), "UNCAST" (no casting, skipped with only_cast) or "NEW"."""
|
||||
if any(m.name == shot["shot"] for m in shots(scene)):
|
||||
return "EXISTS"
|
||||
if only_cast and not shot["casting"]:
|
||||
return "UNCAST"
|
||||
return "NEW"
|
||||
|
||||
|
||||
def create_kitsu_shots(context, data, is_3d, only_cast, duration):
|
||||
"""A camera rig and a shot for every NEW shot of the sequence, in Kitsu order, after the
|
||||
shots of the scene. Kitsu's frame count is the shot length, else duration. Returns the markers."""
|
||||
scene = context.scene
|
||||
number = sequence_number(data["sequence"])
|
||||
if number is not None:
|
||||
scene.wtd_sequence = number
|
||||
markers = []
|
||||
for shot in data["shots"]:
|
||||
if kitsu_shot_state(scene, shot, only_cast) != "NEW":
|
||||
continue
|
||||
camera = _new_camera_rig(context, shot["shot"], is_3d, data["sequence"])
|
||||
markers.append(append_shot(context, shot["shot"], camera, shot.get("nb_frames") or duration))
|
||||
return markers
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Rigs
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -294,17 +388,9 @@ class WTD_OT_shot_viewer_new_shot(bpy.types.Operator):
|
||||
self.report({"WARNING"}, "Pick a camera of the scene, or tick New Camera")
|
||||
return {"CANCELLED"}
|
||||
|
||||
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
|
||||
marker = append_shot(context, name, camera, self.duration)
|
||||
show_shot(context, marker)
|
||||
self.report({"INFO"}, f"Shot '{marker.name}' added at frame {start} ({camera.name})")
|
||||
self.report({"INFO"}, f"Shot '{marker.name}' added at frame {marker.frame} ({camera.name})")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -361,6 +447,142 @@ class WTD_OT_shot_viewer_delete_shot(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def _sequence_items(self, context):
|
||||
return _SEQUENCE_ITEMS
|
||||
|
||||
|
||||
_SEQUENCE_ITEMS = []
|
||||
|
||||
|
||||
class WTD_OT_shot_viewer_kitsu_sequence(bpy.types.Operator):
|
||||
bl_idname = "wtd_tools.shot_viewer_kitsu_sequence"
|
||||
bl_label = "Shots from Kitsu"
|
||||
bl_description = (
|
||||
"Pick a sequence, then build a camera rig and a shot for each of its Kitsu shots "
|
||||
"(Libreflow, connected in the Scene Manager)"
|
||||
)
|
||||
bl_property = "sequence"
|
||||
|
||||
sequence: EnumProperty(items=_sequence_items)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return sm.connected()
|
||||
|
||||
def invoke(self, context, event):
|
||||
oid = sm.project_oid(context.scene)
|
||||
if not oid:
|
||||
self.report({"WARNING"}, "Set a context oid (or open the project in Libreflow) first")
|
||||
return {"CANCELLED"}
|
||||
context.window.cursor_set("WAIT")
|
||||
try:
|
||||
listed = ipc.call("wtd_list_shots", sm.session_uid(), oid=oid)
|
||||
except ipc.LibreflowError as exc:
|
||||
self.report({"WARNING"}, f"Sequences: {exc}")
|
||||
return {"CANCELLED"}
|
||||
finally:
|
||||
context.window.cursor_set("DEFAULT")
|
||||
sequences = {s["sequence_oid"]: f"{s.get('film', '')} / {s['sequence']}" for s in listed if s.get("sequence_oid")}
|
||||
# The sequence of the scene's context first.
|
||||
current = (sm.context_oid(context.scene) or "").split("/shots/")[0]
|
||||
_SEQUENCE_ITEMS[:] = sorted(
|
||||
((oid, label, oid) for oid, label in sequences.items()), key=lambda item: (item[0] != current, item[1])
|
||||
)
|
||||
if not _SEQUENCE_ITEMS:
|
||||
self.report({"WARNING"}, "No sequence in the project")
|
||||
return {"CANCELLED"}
|
||||
context.window_manager.invoke_search_popup(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
context.window.cursor_set("WAIT")
|
||||
try:
|
||||
data = kitsu_sequence_shots(sm.session_uid(), self.sequence)
|
||||
except ipc.LibreflowError as exc:
|
||||
self.report({"WARNING"}, f"Kitsu shots: {exc}")
|
||||
return {"CANCELLED"}
|
||||
finally:
|
||||
context.window.cursor_set("DEFAULT")
|
||||
if not data["shots"]:
|
||||
self.report({"WARNING"}, f"No Kitsu shot in {data['sequence']}")
|
||||
return {"CANCELLED"}
|
||||
KITSU.clear()
|
||||
KITSU.update(data)
|
||||
bpy.ops.wtd_tools.shot_viewer_kitsu_shots("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
KITSU_STATE_ICONS = {"NEW": "CAMERA_DATA", "EXISTS": "CHECKMARK", "UNCAST": "BLANK1"}
|
||||
|
||||
|
||||
class WTD_OT_shot_viewer_kitsu_shots(bpy.types.Operator):
|
||||
bl_idname = "wtd_tools.shot_viewer_kitsu_shots"
|
||||
bl_label = "Build Kitsu Shots"
|
||||
bl_description = "Build a camera rig and a shot for each Kitsu shot of the picked sequence not in the scene yet"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
only_cast: BoolProperty(
|
||||
name="Only Cast Shots",
|
||||
description="Leave out the shots with nothing cast in Kitsu",
|
||||
default=True,
|
||||
)
|
||||
camera_3d: BoolProperty(name="3D Cameras", description="Build 3D crane rigs. Otherwise 2D camera rigs")
|
||||
duration: IntProperty(
|
||||
name="Default Duration",
|
||||
description="Length of the shots without a frame count in Kitsu",
|
||||
min=1,
|
||||
default=48,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return bool(KITSU.get("shots"))
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.duration = config.SHOT_DEFAULT_DURATION
|
||||
return context.window_manager.invoke_props_dialog(self, width=420)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
scene = context.scene
|
||||
layout.label(text=f"{KITSU.get('film') or ''} / {KITSU['sequence']}", icon="SEQUENCE")
|
||||
col = layout.column(align=True)
|
||||
for shot in KITSU["shots"]:
|
||||
state = kitsu_shot_state(scene, shot, self.only_cast)
|
||||
row = col.row()
|
||||
row.active = state == "NEW"
|
||||
row.label(text=shot["shot"], icon=KITSU_STATE_ICONS[state])
|
||||
row.label(text=f"{shot['nb_frames']} fr" if shot.get("nb_frames") else "? fr")
|
||||
names = ", ".join(c["name"] for c in shot["casting"])
|
||||
row.label(text=names or "nothing cast")
|
||||
layout.separator()
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
layout.prop(self, "only_cast")
|
||||
layout.prop(self, "camera_3d")
|
||||
layout.prop(self, "duration")
|
||||
new = [s for s in KITSU["shots"] if kitsu_shot_state(scene, s, self.only_cast) == "NEW"]
|
||||
if not new:
|
||||
layout.label(text="Every shot is in the scene already", icon="INFO")
|
||||
else:
|
||||
first = camera_name(scene, new[0]["shot"], KITSU["sequence"])
|
||||
layout.label(text=f"{len(new)} shot(s) to build: {first}...", icon="CAMERA_DATA")
|
||||
if not camera_rig_available():
|
||||
layout.label(text="Enable the Add Camera Rigs add-on", icon="ERROR")
|
||||
|
||||
def execute(self, context):
|
||||
if not camera_rig_available():
|
||||
self.report({"WARNING"}, "Enable the Add Camera Rigs add-on to build the shot cameras")
|
||||
return {"CANCELLED"}
|
||||
markers = create_kitsu_shots(context, KITSU, self.camera_3d, self.only_cast, self.duration)
|
||||
if not markers:
|
||||
self.report({"INFO"}, "Every shot is in the scene already")
|
||||
return {"CANCELLED"}
|
||||
show_shot(context, markers[0])
|
||||
self.report({"INFO"}, f"{len(markers)} shot(s) built from {KITSU['sequence']}: " + ", ".join(m.name for m in markers))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class WTD_OT_shot_viewer_select_rig(bpy.types.Operator):
|
||||
bl_idname = "wtd_tools.shot_viewer_select_rig"
|
||||
bl_label = "Select Rig"
|
||||
@@ -399,6 +621,8 @@ classes = (
|
||||
WTD_OT_shot_viewer_step,
|
||||
WTD_OT_shot_viewer_new_shot,
|
||||
WTD_OT_shot_viewer_delete_shot,
|
||||
WTD_OT_shot_viewer_kitsu_sequence,
|
||||
WTD_OT_shot_viewer_kitsu_shots,
|
||||
WTD_OT_shot_viewer_select_rig,
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user