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:
NDemaerschalkandClaude Opus 5.5 committed 2026-10-28 10:47:05 +01:00
1 parent 6ec2b228ae
commit fbc7ac18e4
9 files changed
+307 -32

No files matched your search

+9 -1
View File
@@ -3,7 +3,8 @@
python dev/fake_libreflow.py <workdir>
Serves a project with one shot casting bob (main_character, x2), tree (bg_set), rock (main_props),
lamp (main_props, nothing published), car (vehicles, v001 not on this site) 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);
chair (set_props) is not cast, for Load Publish. The asset files come
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).
@@ -36,6 +37,11 @@ ASSETS = { # asset oid -> (type, {file_oid: (label, [revisions oldest first])})
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)])}),
f"{PROJECT}/asset_types/set_props/assets/chair": ("set_props", {
f"{PROJECT}/asset_types/set_props/assets/chair/tasks/shading/files/chair_shading_blend": ("shading/chair_shading.blend", [
rev("v001", f"{ROOT}/lib/chair/v001/chair.blend")]),
f"{PROJECT}/asset_types/set_props/assets/chair/tasks/modeling/files/chair_blend": ("modeling/chair.blend", [
rev("v001", f"{ROOT}/lib/chair/v001/chair.blend"), rev("v002", f"{ROOT}/lib/chair/v001/chair.blend", available=False)])}),
}
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 = []
@@ -100,6 +106,8 @@ def cmd(name, kw):
a["error"] = "No published revision available on this site"
out.append(a)
return {"shot_oid": SHOT, "assets": out}
if name == "wtd_get_asset_files":
return [file_data(f) for f in ASSETS[kw["oid"]][1]]
if name == "wtd_get_latest_revisions":
res = {}
for f in kw["file_oids"]:
+2
View File
@@ -6,6 +6,7 @@ bob: a rig (COL_bob: armature + meshes), v001 has 1 mesh, v002 has 2.
tree: COL_tree holding only LOD collections (COL_tree_LOD0, COL_tree_LOD1), plus an
unrelated top-level collection. v002 adds a mesh outside the LODs, v003 has no LOD.
car: a rig, COL_car: armature + named LODs COL_car_LODlow, _LODhigh, _LODmid.
chair: COL_chair (not cast, loaded with Load Publish).
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).
"""
@@ -67,4 +68,5 @@ write(os.path.join(lib, "tree", "v002", "model.blend"), "COL_tree", meshes=1, ex
write(os.path.join(lib, "tree", "v003", "model.blend"), "COL_tree", meshes=1, extra=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", meshes=0, rig=True, lods=["low", "high", "mid"])
write(os.path.join(lib, "chair", "v001", "chair.blend"), "COL_chair")
print("Test assets written in", lib)
+40 -11
View File
@@ -100,14 +100,14 @@ print("Load")
bpy.ops.wtd_scene.load()
pump()
state = loaded()
check("bob: 2 instances of v002", state.get("bob") == ("v002", ["COL_bob", "COL_bob.001"]), str(state.get("bob")))
check("tree: 1 collection instance", state.get("tree") == ("v001", ["tree"]), str(state.get("tree")))
check("rock: single top-level collection fallback", state.get("rock") == ("v001", ["rock_geo"]))
check("bob: 2 numbered instances of v002", state.get("bob") == ("v002", ["COL_bob_01", "COL_bob_02"]), str(state.get("bob")))
check("tree: 1 numbered collection instance", state.get("tree") == ("v001", ["tree_01"]), str(state.get("tree")))
check("rock: single top-level collection fallback", state.get("rock") == ("v001", ["rock_geo_01"]))
check("lamp and ghost reported", "lamp" in message() and "ghost" in message(), message())
check("type collections", {"COL_main_character", "COL_bg_set", "COL_main_props"} <= set(scene.collection.children.keys()))
check(
"chars are posable overrides",
all(ob.override_library for ob in bpy.data.collections["COL_bob"].all_objects),
all(ob.override_library for ob in bpy.data.collections["COL_bob_01"].all_objects),
)
check("shot manager groups bob as character", "Characters" in shot_viewer.rig_groups(bpy.context.view_layer))
bpy.ops.wtd_scene.load()
@@ -149,7 +149,7 @@ for ob in bpy.context.view_layer.objects:
ob.select_set(ob in (tree, bpy.data.objects["RIG_COL_car"]))
check("LOD of selected", bpy.ops.wtd_scene.set_lod(lod="1", selected=True) == {"FINISHED"})
check("only the selected instances having it switched", (lib.current_lod(tree_lib, tree), lib.current_lod(car_lib, car)) == ("1", "low"))
check("missing LOD refused", bpy.ops.wtd_scene.set_lod(lod="5", library="tree", instance="tree") == {"CANCELLED"})
check("missing LOD refused", bpy.ops.wtd_scene.set_lod(lod="5", library="tree", instance="tree_01") == {"CANCELLED"})
check("LOD order: list, numbers, names", sorted(["b", "10", "low", "2", "a", "high"], key=lib.lod_order) == ["high", "low", "2", "10", "a", "b"])
second = lib.add_instance(scene, bpy.context.view_layer, car_lib)
check(
@@ -161,7 +161,7 @@ lib.remove_instance(second)
lib.update("tree", {"name": "v002", "path": os.path.join(WORKDIR, "lib", "tree", "v002", "model.blend")})
tree = lib.instances(bpy.data.libraries["tree"])[0]
check("LOD kept by an update", lib.current_lod(bpy.data.libraries["tree"], tree) == "1")
check("content outside the LODs: instance refused", bpy.ops.wtd_scene.set_lod(lod="0", library="tree", instance="tree") == {"CANCELLED"})
check("content outside the LODs: instance refused", bpy.ops.wtd_scene.set_lod(lod="0", library="tree", instance="tree_01") == {"CANCELLED"})
lib.update("tree", {"name": "v003", "path": os.path.join(WORKDIR, "lib", "tree", "v003", "model.blend")})
tree = lib.instances(bpy.data.libraries["tree"])[0]
check("LODs gone: whole collection instanced", tree.instance_collection.name == "COL_tree" and not lib.lods(bpy.data.libraries["tree"], tree))
@@ -207,11 +207,11 @@ check("picking a version not on this site requests it", message() == "Requested
check("still on v002", loaded()["bob"][0] == "v002")
print("Remove")
bpy.ops.wtd_scene.remove(instance="COL_bob.001")
check("one instance removed", loaded()["bob"][1] == ["COL_bob"])
bpy.ops.wtd_scene.remove(instance="COL_bob_02")
check("one instance removed", loaded()["bob"][1] == ["COL_bob_01"])
for ob in bpy.context.view_layer.objects:
ob.select_set(False)
bpy.data.objects["tree"].select_set(True)
bpy.data.objects["tree_01"].select_set(True)
bpy.ops.wtd_scene.remove(selected=True)
check("selected instance removed", loaded()["tree"][1] == [])
bpy.ops.wtd_scene.remove("EXEC_DEFAULT", library="rock")
@@ -220,6 +220,35 @@ check("its indirect library removed too", set(bpy.data.libraries.keys()) == {"bo
bpy.ops.wtd_scene.load()
pump()
check("load restores what is missing", {k: len(v[1]) for k, v in loaded().items()} == {"bob": 2, "car": 1, "rock": 1, "tree": 1})
check("numbers go on after the highest one", loaded()["bob"][1] == ["COL_bob_01", "COL_bob_02"], str(loaded()["bob"][1]))
print("Load publish")
CHAIR = "/melvile/asset_types/set_props/assets/chair"
check("project of the context", sm.project_oid(scene) == "/melvile")
sm.STATE.assets = ipc.call("wtd_list_assets", None, oid=sm.project_oid(scene))
check("any asset listed, cast or not", CHAIR in [a["oid"] for a in sm.STATE.assets])
sm.browse_asset(next(a for a in sm.STATE.assets if a["oid"] == CHAIR))
chair_file = sm.default_file("set_props", sm.STATE.browse["files"])
check("default file follows the file patterns", chair_file["label"] == "modeling/chair.blend", chair_file["label"])
check("versions newest first", [r["name"] for r in chair_file["revisions"]] == ["v002", "v001"])
check("default version: latest on this site", sm.default_revision(chair_file)["name"] == "v001")
result = bpy.ops.wtd_scene.load_publish("EXEC_DEFAULT", file=chair_file["file_oid"], revision="v002")
pump()
check(
"version not on this site: requested, not loaded",
result == {"CANCELLED"} and "chair" not in loaded()
and ["wtd_request_revisions", {"revision_oids": [chair_file["file_oid"] + "/history/revisions/v002"]}] in fake("/_test/calls", {}),
)
bpy.ops.wtd_scene.load_publish("EXEC_DEFAULT", file=chair_file["file_oid"], revision="v001", count=2)
check("loaded twice, numbered", loaded().get("chair") == ("v001", ["chair_01", "chair_02"]), str(loaded().get("chair")))
check("its versions followed in Loaded", sm.version_status(bpy.data.libraries["chair"])[0] == "PENDING")
sm.browse_asset(next(a for a in sm.STATE.assets if a["asset"] == "bob"))
check(
"another version of a loaded file refused",
bpy.ops.wtd_scene.load_publish("EXEC_DEFAULT", file=BOB_FILE, revision="v004") == {"CANCELLED"} and loaded()["bob"][0] == "v002",
)
bpy.ops.wtd_scene.load_publish("EXEC_DEFAULT", file=BOB_FILE, revision="v002")
check("same version: one more numbered instance", loaded()["bob"][1] == ["COL_bob_01", "COL_bob_02", "COL_bob_03"], str(loaded()["bob"][1]))
print("Ambiguous file")
try:
@@ -243,7 +272,7 @@ bpy.ops.wtd_scene.assetize(
)
pump()
check("published and replaced", "replaced" in message(), message())
check("lamp linked", loaded().get("lamp") == ("v001", ["COL_lamp"]), str(loaded().get("lamp")))
check("lamp linked", loaded().get("lamp") == ("v001", ["COL_lamp_01"]), str(loaded().get("lamp")))
check("local collection kept, renamed", "COL_lamp_local" in bpy.data.collections)
bad = bpy.data.collections.new("COL_bad")
scene.collection.children.link(bad)
@@ -256,7 +285,7 @@ path = os.path.join(WORKDIR, "scene_manager_test.blend")
bpy.ops.wm.save_as_mainfile(filepath=path)
bpy.ops.wm.open_mainfile(filepath=path)
sm._on_load_post()
check("everything found after reopening", set(loaded()) == {"bob", "car", "lamp", "rock", "tree"})
check("everything found after reopening", set(loaded()) == {"bob", "car", "chair", "lamp", "rock", "tree"})
car_lib = bpy.data.libraries["car"]
check("LOD kept after reopening", lib.current_lod(car_lib, lib.instances(car_lib)[0]) == "low")
sm.connect(bpy.context.scene)