Files
Nero Frame 13andClaude Opus 5.5 00788bb3bf 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 <noreply@anthropic.com>
2026-10-21 13:25:09 +02:00

368 lines
12 KiB
Python

import textwrap
import bpy
from .. import config, rename, shot_viewer, tools_ops
from ..core import CheckContext, Severity, checks_for_context, contexts, runner
from ..core.runner import Status
from .operators import EXPANDED
STATUS_ICONS = {
Status.NOT_RUN: "DOT",
Status.PASSED: "CHECKMARK",
Status.INFO: "INFO",
Status.WARNING: "ERROR",
Status.ERROR: "CANCEL",
Status.SKIPPED: "REMOVE",
Status.CRASHED: "GHOST_ENABLED",
}
SEVERITY_ICONS = {
Severity.INFO: "INFO",
Severity.WARNING: "ERROR",
Severity.ERROR: "CANCEL",
}
def _wrapped_label(layout, context, text, icon="NONE", margin=0):
"""Label wrapped to the region width. ``margin`` is the room (in characters) taken by buttons."""
char_width = 6.5 * context.preferences.system.ui_scale
width = max(16, int(context.region.width / char_width) - 6 - margin)
col = layout.column(align=True)
for index, line in enumerate(textwrap.wrap(text, width) or [""]):
col.label(text=line, icon=icon if index == 0 else "BLANK1")
def draw_sanity_checks_panel(self, context):
layout = self.layout
scene = context.scene
settings = scene.wtd_sanity
report = runner.LAST_REPORT
if report is not None and report.context_id != settings.context:
report = None
layout.prop(settings, "context", text="")
if settings.context in contexts.ASSET:
row = layout.row(align=True)
row.prop(settings, "asset_name", text="Asset")
if not settings.asset_name:
guessed = CheckContext(scene, settings.context).asset_name
row.label(text=guessed or "(from file name)")
row = layout.row(align=True)
row.scale_y = 1.4
row.operator("wtd_sanity.run_checks", text="Run Checks", icon="PLAY")
row.operator("wtd_sanity.fix_all", text="Fix All", icon="TOOL_SETTINGS")
row.operator("wtd_sanity.write_report", text="", icon="TEXT")
if settings.context == contexts.CHAR_MOD:
layout.operator("wtd_sanity.export_char_reference", icon="EXPORT")
if report is not None:
counts = report.counts()
row = layout.row()
for status in (
Status.PASSED,
Status.WARNING,
Status.ERROR,
Status.SKIPPED,
Status.CRASHED,
):
if counts[status]:
row.label(text=str(counts[status]), icon=STATUS_ICONS[status])
disabled = runner.disabled_check_ids(scene)
box = None
category = None
for check in checks_for_context(settings.context):
if check.category != category:
category = check.category
box = layout.box()
box.label(text=category)
_draw_check(box, context, check, report, check.id in disabled)
def _draw_check(layout, context, check, report, disabled):
result = report.results.get(check.id) if report else None
status = result.status if result else Status.NOT_RUN
expanded = check.id in EXPANDED
row = layout.row(align=True)
row.operator(
"wtd_sanity.toggle_check",
text="",
emboss=False,
icon="CHECKBOX_DEHLT" if disabled else "CHECKBOX_HLT",
).check_id = check.id
row.operator(
"wtd_sanity.toggle_expand",
text="",
emboss=False,
icon="DOWNARROW_HLT" if expanded else "RIGHTARROW",
).check_id = check.id
label_row = row.row(align=True)
label_row.enabled = not disabled
label_row.alert = status in {Status.ERROR, Status.CRASHED}
count = f" ({len(result.issues)})" if result and result.issues else ""
label_row.label(text=check.label + count, icon=STATUS_ICONS[status])
buttons = row.row(align=True)
if result and any(issue.id_name or issue.elements for issue in result.issues):
buttons.operator(
"wtd_sanity.select_issues", text="", icon="RESTRICT_SELECT_OFF"
).check_id = check.id
if result and result.failed and check.can_fix():
buttons.operator(
"wtd_sanity.fix_check", text="", icon="TOOL_SETTINGS"
).check_id = check.id
buttons.operator(
"wtd_sanity.run_checks", text="", icon="FILE_REFRESH"
).check_id = check.id
if not expanded:
return
col = layout.column()
col.separator(factor=0.3)
_wrapped_label(col, context, check.description, icon="QUESTION")
if result is None:
return
if result.message:
_wrapped_label(
col,
context,
result.message.strip().splitlines()[-1],
icon=STATUS_ICONS[status],
)
for index, issue in enumerate(result.issues[: config.MAX_ISSUES_DRAWN]):
severity = issue.severity or check.severity
issue_row = col.row(align=True)
selectable = bool(issue.id_name or issue.elements)
_wrapped_label(
issue_row,
context,
issue.message,
icon=SEVERITY_ICONS[severity],
margin=7 if selectable else 0,
)
if selectable:
op = issue_row.operator(
"wtd_sanity.select_issues",
text="",
icon="RESTRICT_SELECT_OFF",
emboss=False,
)
op.check_id = check.id
op.issue_index = index
if check.editable_names:
id_block = issue.get_id()
if id_block is not None and id_block.library is None:
col.prop(id_block, "name", text="", icon="BLANK1")
hidden = len(result.issues) - config.MAX_ISSUES_DRAWN
if hidden > 0:
col.label(text=f"... {hidden} more (see Write Report)", icon="BLANK1")
col.separator(factor=0.5)
class _SanityPanel:
bl_region_type = "UI"
bl_category = "WTD"
bl_label = "Sanity Check"
bl_options = {"DEFAULT_CLOSED"} # panel starts collapsed
def draw(self, context):
draw_sanity_checks_panel(self, context)
class _ToolsPanel:
bl_region_type = "UI"
bl_category = "WTD"
bl_label = "Tools"
def draw(self, context) -> None:
layout = self.layout
layout.operator(
tools_ops.WTDGetOidForPath.bl_idname,
text=tools_ops.WTDGetOidForPath.bl_label,
)
# Adds a separator line
layout.separator()
class _RenamePanel:
bl_region_type = "UI"
bl_category = "WTD"
bl_label = "Rename"
bl_options = {"DEFAULT_CLOSED"} # panel starts collapsed
PREVIEW_COUNT = 10
def draw(self, context) -> None:
layout = self.layout
settings = context.scene.wtd_rename
layout.row().prop(settings, "target", expand=True)
col = layout.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(settings, "base_name")
col.prop(settings, "prefix_mode")
if settings.prefix_mode == "CUSTOM":
col.prop(settings, "prefix_custom", text=" ")
col.prop(settings, "side")
col.prop(settings, "suffix_mode")
if settings.suffix_mode == "CUSTOM":
col.prop(settings, "suffix_custom", text=" ")
col.prop(settings, "sort")
if settings.target == "OBJECTS":
col.prop(settings, "rename_data")
plan, skipped = rename.plan_renames(context, settings)
row = layout.row()
row.scale_y = 1.4
row.operator(
rename.WTD_OT_batch_rename.bl_idname,
text=f"Rename {len(plan)} Item(s)" if plan else "Rename",
icon="SORTALPHA",
)
if not plan and not skipped:
return
box = layout.box()
col = box.column(align=True)
for id_block, name in plan[: self.PREVIEW_COUNT]:
col.label(text=f"{id_block.name} → {name}")
hidden = len(plan) - self.PREVIEW_COUNT
if hidden > 0:
col.label(text=f"... {hidden} more")
for name in skipped:
col.label(text=f"{name}: no base name, set a name", icon="ERROR")
class WTD_UL_shots(bpy.types.UIList):
"""Camera markers of the scene, in time order."""
def draw_item(self, context, layout, data, item, icon, active_data, active_property, index=0, flt_flag=0):
start, end = shot_viewer.shot_range(item)
split = layout.split(factor=0.4)
split.label(text=item.name, icon="MARKER_HLT")
split = split.split(factor=0.6)
split.label(text=item.camera.name)
split.label(text=f"{start}-{end}")
def filter_items(self, context, data, propname):
markers = getattr(data, propname)
ordered = [marker.as_pointer() for marker in shot_viewer.shots(data)]
position = {pointer: index for index, pointer in enumerate(ordered)}
flags = [self.bitflag_filter_item if marker.as_pointer() in position else 0 for marker in markers]
if self.filter_name:
by_name = bpy.types.UI_UL_list.filter_items_by_name(
self.filter_name, self.bitflag_filter_item, markers, "name"
)
flags = [flag & name_flag for flag, name_flag in zip(flags, by_name)]
# Shots in time order, the markers without camera after them.
order = []
hidden = len(ordered)
for marker in markers:
pointer = marker.as_pointer()
if pointer in position:
order.append(position[pointer])
else:
order.append(hidden)
hidden += 1
return flags, order
class _ShotViewerPanel:
bl_region_type = "UI"
bl_category = "WTD"
bl_label = "Shot Viewer"
def draw(self, context) -> None:
layout = self.layout
settings = context.window_manager.wtd_shot_viewer
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
row.operator(shot_viewer.WTD_OT_shot_viewer_step.bl_idname, text="Previous", icon="TRIA_LEFT").step = -1
row.operator(shot_viewer.WTD_OT_shot_viewer_step.bl_idname, text="Next", icon="TRIA_RIGHT").step = 1
row = layout.row()
row.template_list("WTD_UL_shots", "", scene, "timeline_markers", settings, "shot_index", rows=5)
col = row.column(align=True)
col.operator(shot_viewer.WTD_OT_shot_viewer_new_shot.bl_idname, text="", icon="ADD")
col.operator(shot_viewer.WTD_OT_shot_viewer_delete_shot.bl_idname, text="", icon="REMOVE")
if not has_shots:
layout.label(text="No camera marker yet: add a shot with +", icon="INFO")
layout.prop(settings, "loop_shot")
groups = shot_viewer.rig_groups(context.view_layer)
if not groups:
layout.label(text="No rig in the scene", icon="INFO")
for label, rigs in groups.items():
box = layout.box()
box.label(text=label, icon="ARMATURE_DATA")
col = box.column(align=True)
for rig in rigs:
col.operator(
shot_viewer.WTD_OT_shot_viewer_select_rig.bl_idname,
text=rig.name,
depress=rig.select_get(),
).name = rig.name
class WTD_PT_sanity_view3d(_SanityPanel, bpy.types.Panel):
bl_idname = "WTD_PT_sanity_view3d"
bl_space_type = "VIEW_3D"
class WTD_PT_tools_view3d(_ToolsPanel, bpy.types.Panel):
bl_idname = "WTD_PT_tools_view3d"
bl_space_type = "VIEW_3D"
class WTD_PT_rename_view3d(_RenamePanel, bpy.types.Panel):
bl_idname = "WTD_PT_rename_view3d"
bl_space_type = "VIEW_3D"
class WTD_PT_rename_node_editor(_RenamePanel, bpy.types.Panel):
bl_idname = "WTD_PT_rename_node_editor"
bl_space_type = "NODE_EDITOR"
class WTD_PT_shot_viewer_view3d(_ShotViewerPanel, bpy.types.Panel):
bl_idname = "WTD_PT_shot_viewer_view3d"
bl_space_type = "VIEW_3D"
class WTD_PT_sanity_sequencer(_SanityPanel, bpy.types.Panel):
bl_idname = "WTD_PT_sanity_sequencer"
bl_space_type = "SEQUENCE_EDITOR"
classes = (
WTD_UL_shots,
WTD_PT_sanity_view3d,
WTD_PT_tools_view3d,
WTD_PT_shot_viewer_view3d,
WTD_PT_rename_view3d,
WTD_PT_rename_node_editor,
WTD_PT_sanity_sequencer,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)