Shot Manager's Kitsu creator now allows the choice of 2D/3D camera.

This commit is contained in:
Nero Frame 13 committed 2026-10-28 13:49:10 +01:00
1 parent 8dcd4daad1
commit 644aa32014
2 files changed
+58 -18

No files matched your search

+2 -2
View File
@@ -300,7 +300,7 @@ The rig is built at the **3D cursor**, in the active collection: place the curso
**Shots from Kitsu...** builds every camera a sequence needs in one go, from the shots and casting in Kitsu. It needs Libreflow connected in the [Scene Manager](#connecting). **Shots from Kitsu...** builds every camera a sequence needs in one go, from the shots and casting in Kitsu. It needs Libreflow connected in the [Scene Manager](#connecting).
1. Click **Shots from Kitsu...** and pick the sequence. The sequence of the Scene Manager's shot is at the top of the list. 1. Click **Shots from Kitsu...** and pick the sequence. The sequence of the Scene Manager's shot is at the top of the list.
2. The dialog lists the Kitsu shots of the sequence in name order. Each line shows its frame count and what is cast in it: 2. The dialog lists the Kitsu shots of the sequence in name order. Each line shows its frame count, what is cast in it and, for a shot to build, its camera type (**3D** / **2D**):
- camera icon: to build; - camera icon: to build;
- check mark: a shot of that name is in the scene already, left as it is; - check mark: a shot of that name is in the scene already, left as it is;
- greyed out: nothing is cast in it (see **Only Cast Shots**). - greyed out: nothing is cast in it (see **Only Cast Shots**).
@@ -309,7 +309,7 @@ The rig is built at the **3D cursor**, in the active collection: place the curso
| Setting | Use | | Setting | Use |
|---|---| |---|---|
| **Only Cast Shots** | On (default): shots with nothing cast in Kitsu get no camera. | | **Only Cast Shots** | On (default): shots with nothing cast in Kitsu get no camera. |
| **3D Cameras** | On: 3D **crane** rigs. Off: **2D** camera rigs. | | **All Cameras** | Sets the camera type of every shot at once (**3D**: a **crane** rig, **2D**: a 2D camera rig). Then change single shots in their line. The types are the keys of `SHOT_CAMERA_RIG_MODES`: add one there (`"Dolly": "DOLLY"`) and it shows up as a choice. |
| **Default Duration** | Length of the shots without a frame count in Kitsu. | | **Default Duration** | Length of the shots without a frame count in Kitsu. |
Each shot is built as with **+**: an Add Camera Rigs rig at the 3D cursor, `CAM_<sequence>_<shot>` / `RIG_<sequence>_<shot>`, named with the Kitsu sequence name, and a camera marker named after the Kitsu shot. The shots go one after the other after the shots of the scene, in Kitsu order. Each one is as long as its Kitsu frame count (**Nb Frames**). When the sequence name follows `SEQUENCE_NAME_FORMAT`, the **Sequence** field is set from it. Running it again only adds the shots that are still missing, so it also picks up shots added in Kitsu later. **Ctrl+Z** undoes it. Each shot is built as with **+**: an Add Camera Rigs rig at the 3D cursor, `CAM_<sequence>_<shot>` / `RIG_<sequence>_<shot>`, named with the Kitsu sequence name, and a camera marker named after the Kitsu shot. The shots go one after the other after the shots of the scene, in Kitsu order. Each one is as long as its Kitsu frame count (**Nb Frames**). When the sequence name follows `SEQUENCE_NAME_FORMAT`, the **Sequence** field is set from it. Running it again only adds the shots that are still missing, so it also picks up shots added in Kitsu later. **Ctrl+Z** undoes it.
+56 -16
View File
@@ -12,7 +12,7 @@ import re
import bpy import bpy
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
from bpy.props import BoolProperty, EnumProperty, IntProperty, PointerProperty, StringProperty from bpy.props import BoolProperty, CollectionProperty, EnumProperty, IntProperty, PointerProperty, StringProperty
from . import config from . import config
from .scene_manager import ipc from .scene_manager import ipc
@@ -135,14 +135,14 @@ def camera_rig_available():
return hasattr(bpy.types, "OBJECT_OT_build_camera_rig") return hasattr(bpy.types, "OBJECT_OT_build_camera_rig")
def _new_camera_rig(context, shot, is_3d, sequence=None): def _new_camera_rig(context, shot, kind, sequence=None):
"""Camera rig built by the Add Camera Rigs add-on at the 3D cursor, named for the shot. """Camera rig built by the Add Camera Rigs add-on at the 3D cursor, named for the shot.
Returns the camera object.""" kind is a key of config SHOT_CAMERA_RIG_MODES ("3D", "2D"). Returns the camera object."""
scene = context.scene scene = context.scene
if context.mode != "OBJECT": if context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
before = {ob.as_pointer() for ob in bpy.data.objects} before = {ob.as_pointer() for ob in bpy.data.objects}
bpy.ops.object.build_camera_rig(mode=config.SHOT_CAMERA_RIG_MODES["3D" if is_3d else "2D"]) bpy.ops.object.build_camera_rig(mode=config.SHOT_CAMERA_RIG_MODES[kind])
# Found by difference: the add-on may also add widget objects for the bone shapes. # Found by difference: the add-on may also add widget objects for the bone shapes.
new = [ob for ob in bpy.data.objects if ob.as_pointer() not in before] new = [ob for ob in bpy.data.objects if ob.as_pointer() not in before]
camera = next((ob for ob in new if ob.type == "CAMERA"), None) camera = next((ob for ob in new if ob.type == "CAMERA"), None)
@@ -225,9 +225,10 @@ def kitsu_shot_state(scene, shot, only_cast):
return "NEW" return "NEW"
def create_kitsu_shots(context, data, is_3d, only_cast, duration): def create_kitsu_shots(context, data, cameras, only_cast, duration, default_camera):
"""A camera rig and a shot for every NEW shot of the sequence, in Kitsu order, after the """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.""" shots of the scene. The rig is cameras[shot name], else default_camera (keys of config
SHOT_CAMERA_RIG_MODES). Kitsu's frame count is the shot length, else duration. Returns the markers."""
scene = context.scene scene = context.scene
number = sequence_number(data["sequence"]) number = sequence_number(data["sequence"])
if number is not None: if number is not None:
@@ -236,7 +237,8 @@ def create_kitsu_shots(context, data, is_3d, only_cast, duration):
for shot in data["shots"]: for shot in data["shots"]:
if kitsu_shot_state(scene, shot, only_cast) != "NEW": if kitsu_shot_state(scene, shot, only_cast) != "NEW":
continue continue
camera = _new_camera_rig(context, shot["shot"], is_3d, data["sequence"]) kind = cameras.get(shot["shot"], default_camera)
camera = _new_camera_rig(context, shot["shot"], kind, data["sequence"])
markers.append(append_shot(context, shot["shot"], camera, shot.get("nb_frames") or duration)) markers.append(append_shot(context, shot["shot"], camera, shot.get("nb_frames") or duration))
return markers return markers
@@ -381,7 +383,7 @@ class WTD_OT_shot_viewer_new_shot(bpy.types.Operator):
if not camera_rig_available(): if not camera_rig_available():
self.report({"WARNING"}, "Enable the Add Camera Rigs add-on to build the shot camera") self.report({"WARNING"}, "Enable the Add Camera Rigs add-on to build the shot camera")
return {"CANCELLED"} return {"CANCELLED"}
camera = _new_camera_rig(context, name, self.camera_3d) camera = _new_camera_rig(context, name, "3D" if self.camera_3d else "2D")
else: else:
camera = scene.objects.get(self.camera_name) camera = scene.objects.get(self.camera_name)
if camera is None or camera.type != "CAMERA": if camera is None or camera.type != "CAMERA":
@@ -513,6 +515,22 @@ class WTD_OT_shot_viewer_kitsu_sequence(bpy.types.Operator):
KITSU_STATE_ICONS = {"NEW": "CAMERA_DATA", "EXISTS": "CHECKMARK", "UNCAST": "BLANK1"} KITSU_STATE_ICONS = {"NEW": "CAMERA_DATA", "EXISTS": "CHECKMARK", "UNCAST": "BLANK1"}
# Camera rig choices: the keys of config SHOT_CAMERA_RIG_MODES ("3D", "2D").
CAMERA_KIND_ITEMS = [
(kind, kind, f"{mode.title()} camera rig (Add Camera Rigs)") for kind, mode in config.SHOT_CAMERA_RIG_MODES.items()
]
DEFAULT_CAMERA_KIND = "2D" if "2D" in config.SHOT_CAMERA_RIG_MODES else CAMERA_KIND_ITEMS[0][0]
class WTD_KitsuShotCamera(bpy.types.PropertyGroup):
"""Camera rig of one Kitsu shot in Build Kitsu Shots (name: the shot)."""
camera: EnumProperty(name="Camera", items=CAMERA_KIND_ITEMS, default=DEFAULT_CAMERA_KIND)
def _all_cameras_update(self, context):
for item in self.cameras:
item.camera = self.camera
class WTD_OT_shot_viewer_kitsu_shots(bpy.types.Operator): class WTD_OT_shot_viewer_kitsu_shots(bpy.types.Operator):
@@ -526,7 +544,14 @@ class WTD_OT_shot_viewer_kitsu_shots(bpy.types.Operator):
description="Leave out the shots with nothing cast in Kitsu", description="Leave out the shots with nothing cast in Kitsu",
default=True, default=True,
) )
camera_3d: BoolProperty(name="3D Cameras", description="Build 3D crane rigs. Otherwise 2D camera rigs") camera: EnumProperty(
name="All Cameras",
description="Camera rig of every shot. Each shot can then be changed in its line",
items=CAMERA_KIND_ITEMS,
default=DEFAULT_CAMERA_KIND,
update=_all_cameras_update,
)
cameras: CollectionProperty(type=WTD_KitsuShotCamera)
duration: IntProperty( duration: IntProperty(
name="Default Duration", name="Default Duration",
description="Length of the shots without a frame count in Kitsu", description="Length of the shots without a frame count in Kitsu",
@@ -540,7 +565,12 @@ class WTD_OT_shot_viewer_kitsu_shots(bpy.types.Operator):
def invoke(self, context, event): def invoke(self, context, event):
self.duration = config.SHOT_DEFAULT_DURATION self.duration = config.SHOT_DEFAULT_DURATION
return context.window_manager.invoke_props_dialog(self, width=420) self.cameras.clear()
for shot in KITSU["shots"]:
item = self.cameras.add()
item.name = shot["shot"]
item.camera = self.camera
return context.window_manager.invoke_props_dialog(self, width=560)
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
@@ -550,16 +580,24 @@ class WTD_OT_shot_viewer_kitsu_shots(bpy.types.Operator):
for shot in KITSU["shots"]: for shot in KITSU["shots"]:
state = kitsu_shot_state(scene, shot, self.only_cast) state = kitsu_shot_state(scene, shot, self.only_cast)
row = col.row() row = col.row()
row.active = state == "NEW" info = row.row()
row.label(text=shot["shot"], icon=KITSU_STATE_ICONS[state]) info.active = state == "NEW"
row.label(text=f"{shot['nb_frames']} fr" if shot.get("nb_frames") else "? fr") info.label(text=shot["shot"], icon=KITSU_STATE_ICONS[state])
info.label(text=f"{shot['nb_frames']} fr" if shot.get("nb_frames") else "? fr")
names = ", ".join(c["name"] for c in shot["casting"]) names = ", ".join(c["name"] for c in shot["casting"])
row.label(text=names or "nothing cast") info.label(text=names or "nothing cast")
choice = row.row()
choice.ui_units_x = 5
item = self.cameras.get(shot["shot"])
if state == "NEW" and item is not None:
choice.prop(item, "camera", expand=True)
else:
choice.label(text="")
layout.separator() layout.separator()
layout.use_property_split = True layout.use_property_split = True
layout.use_property_decorate = False layout.use_property_decorate = False
layout.prop(self, "camera", expand=True)
layout.prop(self, "only_cast") layout.prop(self, "only_cast")
layout.prop(self, "camera_3d")
layout.prop(self, "duration") layout.prop(self, "duration")
new = [s for s in KITSU["shots"] if kitsu_shot_state(scene, s, self.only_cast) == "NEW"] new = [s for s in KITSU["shots"] if kitsu_shot_state(scene, s, self.only_cast) == "NEW"]
if not new: if not new:
@@ -574,7 +612,8 @@ class WTD_OT_shot_viewer_kitsu_shots(bpy.types.Operator):
if not camera_rig_available(): if not camera_rig_available():
self.report({"WARNING"}, "Enable the Add Camera Rigs add-on to build the shot cameras") self.report({"WARNING"}, "Enable the Add Camera Rigs add-on to build the shot cameras")
return {"CANCELLED"} return {"CANCELLED"}
markers = create_kitsu_shots(context, KITSU, self.camera_3d, self.only_cast, self.duration) cameras = {item.name: item.camera for item in self.cameras}
markers = create_kitsu_shots(context, KITSU, cameras, self.only_cast, self.duration, self.camera)
if not markers: if not markers:
self.report({"INFO"}, "Every shot is in the scene already") self.report({"INFO"}, "Every shot is in the scene already")
return {"CANCELLED"} return {"CANCELLED"}
@@ -622,6 +661,7 @@ classes = (
WTD_OT_shot_viewer_new_shot, WTD_OT_shot_viewer_new_shot,
WTD_OT_shot_viewer_delete_shot, WTD_OT_shot_viewer_delete_shot,
WTD_OT_shot_viewer_kitsu_sequence, WTD_OT_shot_viewer_kitsu_sequence,
WTD_KitsuShotCamera,
WTD_OT_shot_viewer_kitsu_shots, WTD_OT_shot_viewer_kitsu_shots,
WTD_OT_shot_viewer_select_rig, WTD_OT_shot_viewer_select_rig,
) )