Scene Manager: request publishes missing on this site
- Request (globe) on casting rows and loaded assets whose latest publish is not on this site, and Request Missing for all of them, through the new wtdcmds command wtd_request_revisions. Requested publishes show "requested, waiting for the download". - The version picker requests a version that is not on this site. - A 404 explains that the session is gone or wtdcmds is too old. - Fake server: car (vehicles) with a publish not on this site, wtd_request_revisions and /_test/download; 45 checks. - Tested live: car_paul modeling.blend v001 -> Requested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9ee7710479
commit
9fe38ce58a
8 files changed
+197
-19
No files matched your search
@@ -42,7 +42,8 @@ that contains this repo on `sys.path` and importing the repo folder name (see
|
|||||||
1. `blender -b --factory-startup --python dev/make_test_assets.py -- <workdir>`
|
1. `blender -b --factory-startup --python dev/make_test_assets.py -- <workdir>`
|
||||||
2. `python dev/fake_libreflow.py <workdir>` (keep running; restart for a clean state)
|
2. `python dev/fake_libreflow.py <workdir>` (keep running; restart for a clean state)
|
||||||
3. `blender -b --factory-startup --python-exit-code 1 --python dev/test_scene_manager.py -- <workdir>`
|
3. `blender -b --factory-startup --python-exit-code 1 --python dev/test_scene_manager.py -- <workdir>`
|
||||||
(36 checks, "All checks passed").
|
(45 checks, "All checks passed"). `/_test/publish` and `/_test/download` simulate a new
|
||||||
|
publish and a requested revision reaching this site.
|
||||||
The fake server follows the real `wtdcmds` replies; check them against the real
|
The fake server follows the real `wtdcmds` replies; check them against the real
|
||||||
Libreflow when one is available.
|
Libreflow when one is available.
|
||||||
|
|
||||||
@@ -83,8 +84,10 @@ that contains this repo on `sys.path` and importing the repo folder name (see
|
|||||||
- Set dressing publishes (`setdress_low`) hold a whole layout scene: the set is the
|
- Set dressing publishes (`setdress_low`) hold a whole layout scene: the set is the
|
||||||
`environments` collection, next to `lights`, `cameras`, `characters`, `_unused`.
|
`environments` collection, next to `lights`, `cameras`, `characters`, `_unused`.
|
||||||
- Tested live: connection, context, casting, load (instance and override), version switch
|
- Tested live: connection, context, casting, load (instance and override), version switch
|
||||||
and update, remove, assetize errors. Not tested: an actual assetize publish (it writes
|
and update, remove, assetize errors, one request (`car_paul` modeling.blend v001 →
|
||||||
to production).
|
`Requested`). Not tested: an actual assetize publish (it writes to production).
|
||||||
|
New wtdcmds commands need every Libreflow session restarted (a running one keeps the
|
||||||
|
old code and answers 404).
|
||||||
|
|
||||||
## Open points
|
## Open points
|
||||||
|
|
||||||
|
|||||||
@@ -346,8 +346,11 @@ Lists the assets cast in the shot, with how many instances are loaded out of how
|
|||||||
| ✔ | Loaded. |
|
| ✔ | Loaded. |
|
||||||
| Import arrow | Not loaded (or fewer instances than cast): click it to load this asset only. |
|
| Import arrow | Not loaded (or fewer instances than cast): click it to load this asset only. |
|
||||||
| ⚠ red | Cannot be loaded, with the reason from Libreflow: not found in the flow, no published file, or nothing available on this site yet. |
|
| ⚠ red | Cannot be loaded, with the reason from Libreflow: not found in the flow, no published file, or nothing available on this site yet. |
|
||||||
|
| Globe | The latest publish is not on this site: click it to request it (see below). |
|
||||||
|
| Hourglass | Requested, waiting for the download. |
|
||||||
|
|
||||||
- **Load Missing** links every cast asset that is not loaded yet, and adds the missing instances. Running it again only adds what is missing.
|
- **Load Missing** links every cast asset that is not loaded yet, and adds the missing instances. Running it again only adds what is missing.
|
||||||
|
- **Request Missing** (shown when needed) asks Libreflow to download, on this site, the latest publish of every cast or loaded asset that is not here yet: the same as **Request** in Libreflow (from the exchange site, or from the site that published it). The download is done by the Libreflow sync: click **Refresh** later, then load or update.
|
||||||
- **Refresh** reads the casting and the versions again.
|
- **Refresh** reads the casting and the versions again.
|
||||||
- **Loaded, no longer cast** lists assets loaded earlier that the shot's casting no longer contains.
|
- **Loaded, no longer cast** lists assets loaded earlier that the shot's casting no longer contains.
|
||||||
|
|
||||||
@@ -364,9 +367,9 @@ All of these can be changed in the [Configuration](#configuration).
|
|||||||
|
|
||||||
Lists everything the Scene Manager loaded, one line per asset, with its version:
|
Lists everything the Scene Manager loaded, one line per asset, with its version:
|
||||||
|
|
||||||
- The version is **red** when it is not the latest publish. The line under it says either "v004 available" (click the update icon), or "v004 published, not available on this site yet" (wait for the download).
|
- The version is **red** when it is not the latest publish. The line under it says either "v004 available" (click the update icon), or "v004 published, not available on this site yet" (click the globe to request it, then "v004 requested, waiting for the download").
|
||||||
- **Arrow**: shows the asset's instances, each with a button to select its objects and one to remove it.
|
- **Arrow**: shows the asset's instances, each with a button to select its objects and one to remove it.
|
||||||
- **Menu icon**: pick another published version, for example to roll back. Versions not on this site are marked and cannot be picked.
|
- **Menu icon**: pick another published version, for example to roll back. Versions not on this site are marked: picking one requests it; pick it again once it is downloaded.
|
||||||
- **✕**: removes the asset entirely: all its instances and the link to its file.
|
- **✕**: removes the asset entirely: all its instances and the link to its file.
|
||||||
- **Update All** / **Update Selected**: switch all the loaded assets (or the ones of the selected objects) to their latest available publish.
|
- **Update All** / **Update Selected**: switch all the loaded assets (or the ones of the selected objects) to their latest available publish.
|
||||||
- **Remove Selected**: removes the instances of the selected objects.
|
- **Remove Selected**: removes the instances of the selected objects.
|
||||||
@@ -463,7 +466,10 @@ Start Libreflow (with the `wtdcmds` extension), then click the refresh icon of t
|
|||||||
Its red line says why. "No file matching..." means none of its files matches `SCENE_MANAGER_FILE_PATTERNS`. "No collection named..." means the file has none of `COL_<asset>`, `<asset>` or `environments`, and several top-level collections: rename the published collection, or add its name to `SCENE_MANAGER_COLLECTION_NAMES`.
|
Its red line says why. "No file matching..." means none of its files matches `SCENE_MANAGER_FILE_PATTERNS`. "No collection named..." means the file has none of `COL_<asset>`, `<asset>` or `environments`, and several top-level collections: rename the published collection, or add its name to `SCENE_MANAGER_COLLECTION_NAMES`.
|
||||||
|
|
||||||
**Scene Manager: the version stays red after Update.**
|
**Scene Manager: the version stays red after Update.**
|
||||||
The newer version is published but not downloaded on this site yet. Wait for the sync, then update again.
|
The newer version is published but not downloaded on this site yet. Request it (globe icon) if it is not requested yet, wait for the sync, then refresh and update again.
|
||||||
|
|
||||||
|
**Scene Manager: "...its wtdcmds extension is too old".**
|
||||||
|
Libreflow runs an older `wtdcmds` without the command (e.g. requests). Update the extension and restart Libreflow.
|
||||||
|
|
||||||
**Shot Viewer: playback only plays one shot.**
|
**Shot Viewer: playback only plays one shot.**
|
||||||
**Loop Shot** is on. Untick it to play the whole sequence.
|
**Loop Shot** is on. Untick it to play the whole sequence.
|
||||||
|
|||||||
+29
-3
@@ -3,11 +3,12 @@
|
|||||||
python dev/fake_libreflow.py <workdir>
|
python dev/fake_libreflow.py <workdir>
|
||||||
|
|
||||||
Serves a project with one shot casting bob (main_character, x2), tree (bg_set), rock (main_props),
|
Serves a project with one shot casting bob (main_character, x2), tree (bg_set), rock (main_props),
|
||||||
lamp (main_props, nothing published) and ghost (not in the flow). The asset files come
|
lamp (main_props, nothing published), car (vehicles, v001 not on this site) and ghost (not in the flow). The asset files come
|
||||||
from dev/make_test_assets.py (<workdir>/lib). Writes <workdir>/kabaret/server.json:
|
from dev/make_test_assets.py (<workdir>/lib). Writes <workdir>/kabaret/server.json:
|
||||||
point config LIBREFLOW_SERVER_FILE to it (see dev/test_scene_manager.py).
|
point config LIBREFLOW_SERVER_FILE to it (see dev/test_scene_manager.py).
|
||||||
|
|
||||||
Test-only endpoints: POST /_test/publish {file_oid, name, available, path},
|
Test-only endpoints: POST /_test/publish {file_oid, name, available, path},
|
||||||
|
POST /_test/download {revision_oid} (a requested revision reaches this site),
|
||||||
POST /_test/sessions {uid: session}, POST /_test/calls (commands received).
|
POST /_test/sessions {uid: session}, POST /_test/calls (commands received).
|
||||||
State lives in memory: restart the server for a clean state.
|
State lives in memory: restart the server for a clean state.
|
||||||
"""
|
"""
|
||||||
@@ -33,10 +34,22 @@ ASSETS = { # asset oid -> (type, {file_oid: (label, [revisions oldest first])})
|
|||||||
f"{PROJECT}/asset_types/main_props/assets/rock": ("main_props", {f"{PROJECT}/asset_types/main_props/assets/rock/tasks/modeling/files/rock_blend": ("modeling/rock.blend", [
|
f"{PROJECT}/asset_types/main_props/assets/rock": ("main_props", {f"{PROJECT}/asset_types/main_props/assets/rock/tasks/modeling/files/rock_blend": ("modeling/rock.blend", [
|
||||||
rev("v001", f"{ROOT}/lib/rock/v001/rock.blend")])}),
|
rev("v001", f"{ROOT}/lib/rock/v001/rock.blend")])}),
|
||||||
f"{PROJECT}/asset_types/main_props/assets/lamp": ("main_props", {}),
|
f"{PROJECT}/asset_types/main_props/assets/lamp": ("main_props", {}),
|
||||||
|
f"{PROJECT}/asset_types/vehicles/assets/car": ("vehicles", {f"{PROJECT}/asset_types/vehicles/assets/car/tasks/rigging/files/car_blend": ("rigging/car.blend", [
|
||||||
|
rev("v001", f"{ROOT}/lib/car/v001/car.blend", available=False)])}),
|
||||||
}
|
}
|
||||||
CASTING = [("bob", "main_character", 2), ("tree", "bg_set", 1), ("rock", "main_props", 1), ("lamp", "main_props", 1), ("ghost", "main_character", 1)]
|
CASTING = [("bob", "main_character", 2), ("tree", "bg_set", 1), ("rock", "main_props", 1), ("lamp", "main_props", 1), ("car", "vehicles", 1), ("ghost", "main_character", 1)]
|
||||||
CALLS = []
|
CALLS = []
|
||||||
|
|
||||||
|
def set_oids():
|
||||||
|
for _a, (_t, files) in ASSETS.items():
|
||||||
|
for f, (_l, revs) in files.items():
|
||||||
|
for r in revs: r["oid"] = f"{f}/history/revisions/{r['name']}"
|
||||||
|
|
||||||
|
def find_rev(oid):
|
||||||
|
return next((r for _a, (_t, files) in ASSETS.items() for _l, revs in files.values() for r in revs if r["oid"] == oid), None)
|
||||||
|
|
||||||
|
set_oids()
|
||||||
|
|
||||||
def file_data(file_oid):
|
def file_data(file_oid):
|
||||||
for asset_oid, (atype, files) in ASSETS.items():
|
for asset_oid, (atype, files) in ASSETS.items():
|
||||||
if file_oid in files:
|
if file_oid in files:
|
||||||
@@ -98,6 +111,15 @@ def cmd(name, kw):
|
|||||||
if kw["file_oid"] in files:
|
if kw["file_oid"] in files:
|
||||||
return list(reversed(files[kw["file_oid"]][1]))
|
return list(reversed(files[kw["file_oid"]][1]))
|
||||||
raise ValueError("No file")
|
raise ValueError("No file")
|
||||||
|
if name == "wtd_request_revisions":
|
||||||
|
res = {}
|
||||||
|
for oid in kw["revision_oids"]:
|
||||||
|
r = find_rev(oid)
|
||||||
|
if r is None:
|
||||||
|
res[oid] = {"oid": oid, "error": f"Could not find {oid}"}; continue
|
||||||
|
if not r["available"]: r["sync_status"] = "Requested"
|
||||||
|
res[oid] = r
|
||||||
|
return res
|
||||||
if name == "wtd_assetize":
|
if name == "wtd_assetize":
|
||||||
src = kw["source_path"]
|
src = kw["source_path"]
|
||||||
if not os.path.isfile(src): raise ValueError(f"Source file {src} does not exist")
|
if not os.path.isfile(src): raise ValueError(f"Source file {src} does not exist")
|
||||||
@@ -115,7 +137,7 @@ def cmd(name, kw):
|
|||||||
label, revs = files.setdefault(file_oid, (f"{task}/{fname}.blend", []))
|
label, revs = files.setdefault(file_oid, (f"{task}/{fname}.blend", []))
|
||||||
name = f"v{len(revs) + 1:03d}"
|
name = f"v{len(revs) + 1:03d}"
|
||||||
dest = f"{ROOT}/lib/{aname}/{name}/{fname}.blend"; os.makedirs(os.path.dirname(dest), exist_ok=True); shutil.copy(src, dest)
|
dest = f"{ROOT}/lib/{aname}/{name}/{fname}.blend"; os.makedirs(os.path.dirname(dest), exist_ok=True); shutil.copy(src, dest)
|
||||||
revs.append(rev(name, dest, comment=kw.get("comment", "")))
|
revs.append(rev(name, dest, comment=kw.get("comment", ""))); set_oids()
|
||||||
return {"asset_oid": asset_oid, "asset_created": created, **file_data(file_oid)}
|
return {"asset_oid": asset_oid, "asset_created": created, **file_data(file_oid)}
|
||||||
raise KeyError(name)
|
raise KeyError(name)
|
||||||
|
|
||||||
@@ -139,6 +161,10 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
f = body["file_oid"]
|
f = body["file_oid"]
|
||||||
for _a, (_t, files) in ASSETS.items():
|
for _a, (_t, files) in ASSETS.items():
|
||||||
if f in files: files[f][1].append(rev(body["name"], body.get("path", "/nowhere/" + body["name"]), body.get("available", True)))
|
if f in files: files[f][1].append(rev(body["name"], body.get("path", "/nowhere/" + body["name"]), body.get("available", True)))
|
||||||
|
set_oids()
|
||||||
|
return self.reply(200, {"result": True})
|
||||||
|
if url.path == "/_test/download":
|
||||||
|
r = find_rev(body["revision_oid"]); r["available"] = True; r["sync_status"] = "Available"
|
||||||
return self.reply(200, {"result": True})
|
return self.reply(200, {"result": True})
|
||||||
if url.path == "/_test/sessions":
|
if url.path == "/_test/sessions":
|
||||||
SESSIONS.clear(); SESSIONS.update(body); return self.reply(200, {"result": True})
|
SESSIONS.clear(); SESSIONS.update(body); return self.reply(200, {"result": True})
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
bob: a rig (COL_bob: armature + meshes), v001 has 1 mesh, v002 has 2.
|
bob: a rig (COL_bob: armature + meshes), v001 has 1 mesh, v002 has 2.
|
||||||
tree: COL_tree, plus an unrelated top-level collection.
|
tree: COL_tree, plus an unrelated top-level collection.
|
||||||
|
car: COL_car.
|
||||||
rock: a single top-level collection not named after the asset (fallback case), its mesh
|
rock: a single top-level collection not named after the asset (fallback case), its mesh
|
||||||
using a node group linked from Blender's essentials (an indirect library).
|
using a node group linked from Blender's essentials (an indirect library).
|
||||||
"""
|
"""
|
||||||
@@ -56,4 +57,5 @@ write(os.path.join(lib, "bob", "v001", "rig.blend"), "COL_bob", meshes=1, rig=Tr
|
|||||||
write(os.path.join(lib, "bob", "v002", "rig.blend"), "COL_bob", meshes=2, rig=True, extra=True)
|
write(os.path.join(lib, "bob", "v002", "rig.blend"), "COL_bob", meshes=2, rig=True, extra=True)
|
||||||
write(os.path.join(lib, "tree", "v001", "model.blend"), "COL_tree", extra=True)
|
write(os.path.join(lib, "tree", "v001", "model.blend"), "COL_tree", extra=True)
|
||||||
write(os.path.join(lib, "rock", "v001", "rock.blend"), "rock_geo", node_group=True)
|
write(os.path.join(lib, "rock", "v001", "rock.blend"), "rock_geo", node_group=True)
|
||||||
|
write(os.path.join(lib, "car", "v001", "car.blend"), "COL_car")
|
||||||
print("Test assets written in", lib)
|
print("Test assets written in", lib)
|
||||||
@@ -33,6 +33,8 @@ SERVER_FILE = os.path.join(WORKDIR, "kabaret", "server.json")
|
|||||||
config.LIBREFLOW_SERVER_FILE = SERVER_FILE
|
config.LIBREFLOW_SERVER_FILE = SERVER_FILE
|
||||||
SHOT = "/melvile/films/f01/sequences/s010/shots/p010"
|
SHOT = "/melvile/films/f01/sequences/s010/shots/p010"
|
||||||
BOB_FILE = "/melvile/asset_types/main_character/assets/bob/tasks/rigging/files/rig_blend"
|
BOB_FILE = "/melvile/asset_types/main_character/assets/bob/tasks/rigging/files/rig_blend"
|
||||||
|
CAR = "/melvile/asset_types/vehicles/assets/car"
|
||||||
|
CAR_FILE = CAR + "/tasks/rigging/files/car_blend"
|
||||||
FAILED = []
|
FAILED = []
|
||||||
|
|
||||||
|
|
||||||
@@ -92,7 +94,7 @@ scene.wtd_scene_manager.context_oid = SHOT
|
|||||||
pump()
|
pump()
|
||||||
check("shot context validated", sm.context_label(SHOT) == ("shot: f01 / s010 / p010", True))
|
check("shot context validated", sm.context_label(SHOT) == ("shot: f01 / s010 / p010", True))
|
||||||
errors = {a["name"]: a["error"] for a in sm.STATE.casting.get(SHOT, {}).get("assets", [])}
|
errors = {a["name"]: a["error"] for a in sm.STATE.casting.get(SHOT, {}).get("assets", [])}
|
||||||
check("casting read", set(errors) == {"bob", "tree", "rock", "lamp", "ghost"}, str(errors))
|
check("casting read", set(errors) == {"bob", "tree", "rock", "lamp", "car", "ghost"}, str(errors))
|
||||||
|
|
||||||
print("Load")
|
print("Load")
|
||||||
bpy.ops.wtd_scene.load()
|
bpy.ops.wtd_scene.load()
|
||||||
@@ -112,12 +114,31 @@ bpy.ops.wtd_scene.load()
|
|||||||
pump()
|
pump()
|
||||||
check("load again adds nothing", message().startswith("Nothing new to load"), message())
|
check("load again adds nothing", message().startswith("Nothing new to load"), message())
|
||||||
|
|
||||||
|
print("Request")
|
||||||
|
check("car can be requested", sm.missing_files(scene) == [CAR_FILE], str(sm.missing_files(scene)))
|
||||||
|
bpy.ops.wtd_scene.request()
|
||||||
|
pump()
|
||||||
|
check("car v001 requested", message() == "Requested car v001", message())
|
||||||
|
check("nothing left to request", sm.missing_files(scene) == [])
|
||||||
|
calls = fake("/_test/calls", {})
|
||||||
|
check("request sent to Libreflow", ["wtd_request_revisions", {"revision_oids": [CAR_FILE + "/history/revisions/v001"]}] in calls)
|
||||||
|
fake("/_test/download", {"revision_oid": CAR_FILE + "/history/revisions/v001"})
|
||||||
|
bpy.ops.wtd_scene.refresh()
|
||||||
|
pump()
|
||||||
|
bpy.ops.wtd_scene.load(asset_oid=CAR)
|
||||||
|
pump()
|
||||||
|
check("car loaded once downloaded", loaded().get("car", (None,))[0] == "v001", message())
|
||||||
|
|
||||||
print("Versions")
|
print("Versions")
|
||||||
check("bob is the latest", sm.version_status(bpy.data.libraries["bob"])[0] == "LATEST")
|
check("bob is the latest", sm.version_status(bpy.data.libraries["bob"])[0] == "LATEST")
|
||||||
fake("/_test/publish", {"file_oid": BOB_FILE, "name": "v003", "available": False})
|
fake("/_test/publish", {"file_oid": BOB_FILE, "name": "v003", "available": False})
|
||||||
sm.refresh_versions()
|
sm.refresh_versions()
|
||||||
pump()
|
pump()
|
||||||
check("v003 not on this site: pending", sm.version_status(bpy.data.libraries["bob"])[0] == "PENDING")
|
check("v003 not on this site: pending", sm.version_status(bpy.data.libraries["bob"])[0] == "PENDING")
|
||||||
|
bpy.ops.wtd_scene.request(file_oid=BOB_FILE)
|
||||||
|
pump()
|
||||||
|
status = sm.version_status(bpy.data.libraries["bob"])
|
||||||
|
check("v003 requested", status == ("PENDING", "v003 requested, waiting for the download"), str(status))
|
||||||
fake(
|
fake(
|
||||||
"/_test/publish",
|
"/_test/publish",
|
||||||
{"file_oid": BOB_FILE, "name": "v004", "available": True, "path": os.path.join(WORKDIR, "lib", "bob", "v001", "rig.blend")},
|
{"file_oid": BOB_FILE, "name": "v004", "available": True, "path": os.path.join(WORKDIR, "lib", "bob", "v001", "rig.blend")},
|
||||||
@@ -137,6 +158,13 @@ sm._REVISION_ITEMS[:] = [(r["name"], r["name"], "", "NONE", i) for i, r in enume
|
|||||||
bpy.ops.wtd_scene.pick_revision("EXEC_DEFAULT", library="bob", revision="v002")
|
bpy.ops.wtd_scene.pick_revision("EXEC_DEFAULT", library="bob", revision="v002")
|
||||||
check("rolled back to v002", loaded()["bob"][0] == "v002")
|
check("rolled back to v002", loaded()["bob"][0] == "v002")
|
||||||
check("unavailable v003 refused", bpy.ops.wtd_scene.pick_revision("EXEC_DEFAULT", library="bob", revision="v003") == {"CANCELLED"})
|
check("unavailable v003 refused", bpy.ops.wtd_scene.pick_revision("EXEC_DEFAULT", library="bob", revision="v003") == {"CANCELLED"})
|
||||||
|
fake("/_test/publish", {"file_oid": BOB_FILE, "name": "v005", "available": False})
|
||||||
|
sm.STATE.revisions[BOB_FILE] = ipc.call("wtd_get_revisions", None, file_oid=BOB_FILE)
|
||||||
|
sm._REVISION_ITEMS[:] = [(r["name"], r["name"], "", "NONE", i) for i, r in enumerate(sm.STATE.revisions[BOB_FILE])]
|
||||||
|
bpy.ops.wtd_scene.pick_revision("EXEC_DEFAULT", library="bob", revision="v005")
|
||||||
|
pump()
|
||||||
|
check("picking a version not on this site requests it", message() == "Requested bob v005", message())
|
||||||
|
check("still on v002", loaded()["bob"][0] == "v002")
|
||||||
|
|
||||||
print("Remove")
|
print("Remove")
|
||||||
bpy.ops.wtd_scene.remove(instance="COL_bob.001")
|
bpy.ops.wtd_scene.remove(instance="COL_bob.001")
|
||||||
@@ -148,10 +176,10 @@ bpy.ops.wtd_scene.remove(selected=True)
|
|||||||
check("selected instance removed", loaded()["tree"][1] == [])
|
check("selected instance removed", loaded()["tree"][1] == [])
|
||||||
bpy.ops.wtd_scene.remove("EXEC_DEFAULT", library="rock")
|
bpy.ops.wtd_scene.remove("EXEC_DEFAULT", library="rock")
|
||||||
check("whole asset removed", "rock" not in bpy.data.libraries)
|
check("whole asset removed", "rock" not in bpy.data.libraries)
|
||||||
check("its indirect library removed too", set(bpy.data.libraries.keys()) == {"bob", "tree"}, str(bpy.data.libraries.keys()))
|
check("its indirect library removed too", set(bpy.data.libraries.keys()) == {"bob", "car", "tree"}, str(bpy.data.libraries.keys()))
|
||||||
bpy.ops.wtd_scene.load()
|
bpy.ops.wtd_scene.load()
|
||||||
pump()
|
pump()
|
||||||
check("load restores what is missing", {k: len(v[1]) for k, v in loaded().items()} == {"bob": 2, "rock": 1, "tree": 1})
|
check("load restores what is missing", {k: len(v[1]) for k, v in loaded().items()} == {"bob": 2, "car": 1, "rock": 1, "tree": 1})
|
||||||
|
|
||||||
print("Ambiguous file")
|
print("Ambiguous file")
|
||||||
try:
|
try:
|
||||||
@@ -188,7 +216,7 @@ path = os.path.join(WORKDIR, "scene_manager_test.blend")
|
|||||||
bpy.ops.wm.save_as_mainfile(filepath=path)
|
bpy.ops.wm.save_as_mainfile(filepath=path)
|
||||||
bpy.ops.wm.open_mainfile(filepath=path)
|
bpy.ops.wm.open_mainfile(filepath=path)
|
||||||
sm._on_load_post()
|
sm._on_load_post()
|
||||||
check("everything found after reopening", set(loaded()) == {"bob", "lamp", "rock", "tree"})
|
check("everything found after reopening", set(loaded()) == {"bob", "car", "lamp", "rock", "tree"})
|
||||||
sm.connect(bpy.context.scene)
|
sm.connect(bpy.context.scene)
|
||||||
pump()
|
pump()
|
||||||
check("versions checked again", sm.version_status(bpy.data.libraries["bob"])[0] == "UPDATE")
|
check("versions checked again", sm.version_status(bpy.data.libraries["bob"])[0] == "UPDATE")
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from .. import config
|
|||||||
# Hints added to the server's message for the statuses the user can act on.
|
# Hints added to the server's message for the statuses the user can act on.
|
||||||
_STATUS_HINTS = {
|
_STATUS_HINTS = {
|
||||||
401: "the server refused the token, try again",
|
401: "the server refused the token, try again",
|
||||||
|
404: "the Libreflow session is gone, or its wtdcmds extension is too old: update it and restart Libreflow",
|
||||||
504: "no answer within 10 s, the command may still complete in Libreflow",
|
504: "no answer within 10 s, the command may still complete in Libreflow",
|
||||||
}
|
}
|
||||||
# Statuses whose message replaces the server's.
|
# Statuses whose message replaces the server's.
|
||||||
|
|||||||
+104
-3
@@ -120,9 +120,36 @@ def version_status(library):
|
|||||||
return "LATEST", "latest"
|
return "LATEST", "latest"
|
||||||
if available is not None and available["name"] != current:
|
if available is not None and available["name"] != current:
|
||||||
return "UPDATE", f"{available['name']} available"
|
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"
|
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
|
# Background requests
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@@ -225,6 +252,41 @@ def refresh_versions(quiet=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
# Settings
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@@ -486,6 +548,34 @@ class WTD_OT_scene_update(bpy.types.Operator):
|
|||||||
return {"FINISHED"}
|
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_items(self, context):
|
def _revision_items(self, context):
|
||||||
return _REVISION_ITEMS
|
return _REVISION_ITEMS
|
||||||
|
|
||||||
@@ -524,7 +614,12 @@ class WTD_OT_scene_pick_revision(bpy.types.Operator):
|
|||||||
(
|
(
|
||||||
r["name"],
|
r["name"],
|
||||||
f"{r['name']} {time.strftime('%Y-%m-%d', time.localtime(r['date'])) if r.get('date') else ''}"
|
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 " (not on this site)"),
|
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 "",
|
r.get("comment") or "",
|
||||||
"CHECKMARK" if r["name"] == current else "NONE",
|
"CHECKMARK" if r["name"] == current else "NONE",
|
||||||
index,
|
index,
|
||||||
@@ -542,9 +637,14 @@ class WTD_OT_scene_pick_revision(bpy.types.Operator):
|
|||||||
library = bpy.data.libraries.get(self.library)
|
library = bpy.data.libraries.get(self.library)
|
||||||
if library is None:
|
if library is None:
|
||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
revision = next(r for r in STATE.revisions[library["wtd_file_oid"]] if r["name"] == self.revision)
|
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 not revision.get("available"):
|
||||||
self.report({"WARNING"}, f"{revision['name']} is not available on this site")
|
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"}
|
return {"CANCELLED"}
|
||||||
updated, messages = _update_libraries([self.library], target=revision)
|
updated, messages = _update_libraries([self.library], target=revision)
|
||||||
if messages:
|
if messages:
|
||||||
@@ -844,6 +944,7 @@ classes = (
|
|||||||
WTD_OT_scene_refresh,
|
WTD_OT_scene_refresh,
|
||||||
WTD_OT_scene_load,
|
WTD_OT_scene_load,
|
||||||
WTD_OT_scene_update,
|
WTD_OT_scene_update,
|
||||||
|
WTD_OT_scene_request,
|
||||||
WTD_OT_scene_pick_revision,
|
WTD_OT_scene_pick_revision,
|
||||||
WTD_OT_scene_remove,
|
WTD_OT_scene_remove,
|
||||||
WTD_OT_scene_select_instance,
|
WTD_OT_scene_select_instance,
|
||||||
|
|||||||
+14
-3
@@ -384,6 +384,8 @@ class _SceneCastingPanel:
|
|||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.scale_y = 1.3
|
row.scale_y = 1.3
|
||||||
row.operator(sm.WTD_OT_scene_load.bl_idname, text="Load Missing", icon="IMPORT")
|
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")
|
row.operator(sm.WTD_OT_scene_refresh.bl_idname, text="", icon="FILE_REFRESH")
|
||||||
casting = sm.STATE.casting.get(oid)
|
casting = sm.STATE.casting.get(oid)
|
||||||
if casting is None:
|
if casting is None:
|
||||||
@@ -400,16 +402,23 @@ class _SceneCastingPanel:
|
|||||||
have = len(found.get(library.name, [])) if library else 0
|
have = len(found.get(library.name, [])) if library else 0
|
||||||
want = asset.get("nb_occurences") or 1
|
want = asset.get("nb_occurences") or 1
|
||||||
error = asset.get("error")
|
error = asset.get("error")
|
||||||
icon = "ERROR" if error else "CHECKMARK" if have >= want else "IMPORT"
|
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 = col.row(align=True)
|
||||||
row.alert = bool(error)
|
row.alert = bool(error) and not requested
|
||||||
row.label(text=f"{asset['name']} ({asset.get('asset_type', '')})", icon=icon)
|
row.label(text=f"{asset['name']} ({asset.get('asset_type', '')})", icon=icon)
|
||||||
row.label(text=f"{have}/{want}")
|
row.label(text=f"{have}/{want}")
|
||||||
if not error and have < want:
|
if not error and have < want:
|
||||||
row.operator(sm.WTD_OT_scene_load.bl_idname, text="", icon="IMPORT").asset_oid = asset["oid"]
|
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:
|
if error:
|
||||||
sub = col.row()
|
sub = col.row()
|
||||||
sub.alert = True
|
sub.alert = not requested
|
||||||
_wrapped_label(sub, context, error, icon="BLANK1")
|
_wrapped_label(sub, context, error, icon="BLANK1")
|
||||||
|
|
||||||
cast = {asset["oid"] for asset in casting["assets"]}
|
cast = {asset["oid"] for asset in casting["assets"]}
|
||||||
@@ -455,6 +464,8 @@ class _SceneLoadedPanel:
|
|||||||
version.label(text=library.get("wtd_revision", "?"))
|
version.label(text=library.get("wtd_revision", "?"))
|
||||||
if state == "UPDATE":
|
if state == "UPDATE":
|
||||||
row.operator(sm.WTD_OT_scene_update.bl_idname, text="", icon="FILE_REFRESH").library = library.name
|
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_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
|
row.operator(sm.WTD_OT_scene_remove.bl_idname, text="", icon="X").library = library.name
|
||||||
if outdated:
|
if outdated:
|
||||||
|
|||||||
Reference in new issue
Block a user