Files
wtd-libreflow-addon/ui/panel.py
T
NDemaerschalkandClaude Opus 5.5 fbc7ac18e4 Scene Manager: Load Publish and numbered instances
Load Publish (Loaded panel) searches every asset of the project, then
loads any of its published .blend files at any version, cast in the shot
or not. The file the casting would load and the latest version on this
site are preselected; a version not on this site is requested instead.
Built on the existing wtd_list_assets / wtd_get_asset_files /
wtd_get_revisions commands.

New instances are numbered (COL_bob_01, tree_01...), counting on from the
highest number in the file (SCENE_MANAGER_INSTANCE_NAME).

README: Pick Shot only lists shots that exist in Libreflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 10:47:05 +01:00

604 lines
23 KiB
Python

import textwrap
import bpy
from .. import config, rename, shot_viewer, tools_ops
from ..scene_manager import ipc
from ..scene_manager import library as sm_library
from ..scene_manager import operators as sm
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 Manager"
bl_options = {"DEFAULT_CLOSED"} # panel starts collapsed
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
MESSAGE_ICONS = {"INFO": "INFO", "WARNING": "ERROR", "ERROR": "CANCEL"}
class _SceneManagerPanel:
bl_region_type = "UI"
bl_category = "WTD"
bl_label = "Scene Manager"
def draw(self, context) -> None:
layout = self.layout
state = sm.STATE
scene = context.scene
row = layout.row(align=True)
if sm.connected():
session = sm.current_session()
text = f"{session.get('session_name', '?')} ({session.get('cluster_name', '')})"
if state.ping.get("user"):
text += f" {state.ping['user']} @ {state.ping.get('site', '')}"
row.label(text=text, icon="LINKED")
else:
row.label(text=state.connection_error, icon="UNLINKED")
row.operator(sm.WTD_OT_scene_connect.bl_idname, text="", icon="FILE_REFRESH")
if len(state.sessions) > 1:
col = layout.column(align=True)
chosen = context.window_manager.wtd_libreflow.session_uid
for uid, info in state.sessions.items():
col.operator(
sm.WTD_OT_scene_select_session.bl_idname,
text=f"{info.get('session_name', uid)}: {info.get('current_oid') or ''}",
depress=uid == chosen,
).uid = uid
layout.prop(scene.wtd_scene_manager, "context_oid", text="", icon="SEQUENCE")
row = layout.row(align=True)
row.operator(sm.WTD_OT_scene_use_current_oid.bl_idname, text="Libreflow Oid", icon="IMPORT")
row.operator(sm.WTD_OT_scene_pick_shot.bl_idname, text="Pick Shot", icon="VIEWZOOM")
text, valid = sm.context_label(sm.context_oid(scene))
if text:
row = layout.row()
row.alert = not valid
_wrapped_label(row, context, text, icon="CHECKMARK" if valid else "ERROR")
busy = ipc.busy_label()
if busy:
layout.label(text=f"{busy}...", icon="SORTTIME")
message, level = state.message
if message:
row = layout.row()
row.alert = level == "ERROR"
_wrapped_label(row, context, message, icon=MESSAGE_ICONS[level])
class _SceneCastingPanel:
bl_region_type = "UI"
bl_category = "WTD"
bl_label = "Casting"
bl_parent_id = "WTD_PT_scene_manager_view3d"
def draw(self, context) -> None:
layout = self.layout
oid = sm.context_oid(context.scene)
row = layout.row(align=True)
row.scale_y = 1.3
row.operator(sm.WTD_OT_scene_load.bl_idname, text="Load Missing", icon="IMPORT")
if sm.missing_files(context.scene):
row.operator(sm.WTD_OT_scene_request.bl_idname, text="Request Missing", icon="URL")
row.operator(sm.WTD_OT_scene_refresh.bl_idname, text="", icon="FILE_REFRESH")
casting = sm.STATE.casting.get(oid)
if casting is None:
text = "Not read yet: refresh" if sm.is_shot_context(oid) else "Set a shot as context"
layout.label(text=text, icon="INFO")
return
if not casting["assets"]:
layout.label(text="Nothing cast in this shot", icon="INFO")
found = sm_library.all_instances()
col = layout.column(align=True)
for asset in casting["assets"]:
library = sm_library.find_library(asset["file"]["file_oid"]) if asset.get("file") else None
have = len(found.get(library.name, [])) if library else 0
want = asset.get("nb_occurences") or 1
error = asset.get("error")
file = sm.file_data(asset["file"]["file_oid"], asset["file"]) if asset.get("file") else None
latest = (file or {}).get("latest") or {}
requested = bool(error) and latest.get("sync_status") == "Requested"
if requested:
error = f"{latest['name']} requested, waiting for the download"
icon = "SORTTIME" if requested else "ERROR" if error else "CHECKMARK" if have >= want else "IMPORT"
row = col.row(align=True)
row.alert = bool(error) and not requested
row.label(text=f"{asset['name']} ({asset.get('asset_type', '')})", icon=icon)
row.label(text=f"{have}/{want}")
if not error and have < want:
row.operator(sm.WTD_OT_scene_load.bl_idname, text="", icon="IMPORT").asset_oid = asset["oid"]
if sm.to_request(file):
row.operator(sm.WTD_OT_scene_request.bl_idname, text="", icon="URL").file_oid = file["file_oid"]
if error:
sub = col.row()
sub.alert = not requested
_wrapped_label(sub, context, error, icon="BLANK1")
cast = {asset["oid"] for asset in casting["assets"]}
extra = [library for library in sm_library.managed_libraries() if library["wtd_asset_oid"] not in cast]
if extra:
box = layout.box()
box.label(text="Loaded, no longer cast:", icon="ERROR")
for library in extra:
box.label(text=library.get("wtd_asset_name", library.name), icon="BLANK1")
class _SceneLoadedPanel:
bl_region_type = "UI"
bl_category = "WTD"
bl_label = "Loaded"
bl_parent_id = "WTD_PT_scene_manager_view3d"
def draw(self, context) -> None:
layout = self.layout
layout.operator(sm.WTD_OT_scene_browse.bl_idname, text="Load Publish...", icon="VIEWZOOM")
libraries = sm_library.managed_libraries()
if not libraries:
layout.label(text="Nothing loaded by the Scene Manager", icon="INFO")
return
row = layout.row(align=True)
row.operator(sm.WTD_OT_scene_update.bl_idname, text="Update All", icon="FILE_REFRESH")
row.operator(sm.WTD_OT_scene_refresh.bl_idname, text="", icon="VIEWZOOM")
found = sm_library.all_instances()
for library in libraries:
state, text = sm.version_status(library)
outdated = state in {"UPDATE", "PENDING"}
expanded = library.name in sm.EXPANDED
row = layout.row(align=True)
row.operator(
sm.WTD_OT_scene_toggle_expand.bl_idname,
text="",
emboss=False,
icon="DOWNARROW_HLT" if expanded else "RIGHTARROW",
).library = library.name
row.label(text=library.get("wtd_asset_name", library.name), icon="LIBRARY_DATA_DIRECT")
version = row.row(align=True)
version.alert = outdated
version.label(text=library.get("wtd_revision", "?"))
if state == "UPDATE":
row.operator(sm.WTD_OT_scene_update.bl_idname, text="", icon="FILE_REFRESH").library = library.name
elif state == "PENDING" and sm.to_request(sm.file_data(library["wtd_file_oid"])):
row.operator(sm.WTD_OT_scene_request.bl_idname, text="", icon="URL").file_oid = library["wtd_file_oid"]
row.operator(sm.WTD_OT_scene_pick_revision.bl_idname, text="", icon="COLLAPSEMENU").library = library.name
row.operator(sm.WTD_OT_scene_remove.bl_idname, text="", icon="X").library = library.name
if outdated:
sub = layout.row()
sub.alert = True
_wrapped_label(sub, context, text, icon="BLANK1")
if not expanded:
continue
col = layout.column(align=True)
for instance in found.get(library.name, []):
is_object = isinstance(instance, bpy.types.Object)
sub = col.row(align=True)
sub.label(text=instance.name, icon="OUTLINER_OB_EMPTY" if is_object else "OUTLINER_COLLECTION")
op = sub.operator(sm.WTD_OT_scene_select_instance.bl_idname, text="", icon="RESTRICT_SELECT_OFF")
op.library, op.instance = library.name, instance.name
sub.operator(sm.WTD_OT_scene_remove.bl_idname, text="", icon="X").instance = instance.name
lods = sm_library.lods(library, instance)
if lods:
current = sm_library.current_lod(library, instance)
sub = col.row(align=True)
sub.label(text="LOD", icon="BLANK1")
for name in lods:
op = sub.operator(sm.WTD_OT_scene_set_lod.bl_idname, text=name, depress=name == current)
op.lod, op.library, op.instance = name, library.name, instance.name
layout.separator()
row = layout.row(align=True)
row.operator(sm.WTD_OT_scene_update.bl_idname, text="Update Selected").selected = True
row.operator(sm.WTD_OT_scene_remove.bl_idname, text="Remove Selected").selected = True
selected = [
(library, instance)
for library, instance in sm_library.selected_instances(context.view_layer)
if sm_library.lods(library, instance)
]
if selected:
col = layout.column(align=True)
col.label(text="LOD of Selected")
row = col.row(align=True)
shown = {sm_library.current_lod(library, instance) for library, instance in selected}
names = {name for library, instance in selected for name in sm_library.lods(library, instance)}
for name in sorted(names, key=sm_library.lod_order):
op = row.operator(sm.WTD_OT_scene_set_lod.bl_idname, text=name, depress=shown == {name})
op.lod, op.selected = name, True
class _SceneAssetizePanel:
bl_region_type = "UI"
bl_category = "WTD"
bl_label = "Assetize"
bl_parent_id = "WTD_PT_scene_manager_view3d"
bl_options = {"DEFAULT_CLOSED"}
def draw(self, context) -> None:
layout = self.layout
_wrapped_label(
layout, context, "Publish a local collection of this scene to Libreflow as an asset.", icon="INFO"
)
layout.operator(sm.WTD_OT_scene_assetize.bl_idname, text="Assetize Collection...", icon="EXPORT")
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_scene_manager_view3d(_SceneManagerPanel, bpy.types.Panel):
bl_idname = "WTD_PT_scene_manager_view3d"
bl_space_type = "VIEW_3D"
class WTD_PT_scene_casting_view3d(_SceneCastingPanel, bpy.types.Panel):
bl_idname = "WTD_PT_scene_casting_view3d"
bl_space_type = "VIEW_3D"
class WTD_PT_scene_loaded_view3d(_SceneLoadedPanel, bpy.types.Panel):
bl_idname = "WTD_PT_scene_loaded_view3d"
bl_space_type = "VIEW_3D"
class WTD_PT_scene_assetize_view3d(_SceneAssetizePanel, bpy.types.Panel):
bl_idname = "WTD_PT_scene_assetize_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_scene_manager_view3d,
WTD_PT_scene_casting_view3d,
WTD_PT_scene_loaded_view3d,
WTD_PT_scene_assetize_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)