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>
1198 lines
47 KiB
Python
1198 lines
47 KiB
Python
"""Scene Manager operators: connection, shot context, casting, load, update, LOD, remove, assetize.
|
|
|
|
Libreflow is only reached through ``ipc``; every request but the ones filling a
|
|
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
|
|
import tempfile
|
|
import time
|
|
|
|
import bpy
|
|
from bpy.app.handlers import persistent
|
|
from bpy.props import BoolProperty, EnumProperty, IntProperty, PointerProperty, StringProperty
|
|
|
|
from .. import config
|
|
from . import ipc
|
|
from . import library as lib
|
|
|
|
|
|
class _State:
|
|
def __init__(self):
|
|
self.clear()
|
|
|
|
def clear(self):
|
|
self.sessions = {} # session uid -> {session_name, cluster_name, is_gui, current_oid}
|
|
self.connection_error = "Not connected"
|
|
self.ping = {} # wtd_ping: version, project, user, site
|
|
self.contexts = {} # oid -> context, or {"error": message}
|
|
self.casting = {} # context oid -> wtd_get_casting_publishes result
|
|
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 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
|
|
|
|
|
|
STATE = _State()
|
|
# Loaded assets unfolded in the panel (library names), UI state only.
|
|
EXPANDED: set[str] = set()
|
|
|
|
|
|
def report(message, level="INFO"):
|
|
STATE.message = (message, level)
|
|
print(f"[WTD Scene Manager] {message}")
|
|
|
|
|
|
def _undo_push(message):
|
|
try:
|
|
bpy.ops.ed.undo_push(message=message)
|
|
except RuntimeError:
|
|
pass
|
|
|
|
|
|
def _on_error(what, quiet=False):
|
|
def callback(exc):
|
|
if isinstance(exc, ipc.NotRunning):
|
|
STATE.sessions = {}
|
|
STATE.connection_error = str(exc)
|
|
if not quiet:
|
|
report(f"{what}: {exc}", "ERROR")
|
|
|
|
return callback
|
|
|
|
|
|
def session_uid():
|
|
"""The chosen session, None when Libreflow picks it (a single session connected)."""
|
|
uid = bpy.context.window_manager.wtd_libreflow.session_uid
|
|
return uid if uid in STATE.sessions else None
|
|
|
|
|
|
def current_session():
|
|
return STATE.sessions.get(session_uid() or "") or next(iter(STATE.sessions.values()), {})
|
|
|
|
|
|
def connected():
|
|
return bool(STATE.sessions)
|
|
|
|
|
|
def context_oid(scene):
|
|
return scene.wtd_scene_manager.context_oid.strip()
|
|
|
|
|
|
def main_scene():
|
|
"""Scene of the main window (bpy.context.scene is not reliable in timers)."""
|
|
windows = bpy.context.window_manager.windows
|
|
return windows[0].scene if windows else bpy.context.scene
|
|
|
|
|
|
def context_label(oid):
|
|
"""'f01 / s010 / p010' for a validated oid, or the reason it is not valid."""
|
|
ctx = STATE.contexts.get(oid)
|
|
if ctx is None:
|
|
return "", False
|
|
if "error" in ctx:
|
|
return ctx["error"], False
|
|
keys = ("film", "sequence", "shot") if ctx["kind"].startswith("shot") else ("asset_type", "asset")
|
|
parts = [ctx[key] for key in keys if ctx.get(key)]
|
|
return f"{ctx['kind']}: {' / '.join(parts) or ctx.get('project', '')}", True
|
|
|
|
|
|
def is_shot_context(oid):
|
|
ctx = STATE.contexts.get(oid) or {}
|
|
return ctx.get("kind", "").startswith("shot")
|
|
|
|
|
|
def version_status(library):
|
|
"""(state, text) of a loaded asset's revision: state is "LATEST", "UPDATE" (a newer one
|
|
is available), "PENDING" (newer one published, not on this site yet) or "UNKNOWN"."""
|
|
data = STATE.files.get(library.get("wtd_file_oid"))
|
|
if data is None:
|
|
return "UNKNOWN", "not checked"
|
|
if data.get("error"):
|
|
return "UNKNOWN", data["error"]
|
|
current = library.get("wtd_revision")
|
|
latest, available = data.get("latest"), data.get("latest_available")
|
|
if latest is None or latest["name"] == current:
|
|
return "LATEST", "latest"
|
|
if available is not None and available["name"] != current:
|
|
return "UPDATE", f"{available['name']} available"
|
|
if latest.get("sync_status") == "Requested":
|
|
return "PENDING", f"{latest['name']} requested, waiting for the download"
|
|
return "PENDING", f"{latest['name']} published, not available on this site yet"
|
|
|
|
|
|
def file_data(file_oid, default=None):
|
|
"""Latest known revisions of a file (refreshed by requests and version checks)."""
|
|
return STATE.files.get(file_oid) or default
|
|
|
|
|
|
def to_request(file):
|
|
"""The latest revision of a file when it is not on this site and not requested yet, else None."""
|
|
latest = (file or {}).get("latest")
|
|
if latest and latest.get("oid") and not latest.get("available") and latest.get("sync_status") != "Requested":
|
|
return latest
|
|
return None
|
|
|
|
|
|
def missing_files(scene):
|
|
"""File oids of the cast and loaded assets whose latest publish can be requested."""
|
|
casting = STATE.casting.get(context_oid(scene)) or {"assets": []}
|
|
file_oids = [a["file"]["file_oid"] for a in casting["assets"] if a.get("file")]
|
|
file_oids += [library["wtd_file_oid"] for library in lib.managed_libraries()]
|
|
return [oid for oid in dict.fromkeys(file_oids) if to_request(file_data(oid))]
|
|
|
|
|
|
def _asset_of(file_oid):
|
|
return file_oid.split("/assets/", 1)[-1].split("/", 1)[0]
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Background requests
|
|
# --------------------------------------------------------------------------- #
|
|
def connect(scene, quiet=False):
|
|
"""Read the sessions (and who / where we are) in the background, then check the scene's context."""
|
|
wanted = bpy.context.window_manager.wtd_libreflow.session_uid
|
|
oid, scene_name = context_oid(scene), scene.name
|
|
|
|
def fetch():
|
|
sessions = ipc.sessions()
|
|
uid = wanted if wanted in sessions else (next(iter(sessions)) if len(sessions) == 1 else "")
|
|
ping = {}
|
|
if uid:
|
|
target = oid or sessions[uid].get("current_oid")
|
|
try:
|
|
ping = ipc.call("wtd_ping", uid, oid=target) if target else ipc.call("wtd_ping", uid)
|
|
except ipc.LibreflowError:
|
|
ping = ipc.call("wtd_ping", uid)
|
|
return sessions, uid, ping
|
|
|
|
def done(result):
|
|
sessions, uid, ping = result
|
|
STATE.sessions, STATE.ping = sessions, ping
|
|
STATE.connection_error = "" if sessions else "No Libreflow session connected"
|
|
bpy.context.window_manager.wtd_libreflow.session_uid = uid
|
|
scene = bpy.data.scenes.get(scene_name)
|
|
if scene is not None and context_oid(scene):
|
|
validate_context(scene, quiet=quiet)
|
|
|
|
ipc.run("Connecting to Libreflow", fetch, done, _on_error("Connection", quiet))
|
|
|
|
|
|
def validate_context(scene, quiet=False):
|
|
"""Check the context oid; for a shot, then fetch its casting and the loaded versions."""
|
|
oid, uid, scene_name = context_oid(scene), session_uid(), scene.name
|
|
if not oid:
|
|
return
|
|
|
|
def done(ctx):
|
|
STATE.contexts[oid] = ctx
|
|
scene = bpy.data.scenes.get(scene_name)
|
|
if scene is not None and ctx["kind"].startswith("shot"):
|
|
fetch_casting(scene, quiet=quiet)
|
|
refresh_versions(quiet=quiet)
|
|
|
|
def failed(exc):
|
|
if not isinstance(exc, ipc.NotRunning):
|
|
STATE.contexts[oid] = {"error": str(exc)}
|
|
_on_error("Context", quiet)(exc)
|
|
|
|
ipc.run("Checking the context", lambda: ipc.call("wtd_get_context", uid, oid=oid), done, failed)
|
|
|
|
|
|
def _casting_publishes(uid, oid):
|
|
"""Casting with a file per asset, the file chosen with the patterns of its asset type
|
|
(config SCENE_MANAGER_FILE_PATTERNS)."""
|
|
patterns = config.SCENE_MANAGER_FILE_PATTERNS
|
|
default = patterns.get("*", ["*/*.blend"])
|
|
casting = ipc.call("wtd_get_casting_publishes", uid, oid=oid, file_patterns=default)
|
|
specific = {asset_type: p for asset_type, p in patterns.items() if asset_type != "*" and p != default}
|
|
for asset_patterns in {tuple(p) for p in specific.values()}:
|
|
types = {t for t, p in specific.items() if tuple(p) == asset_patterns}
|
|
if not any(asset["asset_type"] in types for asset in casting["assets"]):
|
|
continue
|
|
other = ipc.call("wtd_get_casting_publishes", uid, oid=oid, file_patterns=list(asset_patterns))
|
|
by_key = {(a["asset_type"], a["name"]): a for a in other["assets"]}
|
|
casting["assets"] = [
|
|
by_key.get((a["asset_type"], a["name"]), a) if a["asset_type"] in types else a
|
|
for a in casting["assets"]
|
|
]
|
|
return casting
|
|
|
|
|
|
def _store_casting(oid, casting):
|
|
STATE.casting[oid] = casting
|
|
STATE.files.update({a["file"]["file_oid"]: a["file"] for a in casting["assets"] if a.get("file")})
|
|
|
|
|
|
def fetch_casting(scene, then=None, quiet=False):
|
|
oid, uid = context_oid(scene), session_uid()
|
|
|
|
def done(casting):
|
|
_store_casting(oid, casting)
|
|
if then is not None:
|
|
then(casting)
|
|
|
|
ipc.run("Reading the casting", lambda: _casting_publishes(uid, oid), done, _on_error("Casting", quiet))
|
|
|
|
|
|
def refresh_versions(quiet=False):
|
|
file_oids = [library["wtd_file_oid"] for library in lib.managed_libraries()]
|
|
if not file_oids:
|
|
return
|
|
uid = session_uid()
|
|
ipc.run(
|
|
"Checking versions",
|
|
lambda: ipc.call("wtd_get_latest_revisions", uid, file_oids=file_oids),
|
|
STATE.files.update,
|
|
_on_error("Versions", quiet),
|
|
)
|
|
|
|
|
|
def request_revisions(revisions):
|
|
"""Ask Libreflow to download revisions on this site ({revision oid: file oid})."""
|
|
uid = session_uid()
|
|
|
|
def done(result):
|
|
requested, messages = [], []
|
|
for oid, data in result.items():
|
|
file_oid = revisions.get(oid, "")
|
|
what = f"{_asset_of(file_oid)} {data.get('name') or oid.rsplit('/', 1)[-1]}"
|
|
if data.get("error"):
|
|
messages.append(f"{what}: {data['error']}")
|
|
continue
|
|
file = STATE.files.get(file_oid)
|
|
if file is not None and (file.get("latest") or {}).get("oid") == oid:
|
|
file["latest"] = data
|
|
if data.get("available"):
|
|
file["latest_available"] = data
|
|
for revision in STATE.revisions.get(file_oid, []):
|
|
if revision.get("oid") == oid:
|
|
revision.update(data)
|
|
if data.get("available"):
|
|
messages.append(f"{what} is already on this site: refresh")
|
|
else:
|
|
requested.append(what)
|
|
text = f"Requested {', '.join(requested)}" if requested else "Nothing requested"
|
|
report(text + ("; " + "; ".join(messages) if messages else ""), "WARNING" if messages else "INFO")
|
|
|
|
ipc.run(
|
|
"Requesting",
|
|
lambda: ipc.call("wtd_request_revisions", uid, revision_oids=list(revisions)),
|
|
done,
|
|
_on_error("Request"),
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Settings
|
|
# --------------------------------------------------------------------------- #
|
|
def _context_oid_update(self, context):
|
|
if connected():
|
|
validate_context(self.id_data)
|
|
|
|
|
|
class WTD_SceneManagerSettings(bpy.types.PropertyGroup):
|
|
"""Per-file Scene Manager settings, saved in the .blend."""
|
|
|
|
context_oid: StringProperty(
|
|
name="Context",
|
|
description="Libreflow oid of the shot this scene belongs to",
|
|
update=_context_oid_update,
|
|
)
|
|
|
|
|
|
class WTD_LibreflowSettings(bpy.types.PropertyGroup):
|
|
"""Libreflow connection, for this Blender session."""
|
|
|
|
session_uid: StringProperty(name="Session", description="Libreflow session the commands are sent to")
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Connection / context
|
|
# --------------------------------------------------------------------------- #
|
|
class WTD_OT_scene_connect(bpy.types.Operator):
|
|
bl_idname = "wtd_scene.connect"
|
|
bl_label = "Connect"
|
|
bl_description = "Connect to the running Libreflow (or refresh the sessions, context, casting and versions)"
|
|
|
|
def execute(self, context):
|
|
connect(context.scene)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class WTD_OT_scene_select_session(bpy.types.Operator):
|
|
bl_idname = "wtd_scene.select_session"
|
|
bl_label = "Select Session"
|
|
bl_description = "Send the commands to this Libreflow session"
|
|
|
|
uid: StringProperty()
|
|
|
|
def execute(self, context):
|
|
context.window_manager.wtd_libreflow.session_uid = self.uid
|
|
connect(context.scene)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class WTD_OT_scene_use_current_oid(bpy.types.Operator):
|
|
bl_idname = "wtd_scene.use_current_oid"
|
|
bl_label = "Use Libreflow Current Oid"
|
|
bl_description = "Set the context to the oid shown in the Libreflow window"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return connected()
|
|
|
|
def execute(self, context):
|
|
oid = current_session().get("current_oid")
|
|
if not oid:
|
|
self.report({"WARNING"}, "Libreflow shows no oid")
|
|
return {"CANCELLED"}
|
|
context.scene.wtd_scene_manager.context_oid = oid
|
|
return {"FINISHED"}
|
|
|
|
|
|
def _shot_items(self, context):
|
|
return _SHOT_ITEMS
|
|
|
|
|
|
_SHOT_ITEMS = []
|
|
|
|
|
|
class WTD_OT_scene_pick_shot(bpy.types.Operator):
|
|
bl_idname = "wtd_scene.pick_shot"
|
|
bl_label = "Pick Shot"
|
|
bl_description = "Set the context to a shot of the project"
|
|
bl_property = "shot"
|
|
|
|
shot: EnumProperty(items=_shot_items)
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return connected()
|
|
|
|
def invoke(self, context, event):
|
|
uid = session_uid()
|
|
oid = context_oid(context.scene) or current_session().get("current_oid")
|
|
if not oid:
|
|
self.report({"WARNING"}, "Set a context oid (or open the project in Libreflow) first")
|
|
return {"CANCELLED"}
|
|
context.window.cursor_set("WAIT")
|
|
try:
|
|
STATE.shots = ipc.call("wtd_list_shots", uid, oid=oid)
|
|
except ipc.LibreflowError as exc:
|
|
self.report({"WARNING"}, f"Shots: {exc}")
|
|
return {"CANCELLED"}
|
|
finally:
|
|
context.window.cursor_set("DEFAULT")
|
|
_SHOT_ITEMS[:] = [
|
|
(s["oid"], f"{s.get('film', '')} / {s.get('sequence', '')} / {s.get('shot', '')}", s["oid"])
|
|
for s in STATE.shots
|
|
]
|
|
if not _SHOT_ITEMS:
|
|
self.report({"WARNING"}, "No shot in the project")
|
|
return {"CANCELLED"}
|
|
context.window_manager.invoke_search_popup(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
def execute(self, context):
|
|
context.scene.wtd_scene_manager.context_oid = self.shot
|
|
return {"FINISHED"}
|
|
|
|
|
|
class WTD_OT_scene_refresh(bpy.types.Operator):
|
|
bl_idname = "wtd_scene.refresh"
|
|
bl_label = "Refresh"
|
|
bl_description = "Read the casting and the latest versions of the loaded assets again"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return connected()
|
|
|
|
def execute(self, context):
|
|
if is_shot_context(context_oid(context.scene)):
|
|
fetch_casting(context.scene)
|
|
refresh_versions()
|
|
return {"FINISHED"}
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Load
|
|
# --------------------------------------------------------------------------- #
|
|
def load_casting(scene, view_layer, casting, context_oid_value, only=None):
|
|
"""Link the missing assets of the casting and create their missing instances.
|
|
Returns (number of instances added, error messages)."""
|
|
added, errors = 0, []
|
|
for asset in casting["assets"]:
|
|
if only and asset["oid"] != only:
|
|
continue
|
|
if asset.get("error") or not asset.get("file"):
|
|
errors.append(f"{asset['name']}: {asset.get('error') or 'nothing to load'}")
|
|
continue
|
|
try:
|
|
library = lib.find_library(asset["file"]["file_oid"]) or lib.link_asset(
|
|
asset, asset["file"]["latest_available"], context_oid_value
|
|
)
|
|
wanted = asset.get("nb_occurences") or 1
|
|
for _ in range(wanted - len(lib.instances(library))):
|
|
lib.add_instance(scene, view_layer, library)
|
|
added += 1
|
|
except (lib.LoadError, RuntimeError) as exc:
|
|
errors.append(str(exc) if isinstance(exc, lib.LoadError) else f"{asset['name']}: {exc}")
|
|
return added, errors
|
|
|
|
|
|
class WTD_OT_scene_load(bpy.types.Operator):
|
|
bl_idname = "wtd_scene.load"
|
|
bl_label = "Load"
|
|
bl_description = "Link the latest available publish of the cast assets that are not loaded yet"
|
|
|
|
asset_oid: StringProperty(description="Only this asset", options={"SKIP_SAVE"})
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return connected() and is_shot_context(context_oid(context.scene))
|
|
|
|
def execute(self, context):
|
|
oid = context_oid(context.scene)
|
|
scene_name, view_layer_name, only = context.scene.name, context.view_layer.name, self.asset_oid
|
|
|
|
def apply(casting):
|
|
scene = bpy.data.scenes.get(scene_name)
|
|
if scene is None:
|
|
return
|
|
added, errors = load_casting(scene, scene.view_layers[view_layer_name], casting, oid, only)
|
|
_undo_push("Load casting")
|
|
text = f"{added} instance(s) added" if added else "Nothing new to load"
|
|
if errors:
|
|
report(f"{text}, {len(errors)} asset(s) cannot be loaded: " + "; ".join(errors), "WARNING")
|
|
else:
|
|
report(text)
|
|
|
|
cached = STATE.casting.get(oid)
|
|
if only and cached is not None:
|
|
apply(cached)
|
|
else:
|
|
fetch_casting(context.scene, then=apply)
|
|
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
|
|
# --------------------------------------------------------------------------- #
|
|
def _update_libraries(names, target=None):
|
|
"""Update libraries to their latest available revision (or to ``target``). Returns messages."""
|
|
updated, messages = [], []
|
|
for name in names:
|
|
library = bpy.data.libraries.get(name)
|
|
if library is None:
|
|
continue
|
|
revision = target
|
|
if revision is None:
|
|
state, text = version_status(library)
|
|
if state != "UPDATE":
|
|
if state == "PENDING":
|
|
messages.append(f"{name}: {text}")
|
|
continue
|
|
revision = STATE.files[library["wtd_file_oid"]]["latest_available"]
|
|
try:
|
|
lib.update(name, revision)
|
|
updated.append(f"{name} {revision['name']}")
|
|
except (lib.LoadError, RuntimeError) as exc:
|
|
messages.append(str(exc))
|
|
return updated, messages
|
|
|
|
|
|
class WTD_OT_scene_update(bpy.types.Operator):
|
|
bl_idname = "wtd_scene.update"
|
|
bl_label = "Update"
|
|
bl_description = "Switch the loaded assets to their latest available publish"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
library: StringProperty(description="Only this loaded asset", options={"SKIP_SAVE"})
|
|
selected: BoolProperty(description="Only the assets of the selected objects", options={"SKIP_SAVE"})
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return connected() and bool(lib.managed_libraries())
|
|
|
|
def execute(self, context):
|
|
if self.library:
|
|
names = [self.library]
|
|
elif self.selected:
|
|
names = sorted({library.name for library, _ in lib.selected_instances(context.view_layer)})
|
|
if not names:
|
|
self.report({"WARNING"}, "No loaded asset selected")
|
|
return {"CANCELLED"}
|
|
else:
|
|
names = [library.name for library in lib.managed_libraries()]
|
|
file_oids = [bpy.data.libraries[name]["wtd_file_oid"] for name in names]
|
|
|
|
def apply(latest):
|
|
STATE.files.update(latest)
|
|
updated, messages = _update_libraries(names)
|
|
_undo_push("Update assets")
|
|
text = f"Updated {', '.join(updated)}" if updated else "Nothing to update"
|
|
report(text + ("; " + "; ".join(messages) if messages else ""), "WARNING" if messages else "INFO")
|
|
|
|
uid = session_uid()
|
|
ipc.run(
|
|
"Updating",
|
|
lambda: ipc.call("wtd_get_latest_revisions", uid, file_oids=file_oids),
|
|
apply,
|
|
_on_error("Update"),
|
|
)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class WTD_OT_scene_request(bpy.types.Operator):
|
|
bl_idname = "wtd_scene.request"
|
|
bl_label = "Request"
|
|
bl_description = (
|
|
"Ask Libreflow to download the latest publish on this site (from the exchange site, "
|
|
"or from the site that published it). Refresh once it is downloaded"
|
|
)
|
|
|
|
file_oid: StringProperty(description="Only the latest publish of this file", options={"SKIP_SAVE"})
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return connected()
|
|
|
|
def execute(self, context):
|
|
file_oids = [self.file_oid] if self.file_oid else missing_files(context.scene)
|
|
revisions = {}
|
|
for file_oid in file_oids:
|
|
revision = to_request(file_data(file_oid))
|
|
if revision is not None:
|
|
revisions[revision["oid"]] = file_oid
|
|
if not revisions:
|
|
self.report({"INFO"}, "Nothing to request")
|
|
return {"CANCELLED"}
|
|
request_revisions(revisions)
|
|
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
|
|
|
|
|
|
_REVISION_ITEMS = []
|
|
|
|
|
|
class WTD_OT_scene_pick_revision(bpy.types.Operator):
|
|
bl_idname = "wtd_scene.pick_revision"
|
|
bl_label = "Pick Version"
|
|
bl_description = "Switch this asset to another published version (e.g. roll back)"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
library: StringProperty()
|
|
revision: EnumProperty(name="Version", items=_revision_items)
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return connected()
|
|
|
|
def invoke(self, context, event):
|
|
library = bpy.data.libraries.get(self.library)
|
|
if library is None:
|
|
return {"CANCELLED"}
|
|
file_oid = library["wtd_file_oid"]
|
|
context.window.cursor_set("WAIT")
|
|
try:
|
|
STATE.revisions[file_oid] = ipc.call("wtd_get_revisions", session_uid(), file_oid=file_oid)
|
|
except ipc.LibreflowError as exc:
|
|
self.report({"WARNING"}, f"Versions: {exc}")
|
|
return {"CANCELLED"}
|
|
finally:
|
|
context.window.cursor_set("DEFAULT")
|
|
current = library.get("wtd_revision")
|
|
_REVISION_ITEMS[:] = [
|
|
(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:
|
|
self.report({"WARNING"}, "No published version")
|
|
return {"CANCELLED"}
|
|
if current in {item[0] for item in _REVISION_ITEMS}:
|
|
self.revision = current
|
|
return context.window_manager.invoke_props_dialog(self, title=f"{self.library}: version")
|
|
|
|
def execute(self, context):
|
|
library = bpy.data.libraries.get(self.library)
|
|
if library is None:
|
|
return {"CANCELLED"}
|
|
file_oid = library["wtd_file_oid"]
|
|
revision = next(r for r in STATE.revisions[file_oid] if r["name"] == self.revision)
|
|
if not revision.get("available"):
|
|
if revision.get("sync_status") == "Requested":
|
|
self.report({"WARNING"}, f"{revision['name']} is requested, waiting for the download")
|
|
else:
|
|
request_revisions({revision["oid"]: file_oid})
|
|
self.report({"INFO"}, f"{revision['name']} is not on this site: requested, pick it again once downloaded")
|
|
return {"CANCELLED"}
|
|
updated, messages = _update_libraries([self.library], target=revision)
|
|
if messages:
|
|
self.report({"WARNING"}, "; ".join(messages))
|
|
return {"CANCELLED"}
|
|
self.report({"INFO"}, f"{self.library} switched to {revision['name']}")
|
|
return {"FINISHED"}
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Remove / select
|
|
# --------------------------------------------------------------------------- #
|
|
class WTD_OT_scene_remove(bpy.types.Operator):
|
|
bl_idname = "wtd_scene.remove"
|
|
bl_label = "Remove"
|
|
bl_description = "Remove loaded assets from the scene"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
library: StringProperty(description="Remove this whole asset (all its instances and the link)", options={"SKIP_SAVE"})
|
|
instance: StringProperty(description="Remove only this instance (object or collection name)", options={"SKIP_SAVE"})
|
|
selected: BoolProperty(description="Remove the instances of the selected objects", options={"SKIP_SAVE"})
|
|
|
|
def invoke(self, context, event):
|
|
if self.library:
|
|
return context.window_manager.invoke_confirm(
|
|
self, event, title=f"Remove {self.library}?", message="All its instances and the link to its file"
|
|
)
|
|
return self.execute(context)
|
|
|
|
def execute(self, context):
|
|
if context.mode != "OBJECT" and context.view_layer.objects.active is not None:
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
|
if self.library:
|
|
library = bpy.data.libraries.get(self.library)
|
|
if library is None:
|
|
return {"CANCELLED"}
|
|
lib.remove_library(library)
|
|
self.report({"INFO"}, f"Removed {self.library}")
|
|
return {"FINISHED"}
|
|
if self.selected:
|
|
targets = [instance for _library, instance in lib.selected_instances(context.view_layer)]
|
|
else:
|
|
targets = [
|
|
instance
|
|
for library in lib.managed_libraries()
|
|
for instance in lib.instances(library)
|
|
if instance.name == self.instance
|
|
]
|
|
if not targets:
|
|
self.report({"WARNING"}, "No loaded instance selected")
|
|
return {"CANCELLED"}
|
|
for instance in targets:
|
|
lib.remove_instance(instance)
|
|
self.report({"INFO"}, f"Removed {len(targets)} instance(s)")
|
|
return {"FINISHED"}
|
|
|
|
|
|
class WTD_OT_scene_select_instance(bpy.types.Operator):
|
|
bl_idname = "wtd_scene.select_instance"
|
|
bl_label = "Select"
|
|
bl_description = "Select the objects of this instance"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
library: StringProperty()
|
|
instance: StringProperty()
|
|
|
|
def execute(self, context):
|
|
library = bpy.data.libraries.get(self.library)
|
|
instance = next((i for i in lib.instances(library) if i.name == self.instance), None) if library else None
|
|
if instance is None:
|
|
return {"CANCELLED"}
|
|
if context.mode != "OBJECT" and context.view_layer.objects.active is not None:
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
|
view_layer = context.view_layer
|
|
objects = [ob for ob in lib.instance_objects(instance) if ob.name in view_layer.objects and ob.visible_get()]
|
|
if not objects:
|
|
self.report({"WARNING"}, "Nothing selectable (hidden or excluded)")
|
|
return {"CANCELLED"}
|
|
for ob in context.selected_objects:
|
|
ob.select_set(False)
|
|
for ob in objects:
|
|
ob.select_set(True)
|
|
view_layer.objects.active = next((ob for ob in objects if ob.type == "ARMATURE"), objects[0])
|
|
return {"FINISHED"}
|
|
|
|
|
|
class WTD_OT_scene_set_lod(bpy.types.Operator):
|
|
bl_idname = "wtd_scene.set_lod"
|
|
bl_label = "Set LOD"
|
|
bl_description = "Show this level of detail (one of the asset's LOD collections)"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
lod: StringProperty(name="LOD", description="LOD name (the {lod} part of the LOD collection name)")
|
|
library: StringProperty(options={"SKIP_SAVE"})
|
|
instance: StringProperty(description="Only this instance (object or collection name)", options={"SKIP_SAVE"})
|
|
selected: BoolProperty(description="The instances of the selected objects", options={"SKIP_SAVE"})
|
|
|
|
def execute(self, context):
|
|
if self.selected:
|
|
targets = lib.selected_instances(context.view_layer)
|
|
else:
|
|
library = bpy.data.libraries.get(self.library)
|
|
targets = [(library, i) for i in lib.instances(library) if i.name == self.instance] if library else []
|
|
targets = [(library, instance) for library, instance in targets if lib.lods(library, instance)]
|
|
if not targets:
|
|
self.report({"WARNING"}, "No loaded instance with LODs selected")
|
|
return {"CANCELLED"}
|
|
switched, errors = [], []
|
|
for library, instance in targets:
|
|
try:
|
|
lib.set_lod(library, instance, self.lod)
|
|
switched.append(instance.name)
|
|
except lib.LoadError as exc:
|
|
errors.append(str(exc))
|
|
text = f"LOD {self.lod}: {', '.join(switched)}" if switched else f"LOD {self.lod} not set"
|
|
self.report({"WARNING"} if errors else {"INFO"}, "; ".join([text, *errors]))
|
|
return {"FINISHED"} if switched else {"CANCELLED"}
|
|
|
|
|
|
class WTD_OT_scene_toggle_expand(bpy.types.Operator):
|
|
bl_idname = "wtd_scene.toggle_expand"
|
|
bl_label = "Show Instances"
|
|
bl_description = "Show or hide the instances of this asset"
|
|
|
|
library: StringProperty()
|
|
|
|
def execute(self, context):
|
|
EXPANDED.symmetric_difference_update({self.library})
|
|
return {"FINISHED"}
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Assetize
|
|
# --------------------------------------------------------------------------- #
|
|
def _collection_items(self, context):
|
|
_COLLECTION_ITEMS[:] = [(c.name, c.name, "") for c in lib.assetize_candidates()]
|
|
return _COLLECTION_ITEMS
|
|
|
|
|
|
def _asset_type_items(self, context):
|
|
types = sorted({a["asset_type"] for a in STATE.assets if a.get("asset_type")})
|
|
_ASSET_TYPE_ITEMS[:] = [(t, t, "") for t in types]
|
|
return _ASSET_TYPE_ITEMS
|
|
|
|
|
|
_COLLECTION_ITEMS = []
|
|
_ASSET_TYPE_ITEMS = []
|
|
|
|
|
|
def _existing_asset(asset_type, asset_name, asset_lib):
|
|
return any(
|
|
a.get("asset_type") == asset_type
|
|
and a.get("asset") == asset_name
|
|
and (a.get("asset_lib") or "") == (asset_lib or "")
|
|
for a in STATE.assets
|
|
)
|
|
|
|
|
|
def replace_with_publish(scene, view_layer, collection_name, asset, context_oid_value):
|
|
"""Link the published revision in place of the local collection (kept in the file, unlinked)."""
|
|
local = bpy.data.collections.get(collection_name)
|
|
revision = asset["file"].get("latest_available")
|
|
if local is None or revision is None:
|
|
raise lib.LoadError(f"{asset['name']}: published, but not available on this site to replace it")
|
|
library = lib.find_library(asset["file"]["file_oid"])
|
|
if library is not None:
|
|
library = lib.update(library.name, revision)
|
|
else:
|
|
library = lib.link_asset(asset, revision, context_oid_value)
|
|
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 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)
|
|
|
|
|
|
class WTD_OT_scene_assetize(bpy.types.Operator):
|
|
bl_idname = "wtd_scene.assetize"
|
|
bl_label = "Assetize"
|
|
bl_description = "Publish a collection of this scene to Libreflow as an asset (new asset or new version)"
|
|
|
|
collection: EnumProperty(name="Collection", items=_collection_items)
|
|
asset_type: EnumProperty(name="Asset Type", items=_asset_type_items)
|
|
asset_name: StringProperty(name="Asset Name", description="Letters, digits and underscores")
|
|
asset_lib: StringProperty(name="Asset Library", description="Publish under this asset library (optional)")
|
|
task: StringProperty(name="Task", description="Asset task to publish in")
|
|
file_name: StringProperty(name="File Name", description="Name of the .blend (defaults to the asset name)")
|
|
comment: StringProperty(name="Comment")
|
|
upload: BoolProperty(name="Upload", description="Also upload the revision to the exchange site")
|
|
replace: BoolProperty(
|
|
name="Replace with the Publish",
|
|
description="Link the published asset in place of the local collection (which stays in the file, unlinked)",
|
|
default=True,
|
|
)
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return connected()
|
|
|
|
def _oid(self, context):
|
|
return context_oid(context.scene) or current_session().get("current_oid") or ""
|
|
|
|
def invoke(self, context, event):
|
|
candidates = lib.assetize_candidates()
|
|
if not candidates:
|
|
self.report({"WARNING"}, "No local collection to publish")
|
|
return {"CANCELLED"}
|
|
oid = self._oid(context)
|
|
if not oid:
|
|
self.report({"WARNING"}, "Set a context oid first")
|
|
return {"CANCELLED"}
|
|
context.window.cursor_set("WAIT")
|
|
try:
|
|
STATE.assets = ipc.call("wtd_list_assets", session_uid(), oid=oid)
|
|
except ipc.LibreflowError as exc:
|
|
self.report({"WARNING"}, f"Assets: {exc}")
|
|
return {"CANCELLED"}
|
|
finally:
|
|
context.window.cursor_set("DEFAULT")
|
|
if not _asset_type_items(self, context):
|
|
self.report({"WARNING"}, "No asset type in the project")
|
|
return {"CANCELLED"}
|
|
active = context.collection
|
|
self.collection = active.name if active in candidates else candidates[0].name
|
|
self.asset_name = lib.asset_name_from(self.collection)
|
|
self.task = config.SCENE_MANAGER_ASSETIZE_TASK
|
|
return context.window_manager.invoke_props_dialog(self, width=420)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
layout.use_property_decorate = False
|
|
layout.prop(self, "collection")
|
|
layout.prop(self, "asset_type")
|
|
layout.prop(self, "asset_name")
|
|
exists = _existing_asset(self.asset_type, self.asset_name, self.asset_lib)
|
|
row = layout.row()
|
|
row.label(text="")
|
|
row.label(text="New version of this asset" if exists else "New asset", icon="FILE_REFRESH" if exists else "ADD")
|
|
layout.prop(self, "asset_lib")
|
|
layout.prop(self, "task")
|
|
layout.prop(self, "file_name")
|
|
layout.prop(self, "comment")
|
|
layout.prop(self, "upload")
|
|
layout.prop(self, "replace")
|
|
|
|
def execute(self, context):
|
|
name = self.asset_name.strip()
|
|
if not re.fullmatch(r"[A-Za-z0-9_]+", name or ""):
|
|
self.report({"WARNING"}, "The asset name can only use letters, digits and underscores")
|
|
return {"CANCELLED"}
|
|
collection = bpy.data.collections.get(self.collection)
|
|
if collection is None or not self.asset_type or not self.task.strip():
|
|
self.report({"WARNING"}, "Pick a collection, an asset type and a task")
|
|
return {"CANCELLED"}
|
|
|
|
folder = tempfile.mkdtemp(prefix="wtd_assetize_")
|
|
path = os.path.join(folder, f"{self.file_name.strip() or name}.blend")
|
|
lib.write_collection(collection, path)
|
|
kwargs = dict(
|
|
oid=self._oid(context),
|
|
asset_type=self.asset_type,
|
|
asset_name=name,
|
|
task=self.task.strip(),
|
|
source_path=path,
|
|
file_name=self.file_name.strip() or None,
|
|
comment=self.comment,
|
|
asset_lib=self.asset_lib.strip() or None,
|
|
create_asset=not _existing_asset(self.asset_type, name, self.asset_lib.strip()),
|
|
upload=self.upload,
|
|
)
|
|
uid, replace, asset_type = session_uid(), self.replace, self.asset_type
|
|
scene_name, view_layer_name, collection_name = context.scene.name, context.view_layer.name, collection.name
|
|
|
|
def done(result):
|
|
shutil.rmtree(folder, ignore_errors=True)
|
|
STATE.files[result["file_oid"]] = result
|
|
revision = (result.get("latest") or {}).get("name", "")
|
|
created = " (new asset)" if result.get("asset_created") else ""
|
|
message = f"Published {name} {revision}{created}"
|
|
if replace and (scene := bpy.data.scenes.get(scene_name)) is not None:
|
|
asset = {"name": name, "oid": result["asset_oid"], "asset_type": asset_type, "file": result}
|
|
try:
|
|
replace_with_publish(
|
|
scene, scene.view_layers[view_layer_name], collection_name, asset, kwargs["oid"]
|
|
)
|
|
_undo_push("Replace with the publish")
|
|
message += ", the collection is replaced by its link"
|
|
except (lib.LoadError, RuntimeError) as exc:
|
|
report(f"{message}, not replaced: {exc}", "WARNING")
|
|
return
|
|
report(message)
|
|
|
|
def failed(exc):
|
|
shutil.rmtree(folder, ignore_errors=True)
|
|
_on_error(f"Publishing {name}")(exc)
|
|
|
|
ipc.run(f"Publishing {name}", lambda: ipc.call("wtd_assetize", uid, **kwargs), done, failed)
|
|
return {"FINISHED"}
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Registration
|
|
# --------------------------------------------------------------------------- #
|
|
@persistent
|
|
def _on_load_post(*_args):
|
|
"""New file: forget the cached answers, then reconnect in the background."""
|
|
ipc.reset()
|
|
sessions, error = STATE.sessions, STATE.connection_error
|
|
STATE.clear()
|
|
STATE.sessions, STATE.connection_error = sessions, error
|
|
bpy.app.timers.register(_auto_connect, first_interval=0.5)
|
|
|
|
|
|
def _auto_connect():
|
|
if os.path.isfile(os.path.expanduser(config.LIBREFLOW_SERVER_FILE)):
|
|
connect(main_scene(), quiet=True)
|
|
return None
|
|
|
|
|
|
classes = (
|
|
WTD_SceneManagerSettings,
|
|
WTD_LibreflowSettings,
|
|
WTD_OT_scene_connect,
|
|
WTD_OT_scene_select_session,
|
|
WTD_OT_scene_use_current_oid,
|
|
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,
|
|
WTD_OT_scene_remove,
|
|
WTD_OT_scene_select_instance,
|
|
WTD_OT_scene_set_lod,
|
|
WTD_OT_scene_toggle_expand,
|
|
WTD_OT_scene_assetize,
|
|
)
|
|
|
|
|
|
def register():
|
|
for cls in classes:
|
|
bpy.utils.register_class(cls)
|
|
bpy.types.Scene.wtd_scene_manager = PointerProperty(type=WTD_SceneManagerSettings)
|
|
bpy.types.WindowManager.wtd_libreflow = PointerProperty(type=WTD_LibreflowSettings)
|
|
bpy.app.handlers.load_post.append(_on_load_post)
|
|
bpy.app.timers.register(_auto_connect, first_interval=1.0)
|
|
|
|
|
|
def unregister():
|
|
ipc.reset()
|
|
if _on_load_post in bpy.app.handlers.load_post:
|
|
bpy.app.handlers.load_post.remove(_on_load_post)
|
|
if bpy.app.timers.is_registered(_auto_connect):
|
|
bpy.app.timers.unregister(_auto_connect)
|
|
del bpy.types.WindowManager.wtd_libreflow
|
|
del bpy.types.Scene.wtd_scene_manager
|
|
for cls in reversed(classes):
|
|
bpy.utils.unregister_class(cls)
|