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>
This commit is contained in:
1 parent
6ec2b228ae
commit
fbc7ac18e4
9 files changed
+307
-32
No files matched your search
@@ -163,8 +163,10 @@ def add_instance(scene, view_layer, library, parent=None):
|
||||
scene.collection.children.unlink(instance)
|
||||
if parent.children.get(instance.name) != instance:
|
||||
parent.children.link(instance)
|
||||
instance.name = numbered_name(collection.name, bpy.data.collections)
|
||||
else:
|
||||
instance = bpy.data.objects.new(library.get("wtd_asset_name", collection.name), None)
|
||||
name = numbered_name(library.get("wtd_asset_name", collection.name), bpy.data.objects)
|
||||
instance = bpy.data.objects.new(name, None)
|
||||
instance.instance_type = "COLLECTION"
|
||||
instance.instance_collection = collection
|
||||
parent.objects.link(instance)
|
||||
@@ -172,6 +174,17 @@ def add_instance(scene, view_layer, library, parent=None):
|
||||
return instance
|
||||
|
||||
|
||||
def numbered_name(base, ids):
|
||||
"""config SCENE_MANAGER_INSTANCE_NAME for the number after the highest one ``ids`` use
|
||||
(bob_01, bob_02...; removing bob_01 does not free its number while bob_02 exists)."""
|
||||
template = config.SCENE_MANAGER_INSTANCE_NAME
|
||||
before, _, rest = template.partition("{number")
|
||||
after = rest.partition("}")[2]
|
||||
pattern = re.compile(re.escape(before.format(name=base)) + r"(\d+)" + re.escape(after.format(name=base)) + "$")
|
||||
numbers = [int(match.group(1)) for id in ids if (match := pattern.match(id.name))]
|
||||
return template.format(name=base, number=max(numbers, default=0) + 1)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Levels of detail
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
+205
-16
@@ -5,6 +5,7 @@ dialog runs in the background and applies its result on the main thread.
|
||||
What comes back from Libreflow is cached in ``STATE`` (not saved with the file).
|
||||
"""
|
||||
|
||||
import fnmatch
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
@@ -13,7 +14,7 @@ import time
|
||||
|
||||
import bpy
|
||||
from bpy.app.handlers import persistent
|
||||
from bpy.props import BoolProperty, EnumProperty, PointerProperty, StringProperty
|
||||
from bpy.props import BoolProperty, EnumProperty, IntProperty, PointerProperty, StringProperty
|
||||
|
||||
from .. import config
|
||||
from . import ipc
|
||||
@@ -33,7 +34,8 @@ class _State:
|
||||
self.files = {} # file oid -> {latest, latest_available} or {error}
|
||||
self.revisions = {} # file oid -> wtd_get_revisions result
|
||||
self.shots = [] # wtd_list_shots, for the shot picker
|
||||
self.assets = [] # wtd_list_assets, for the assetize dialog
|
||||
self.assets = [] # wtd_list_assets, for Load Publish and the assetize dialog
|
||||
self.browse = {} # Load Publish: {asset, files: [wtd_get_asset_files + revisions]}
|
||||
self.message = ("", "INFO") # last outcome, shown in the panel
|
||||
|
||||
|
||||
@@ -480,6 +482,190 @@ class WTD_OT_scene_load(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Load any publish
|
||||
# --------------------------------------------------------------------------- #
|
||||
def project_oid(scene):
|
||||
"""Project of the scene's context, else of the oid open in Libreflow ("/melvile")."""
|
||||
oid = (context_oid(scene) or current_session().get("current_oid") or "").strip("/")
|
||||
return "/" + oid.split("/")[0] if oid else ""
|
||||
|
||||
|
||||
def default_file(asset_type, files):
|
||||
"""The file the casting would load (config SCENE_MANAGER_FILE_PATTERNS), else the first one."""
|
||||
patterns = config.SCENE_MANAGER_FILE_PATTERNS
|
||||
for pattern in patterns.get(asset_type, patterns.get("*", [])):
|
||||
found = next((f for f in files if fnmatch.fnmatchcase(f["label"], pattern)), None)
|
||||
if found:
|
||||
return found
|
||||
return files[0]
|
||||
|
||||
|
||||
def default_revision(file):
|
||||
"""Latest revision on this site, else the latest one."""
|
||||
revisions = file.get("revisions") or []
|
||||
return next((r for r in revisions if r.get("available")), revisions[0] if revisions else None)
|
||||
|
||||
|
||||
_BROWSE_ASSET_ITEMS = []
|
||||
_BROWSE_FILE_ITEMS = []
|
||||
_BROWSE_REVISION_ITEMS = {} # file oid -> enum items (kept alive for Blender)
|
||||
|
||||
|
||||
def _browse_asset_items(self, context):
|
||||
return _BROWSE_ASSET_ITEMS
|
||||
|
||||
|
||||
def _browse_file_items(self, context):
|
||||
return _BROWSE_FILE_ITEMS
|
||||
|
||||
|
||||
def _browse_revision_items(self, context):
|
||||
return _BROWSE_REVISION_ITEMS.get(self.file, [])
|
||||
|
||||
|
||||
def _browse_file_update(self, context):
|
||||
file = next((f for f in STATE.browse.get("files", []) if f["file_oid"] == self.file), None)
|
||||
revision = default_revision(file) if file else None
|
||||
if revision is not None:
|
||||
self.revision = revision["name"]
|
||||
|
||||
|
||||
class WTD_OT_scene_browse(bpy.types.Operator):
|
||||
bl_idname = "wtd_scene.browse"
|
||||
bl_label = "Load Publish"
|
||||
bl_description = "Search any asset of the project, then load one of its published files at any version"
|
||||
bl_property = "asset"
|
||||
|
||||
asset: EnumProperty(items=_browse_asset_items)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return connected()
|
||||
|
||||
def invoke(self, context, event):
|
||||
project = project_oid(context.scene)
|
||||
if not project:
|
||||
self.report({"WARNING"}, "Set a context oid (or open the project in Libreflow) first")
|
||||
return {"CANCELLED"}
|
||||
context.window.cursor_set("WAIT")
|
||||
try:
|
||||
STATE.assets = ipc.call("wtd_list_assets", session_uid(), oid=project)
|
||||
except ipc.LibreflowError as exc:
|
||||
self.report({"WARNING"}, f"Assets: {exc}")
|
||||
return {"CANCELLED"}
|
||||
finally:
|
||||
context.window.cursor_set("DEFAULT")
|
||||
assets = sorted(STATE.assets, key=lambda a: (a["asset"].lower(), a.get("asset_type") or ""))
|
||||
_BROWSE_ASSET_ITEMS[:] = [(a["oid"], f"{a['asset']} ({a.get('asset_type') or ''})", a["oid"]) for a in assets]
|
||||
if not _BROWSE_ASSET_ITEMS:
|
||||
self.report({"WARNING"}, f"No asset in {project}")
|
||||
return {"CANCELLED"}
|
||||
context.window_manager.invoke_search_popup(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
asset = next((a for a in STATE.assets if a["oid"] == self.asset), None)
|
||||
if asset is None:
|
||||
return {"CANCELLED"}
|
||||
context.window.cursor_set("WAIT")
|
||||
try:
|
||||
browse_asset(asset)
|
||||
except ipc.LibreflowError as exc:
|
||||
self.report({"WARNING"}, f"{asset['asset']}: {exc}")
|
||||
return {"CANCELLED"}
|
||||
finally:
|
||||
context.window.cursor_set("DEFAULT")
|
||||
if not STATE.browse["files"]:
|
||||
self.report({"WARNING"}, f"{asset['asset']}: no published .blend file")
|
||||
return {"CANCELLED"}
|
||||
bpy.ops.wtd_scene.load_publish("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def browse_asset(asset):
|
||||
"""Read the published files of an asset and all their revisions, for the Load Publish dialog."""
|
||||
uid = session_uid()
|
||||
files = ipc.call("wtd_get_asset_files", uid, oid=asset["oid"])
|
||||
for file in files:
|
||||
file["revisions"] = ipc.call("wtd_get_revisions", uid, file_oid=file["file_oid"])
|
||||
files = [f for f in files if f["revisions"]]
|
||||
STATE.browse = {"asset": asset, "files": files}
|
||||
_BROWSE_FILE_ITEMS[:] = [(f["file_oid"], f["label"], f["file_oid"], index) for index, f in enumerate(files)]
|
||||
_BROWSE_REVISION_ITEMS.clear()
|
||||
for file in files:
|
||||
_BROWSE_REVISION_ITEMS[file["file_oid"]] = [
|
||||
(r["name"], revision_label(r), r.get("comment") or "", index) for index, r in enumerate(file["revisions"])
|
||||
]
|
||||
|
||||
|
||||
class WTD_OT_scene_load_publish(bpy.types.Operator):
|
||||
bl_idname = "wtd_scene.load_publish"
|
||||
bl_label = "Load Publish"
|
||||
bl_description = "Load a published file of the asset picked in Load Publish"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
file: EnumProperty(name="File", items=_browse_file_items, update=_browse_file_update)
|
||||
revision: EnumProperty(name="Version", items=_browse_revision_items)
|
||||
count: IntProperty(name="Instances", default=1, min=1, max=50)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return connected() and bool(STATE.browse)
|
||||
|
||||
def invoke(self, context, event):
|
||||
asset = STATE.browse["asset"]
|
||||
self.file = default_file(asset.get("asset_type") or "", STATE.browse["files"])["file_oid"]
|
||||
_browse_file_update(self, context)
|
||||
title = f"Load {asset['asset']} ({asset.get('asset_type') or ''})"
|
||||
return context.window_manager.invoke_props_dialog(self, width=450, title=title, confirm_text="Load")
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "file")
|
||||
layout.prop(self, "revision")
|
||||
layout.prop(self, "count")
|
||||
library = lib.find_library(self.file)
|
||||
if library is not None:
|
||||
layout.label(text=f"Loaded at {library.get('wtd_revision')}: adds instances of that version", icon="INFO")
|
||||
|
||||
def execute(self, context):
|
||||
asset = STATE.browse["asset"]
|
||||
file = next(f for f in STATE.browse["files"] if f["file_oid"] == self.file)
|
||||
revision = next(r for r in file["revisions"] if r["name"] == self.revision)
|
||||
name = asset["asset"]
|
||||
if not revision.get("available"):
|
||||
if revision.get("sync_status") == "Requested":
|
||||
self.report({"WARNING"}, f"{name} {revision['name']} is requested, waiting for the download")
|
||||
else:
|
||||
request_revisions({revision["oid"]: file["file_oid"]})
|
||||
self.report({"INFO"}, f"{name} {revision['name']} is not on this site: requested, load it once downloaded")
|
||||
return {"CANCELLED"}
|
||||
library = lib.find_library(file["file_oid"])
|
||||
if library is not None and library.get("wtd_revision") != revision["name"]:
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
f"{name} {file['label']} is loaded at {library.get('wtd_revision')}: "
|
||||
f"switch it with the version picker of the Loaded panel",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
# Known file: the Loaded panel can tell whether it is the latest version.
|
||||
STATE.files[file["file_oid"]] = {k: file.get(k) for k in ("file_oid", "label", "latest", "latest_available")}
|
||||
try:
|
||||
if library is None:
|
||||
library = lib.link_asset(
|
||||
{"name": name, "oid": asset["oid"], "asset_type": asset.get("asset_type") or "", "file": file},
|
||||
revision,
|
||||
context_oid(context.scene),
|
||||
)
|
||||
added = [lib.add_instance(context.scene, context.view_layer, library).name for _ in range(self.count)]
|
||||
except (lib.LoadError, RuntimeError) as exc:
|
||||
self.report({"WARNING"}, str(exc) if isinstance(exc, lib.LoadError) else f"{name}: {exc}")
|
||||
return {"CANCELLED"}
|
||||
report(f"Loaded {name} {revision['name']}: {', '.join(added)}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Update / versions
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -576,6 +762,19 @@ class WTD_OT_scene_request(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def revision_label(revision):
|
||||
"""'v002 2026-10-27 nero', marked when the revision is not on this site."""
|
||||
r = revision
|
||||
date = time.strftime("%Y-%m-%d", time.localtime(r["date"])) if r.get("date") else ""
|
||||
if r.get("available"):
|
||||
where = ""
|
||||
elif r.get("sync_status") == "Requested":
|
||||
where = " (requested)"
|
||||
else:
|
||||
where = " (not on this site: request)"
|
||||
return f"{r['name']} {date} {r.get('user') or ''}{where}"
|
||||
|
||||
|
||||
def _revision_items(self, context):
|
||||
return _REVISION_ITEMS
|
||||
|
||||
@@ -611,19 +810,7 @@ class WTD_OT_scene_pick_revision(bpy.types.Operator):
|
||||
context.window.cursor_set("DEFAULT")
|
||||
current = library.get("wtd_revision")
|
||||
_REVISION_ITEMS[:] = [
|
||||
(
|
||||
r["name"],
|
||||
f"{r['name']} {time.strftime('%Y-%m-%d', time.localtime(r['date'])) if r.get('date') else ''}"
|
||||
f" {r.get('user') or ''}"
|
||||
+ (
|
||||
""
|
||||
if r.get("available")
|
||||
else " (requested)" if r.get("sync_status") == "Requested" else " (not on this site: request)"
|
||||
),
|
||||
r.get("comment") or "",
|
||||
"CHECKMARK" if r["name"] == current else "NONE",
|
||||
index,
|
||||
)
|
||||
(r["name"], revision_label(r), r.get("comment") or "", "CHECKMARK" if r["name"] == current else "NONE", index)
|
||||
for index, r in enumerate(STATE.revisions[file_oid])
|
||||
]
|
||||
if not _REVISION_ITEMS:
|
||||
@@ -817,7 +1004,7 @@ def replace_with_publish(scene, view_layer, collection_name, asset, context_oid_
|
||||
parents = lib.parents_of(local, scene)
|
||||
for parent in parents:
|
||||
parent.children.unlink(local)
|
||||
# Kept in the file, out of the way: the published asset takes its name.
|
||||
# Kept in the file, out of the way: the published asset takes its place (numbered, COL_x_01).
|
||||
local.use_fake_user = True
|
||||
local.name = f"{collection_name}_local"
|
||||
lib.add_instance(scene, view_layer, library, parent=parents[0] if parents else None)
|
||||
@@ -976,6 +1163,8 @@ classes = (
|
||||
WTD_OT_scene_pick_shot,
|
||||
WTD_OT_scene_refresh,
|
||||
WTD_OT_scene_load,
|
||||
WTD_OT_scene_browse,
|
||||
WTD_OT_scene_load_publish,
|
||||
WTD_OT_scene_update,
|
||||
WTD_OT_scene_request,
|
||||
WTD_OT_scene_pick_revision,
|
||||
|
||||
Reference in new issue
Block a user