From 00788bb3bf84aff974c5edfc7b4732888b6d5470 Mon Sep 17 00:00:00 2001 From: Nero Frame 13 Date: Wed, 21 Oct 2026 13:25:09 +0200 Subject: [PATCH] Shot Viewer: sequence field, cameras named CAM_sq0000_sh0000 The Shot Viewer panel gets a Sequence number, saved with the scene. New shot cameras are named from it with SHOT_CAMERA_NAME_FORMAT ("{prefix}{sequence}_{shot}") and SEQUENCE_NAME_FORMAT ("sq{:04d}"), both overridable in the config JSON. Co-Authored-By: Claude Opus 5.5 --- README.md | 11 +++++++++-- config.py | 5 +++++ shot_viewer.py | 32 ++++++++++++++++++++++++++------ ui/panel.py | 4 ++++ 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cf001b3..e6271f6 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,10 @@ Choosing a shot in the panel moves the playhead to it and puts **the 3D view you The list always highlights the shot under the playhead. The file does not need any shots yet: add the first one with **+**. +### Sequence + +Set the sequence number in the **Sequence** field at the top of the panel. It is saved with the scene, and the formatted name is shown next to it (`10` → `sq0010`). It is used to name the cameras of new shots: `CAM_sq0010_sh0030`. Both formats can be changed in the [Configuration](#configuration) (`SEQUENCE_NAME_FORMAT`, `SHOT_CAMERA_NAME_FORMAT`). + ### Moving through the sequence | To… | Do… | @@ -275,7 +279,7 @@ Click **+** next to the shot list: |---|---| | **Name** | Pre-filled with the next free shot number (`sh0010`, `sh0020`…). This is the marker name. | | **Duration** | Length of the shot in frames. | -| **New Camera** | On: creates the camera `CAM_`, copying the lens and position of the current camera, so you start from the current framing. Off: pick an existing camera of the scene. | +| **New Camera** | On: creates the camera `CAM__` (its name is shown in the dialog), copying the lens and position of the current camera, so you start from the current framing. Off: pick an existing camera of the scene. | The shot is added at the end of the sequence and the scene's **End** frame grows to include it. The first shot starts at frame 1001, and the scene **Start** moves there too. The playhead and the 3D view then jump to the new shot. @@ -316,6 +320,7 @@ To change them **without editing the code**, write a JSON file with the keys to "RESOLUTION": [1920, 1080], "RENAME_SIDES": ["center", "left", "right"], "SHOT_FRAME_START": 1001, + "SEQUENCE_NAME_FORMAT": "sq{:03d}", "SHOT_VIEWER_RIG_GROUPS": { "Characters": "(?i)^(?:chr|ch)[_-]" } @@ -324,6 +329,8 @@ To change them **without editing the code**, write a JSON file with the keys to Unknown keys are ignored and reported in the system console. +Name formats use Python's `str.format`: `"sq{:04d}"` turns `10` into `sq0010`. `SHOT_CAMERA_NAME_FORMAT` takes the named tokens `{prefix}` (the camera prefix of `OBJECT_PREFIXES`, `CAM_`), `{sequence}` and `{shot}`. The default `"{prefix}{sequence}_{shot}"` gives `CAM_sq0010_sh0030`. + ### Keys by tool | Tool | Keys | @@ -336,7 +343,7 @@ Unknown keys are ignored and reported in the system console. | Sanity Check: layout | `SET_COLLECTION_PATTERN` | | Sanity Check: panel | `CATEGORY_ORDER`, `MAX_ISSUES_DRAWN` | | Rename | `RENAME_SEPARATOR`, `RENAME_PADDING`, `RENAME_SIDES`, `RENAME_TYPE_SUFFIXES` (plus `OBJECT_PREFIXES` / `DATABLOCK_PREFIXES` for *By Type* prefixes) | -| Shot Viewer | `SHOT_VIEWER_RIG_GROUPS`, `SHOT_NAME_FORMAT`, `SHOT_NUMBER_STEP`, `SHOT_FRAME_START`, `SHOT_DEFAULT_DURATION` | +| Shot Viewer | `SHOT_VIEWER_RIG_GROUPS`, `SHOT_NAME_FORMAT`, `SHOT_NUMBER_STEP`, `SHOT_FRAME_START`, `SHOT_DEFAULT_DURATION`, `SEQUENCE_NAME_FORMAT`, `SHOT_CAMERA_NAME_FORMAT` | --- diff --git a/config.py b/config.py index 0a9c9d8..6decc7b 100644 --- a/config.py +++ b/config.py @@ -246,6 +246,11 @@ SHOT_VIEWER_RIG_GROUPS = { # Shots added by the shot viewer. TODO: confirm with production. SHOT_NAME_FORMAT = "sh{:04d}" +# Sequence of the scene (number set in the Shot Viewer panel): SEQUENCE_NAME_FORMAT.format(number). +SEQUENCE_NAME_FORMAT = "sq{:04d}" +# Cameras added by the shot viewer. Tokens: {prefix} (OBJECT_PREFIXES["CAMERA"]), +# {sequence} (SEQUENCE_NAME_FORMAT) and {shot} (the shot name). +SHOT_CAMERA_NAME_FORMAT = "{prefix}{sequence}_{shot}" SHOT_NUMBER_STEP = 10 SHOT_FRAME_START = 1001 SHOT_DEFAULT_DURATION = 48 diff --git a/shot_viewer.py b/shot_viewer.py index 86494f5..86566a7 100644 --- a/shot_viewer.py +++ b/shot_viewer.py @@ -110,16 +110,27 @@ def next_shot_name(scene): return config.SHOT_NAME_FORMAT.format((max(numbers, default=0) // step + 1) * step) -def _new_camera(scene, name, source=None): +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).""" + return config.SHOT_CAMERA_NAME_FORMAT.format( + prefix=config.OBJECT_PREFIXES.get("CAMERA", ""), sequence=sequence_name(scene), shot=shot + ) + + +def _new_camera(scene, shot, source=None): """New camera in the scene collection, copying ``source`` (lens and placement) when given.""" - camera_name = config.OBJECT_PREFIXES.get("CAMERA", "") + name + name = camera_name(scene, shot) if source is not None and source.type == "CAMERA": data = source.data.copy() - data.name = camera_name + data.name = name else: source = None - data = bpy.data.cameras.new(camera_name) - camera = bpy.data.objects.new(camera_name, data) + data = bpy.data.cameras.new(name) + camera = bpy.data.objects.new(name, data) if source is not None: camera.matrix_world = source.matrix_world.copy() else: @@ -231,7 +242,10 @@ class WTD_OT_shot_viewer_new_shot(bpy.types.Operator): layout.prop(self, "name") layout.prop(self, "duration") layout.prop(self, "new_camera") - if not self.new_camera: + if self.new_camera: + name = self.name.strip() or next_shot_name(context.scene) + layout.label(text=camera_name(context.scene, name), icon="CAMERA_DATA") + else: layout.prop_search(self, "camera_name", context.scene, "objects", icon="CAMERA_DATA") def execute(self, context): @@ -358,12 +372,18 @@ def register(): for cls in classes: bpy.utils.register_class(cls) bpy.types.WindowManager.wtd_shot_viewer = PointerProperty(type=WTD_ShotViewerSettings) + bpy.types.Scene.wtd_sequence = IntProperty( + name="Sequence", + description="Sequence number of the scene, used in the names of new cameras (config SEQUENCE_NAME_FORMAT)", + min=0, + ) bpy.app.handlers.frame_change_post.append(_on_frame_change) def unregister(): if _on_frame_change in bpy.app.handlers.frame_change_post: bpy.app.handlers.frame_change_post.remove(_on_frame_change) + del bpy.types.Scene.wtd_sequence del bpy.types.WindowManager.wtd_shot_viewer for cls in reversed(classes): bpy.utils.unregister_class(cls) diff --git a/ui/panel.py b/ui/panel.py index f480932..9c25681 100644 --- a/ui/panel.py +++ b/ui/panel.py @@ -283,6 +283,10 @@ class _ShotViewerPanel: scene = context.scene has_shots = bool(shot_viewer.shots(scene)) + row = layout.row(align=True) + row.prop(scene, "wtd_sequence") + row.label(text=shot_viewer.sequence_name(scene)) + row = layout.row(align=True) row.scale_y = 1.4 row.enabled = has_shots