Scene Manager: named LODs

{lod} in SCENE_MANAGER_LOD_COLLECTION now matches any name (COL_bob_LODhigh
as well as COL_bob_LOD0). SCENE_MANAGER_LODS (high, mid, low) sets the
button order and the LOD shown on load; unlisted LODs follow, numbers in
order, then names A-Z. It replaces SCENE_MANAGER_DEFAULT_LOD. The LOD of
Selected buttons get their own line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NDemaerschalkandClaude Opus 5.5 committed 2026-10-27 16:46:36 +01:00
1 parent bac7a6f5da
commit 6ec2b228ae
8 files changed
+80 -61

No files matched your search

+3 -2
View File
@@ -42,7 +42,7 @@ 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>`
(58 checks, "All checks passed"). `/_test/publish` and `/_test/download` simulate a new (60 checks, "All checks passed"). `/_test/publish` and `/_test/download` simulate a new
publish and a requested revision reaching this site. 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.
@@ -72,7 +72,8 @@ that contains this repo on `sys.path` and importing the repo folder name (see
- Removing a library leaves the libraries it linked indirectly (e.g. Blender's - Removing a library leaves the libraries it linked indirectly (e.g. Blender's
`geometry_nodes_essentials.blend`): `remove_unused_libraries()` drops the ones whose data `geometry_nodes_essentials.blend`): `remove_unused_libraries()` drops the ones whose data
is no longer used, never a managed library. is no longer used, never a managed library.
- LODs are child collections of the asset collection (`COL_{asset}_LOD{n}`). Overrides - LODs are child collections of the asset collection (`COL_{asset}_LOD{lod}`, any name,
ordered by `SCENE_MANAGER_LODS`, then numbers, then names; the first is the default). Overrides
switch by `hide_viewport`/`hide_render` on the LOD override collections: Blender stores switch by `hide_viewport`/`hide_render` on the LOD override collections: Blender stores
them as override properties (kept through save, reload and resync) and hidden LODs are them as override properties (kept through save, reload and resync) and hidden LODs are
not evaluated. Collection instances instance the LOD collection itself. A second override's not evaluated. Collection instances instance the LOD collection itself. A second override's
+12 -10
View File
@@ -360,7 +360,7 @@ Lists the assets cast in the shot, with how many instances are loaded out of how
- **Collection:** from that file, `COL_<asset>`, else `<asset>`, else `environments` (set dressing files), else the file's only top-level collection. - **Collection:** from that file, `COL_<asset>`, else `<asset>`, else `environments` (set dressing files), else the file's only top-level collection.
- **Characters, props and vehicles** (`*_character`, `main_props`, `secondary_props`, `vehicles`) become **library overrides**, so animators can pose them. Other types, like sets and set props, become **collection instances**. - **Characters, props and vehicles** (`*_character`, `main_props`, `secondary_props`, `vehicles`) become **library overrides**, so animators can pose them. Other types, like sets and set props, become **collection instances**.
- **Where they go:** each asset type gets its own collection, `COL_main_character`, `COL_main_props`, `COL_bg_location`… - **Where they go:** each asset type gets its own collection, `COL_main_character`, `COL_main_props`, `COL_bg_location`…
- **Level of detail:** when the asset collection holds LOD collections (`COL_<asset>_LOD0`, `COL_<asset>_LOD1`…), LOD 0 is shown (the lowest LOD the file has, if not LOD 0). See [Levels of detail](#levels-of-detail). - **Level of detail:** when the asset collection holds LOD collections (`COL_<asset>_LODhigh`, `COL_<asset>_LOD0`…), only the first one is shown. See [Levels of detail](#levels-of-detail).
All of these can be changed in the [Configuration](#configuration). All of these can be changed in the [Configuration](#configuration).
@@ -369,32 +369,34 @@ 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" (click the globe to request it, then "v004 requested, waiting 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. Under an instance with levels of detail, one button per LOD (`LOD 0 1 2`), the shown one pressed. - **Arrow**: shows the asset's instances, each with a button to select its objects and one to remove it. Under an instance with levels of detail, one button per LOD (`LOD high mid low`), the shown one pressed.
- **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. - **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.
- **Selected LOD** (shown when a selected instance has LODs): switch the instances of the selected objects to that LOD. - **LOD of Selected** (shown when a selected instance has LODs): switch the instances of the selected objects to that LOD.
- The magnifier re-checks the versions. - The magnifier re-checks the versions.
Updating swaps the linked file and reloads it. Overrides keep their posing and animation, and instances keep their LOD (an instance showing a LOD the new version no longer has goes back to LOD 0). **Ctrl+Z** undoes removing, updating and LOD switches. Updating swaps the linked file and reloads it. Overrides keep their posing and animation, and instances keep their LOD (an instance showing a LOD the new version no longer has goes back to the first one). **Ctrl+Z** undoes removing, updating and LOD switches.
### Levels of detail ### Levels of detail
Rigs and models can hold their levels of detail as **child collections of the asset collection**, named `COL_<asset>_LOD<n>`. One LOD is shown at a time: Rigs and models can hold their levels of detail as **child collections of the asset collection**, named `COL_<asset>_LOD<name>`. The name can be anything: `high`, `mid`, `low`, or numbers `0`, `1`, `2`. One LOD is shown at a time:
``` ```
COL_bob COL_bob
├─ RIG_bob (shared: rig, controllers…) ├─ RIG_bob (shared: rig, controllers…)
├─ COL_bob_LOD0 shown ├─ COL_bob_LODhigh shown
├─ COL_bob_LOD1 hidden ├─ COL_bob_LODmid hidden
└─ COL_bob_LOD2 hidden └─ COL_bob_LODlow hidden
``` ```
- **Overrides** (characters, props, vehicles): the other LOD collections are hidden in the viewport and the render, so they cost nothing. The rig and everything outside the LOD collections stay; posing and animation are kept when switching. - **Overrides** (characters, props, vehicles): the other LOD collections are hidden in the viewport and the render, so they cost nothing. The rig and everything outside the LOD collections stay; posing and animation are kept when switching.
- **Collection instances** (sets, set props…): the instance shows the LOD collection itself. This needs everything of the asset to sit in its LOD collections: an asset with objects or collections next to them cannot switch LODs as an instance (load it as an override instead). - **Collection instances** (sets, set props…): the instance shows the LOD collection itself. This needs everything of the asset to sit in its LOD collections: an asset with objects or collections next to them cannot switch LODs as an instance (load it as an override instead).
Each instance has its own LOD: two instances of the same asset can show different LODs. The choice is saved with the file. The naming and the LOD shown on load are `SCENE_MANAGER_LOD_COLLECTION` and `SCENE_MANAGER_DEFAULT_LOD` in the [Configuration](#configuration). Each instance has its own LOD: two instances of the same asset can show different LODs. The choice is saved with the file.
The buttons follow `SCENE_MANAGER_LODS` (`high`, `mid`, `low`), and the first of these the file has is shown on load. LODs not in that list come after it: numbers in order (`0`, `1`, `2`… `10`), then other names A-Z. So a file with `COL_bob_LOD0`, `COL_bob_LOD1` shows LOD `0` on load. The naming is `SCENE_MANAGER_LOD_COLLECTION` (`COL_{asset}_LOD{lod}`): keep a fixed part like `LOD` in it, since every child collection matching it counts as a LOD. Both are in the [Configuration](#configuration).
### Assetize ### Assetize
@@ -454,7 +456,7 @@ Name formats use Python's `str.format`: `"sq{:04d}"` turns `10` into `sq0010`. `
| Sanity Check: panel | `CATEGORY_ORDER`, `MAX_ISSUES_DRAWN` | | Sanity Check: panel | `CATEGORY_ORDER`, `MAX_ISSUES_DRAWN` |
| Rename | `RENAME_SEPARATOR`, `RENAME_PADDING`, `RENAME_SIDES`, `RENAME_TYPE_SUFFIXES` (plus `OBJECT_PREFIXES` / `DATABLOCK_PREFIXES` for *By Type* prefixes) | | Rename | `RENAME_SEPARATOR`, `RENAME_PADDING`, `RENAME_SIDES`, `RENAME_TYPE_SUFFIXES` (plus `OBJECT_PREFIXES` / `DATABLOCK_PREFIXES` for *By Type* prefixes) |
| Shot Manager | `SHOT_VIEWER_RIG_GROUPS`, `SHOT_VIEWER_ASSET_TYPE_GROUPS`, `SHOT_NAME_FORMAT`, `SHOT_NUMBER_STEP`, `SHOT_FRAME_START`, `SHOT_DEFAULT_DURATION`, `SEQUENCE_NAME_FORMAT`, `SHOT_CAMERA_NAME_FORMAT`, `SHOT_CAMERA_RIG_NAME_FORMAT`, `SHOT_CAMERA_RIG_MODES` | | Shot Manager | `SHOT_VIEWER_RIG_GROUPS`, `SHOT_VIEWER_ASSET_TYPE_GROUPS`, `SHOT_NAME_FORMAT`, `SHOT_NUMBER_STEP`, `SHOT_FRAME_START`, `SHOT_DEFAULT_DURATION`, `SEQUENCE_NAME_FORMAT`, `SHOT_CAMERA_NAME_FORMAT`, `SHOT_CAMERA_RIG_NAME_FORMAT`, `SHOT_CAMERA_RIG_MODES` |
| Scene Manager | `LIBREFLOW_SERVER_FILE`, `LIBREFLOW_TIMEOUT`, `SCENE_MANAGER_FILE_PATTERNS`, `SCENE_MANAGER_COLLECTION_NAMES`, `SCENE_MANAGER_LINK_MODES`, `SCENE_MANAGER_LOD_COLLECTION`, `SCENE_MANAGER_DEFAULT_LOD`, `SCENE_MANAGER_TYPE_COLLECTION`, `SCENE_MANAGER_ASSETIZE_TASK` | | Scene Manager | `LIBREFLOW_SERVER_FILE`, `LIBREFLOW_TIMEOUT`, `SCENE_MANAGER_FILE_PATTERNS`, `SCENE_MANAGER_COLLECTION_NAMES`, `SCENE_MANAGER_LINK_MODES`, `SCENE_MANAGER_LOD_COLLECTION`, `SCENE_MANAGER_LODS`, `SCENE_MANAGER_TYPE_COLLECTION`, `SCENE_MANAGER_ASSETIZE_TASK` |
--- ---
+6 -4
View File
@@ -299,11 +299,13 @@ SCENE_MANAGER_LINK_MODES = {
"*": "INSTANCE", "*": "INSTANCE",
} }
# Levels of detail: child collections of the asset collection, one per LOD ({asset} = # Levels of detail: child collections of the asset collection, one per LOD ({asset} =
# asset name, {lod} = LOD number). An override shows one and hides the others; a # asset name, {lod} = LOD name, any name: COL_bob_LOD0, COL_bob_LODhigh...). An override
# collection instance instances it. # shows one and hides the others; a collection instance instances it. Keep a fixed part
# around {lod} ("LOD"): every child collection matching the pattern counts as a LOD.
SCENE_MANAGER_LOD_COLLECTION = "COL_{asset}_LOD{lod}" SCENE_MANAGER_LOD_COLLECTION = "COL_{asset}_LOD{lod}"
# LOD shown when an asset is loaded (the lowest one when missing). None: as in the file. # Order of the LOD names; the first one the file has is shown when an asset is loaded.
SCENE_MANAGER_DEFAULT_LOD = 0 # LODs not listed come after them: numbers in order (0, 1, 2...), then names A-Z.
SCENE_MANAGER_LODS = ["high", "mid", "low"]
# Collection gathering the loaded assets of a type, under the scene collection. # Collection gathering the loaded assets of a type, under the scene collection.
SCENE_MANAGER_TYPE_COLLECTION = "COL_{asset_type}" SCENE_MANAGER_TYPE_COLLECTION = "COL_{asset_type}"
# Default task of the assetize dialog. # Default task of the assetize dialog.
+6 -6
View File
@@ -5,7 +5,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 holding only LOD collections (COL_tree_LOD0, COL_tree_LOD1), plus an 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. unrelated top-level collection. v002 adds a mesh outside the LODs, v003 has no LOD.
car: a rig, COL_car: armature + COL_car_LOD0..2. car: a rig, COL_car: armature + named LODs COL_car_LODlow, _LODhigh, _LODmid.
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).
""" """
@@ -24,7 +24,7 @@ def mesh_object(name):
return bpy.data.objects.new(name, mesh) return bpy.data.objects.new(name, mesh)
def write(path, collection_name, meshes=1, rig=False, extra=False, node_group=False, lods=0): def write(path, collection_name, meshes=1, rig=False, extra=False, node_group=False, lods=()):
bpy.ops.wm.read_homefile(use_empty=True) bpy.ops.wm.read_homefile(use_empty=True)
scene = bpy.context.scene scene = bpy.context.scene
collection = bpy.data.collections.new(collection_name) collection = bpy.data.collections.new(collection_name)
@@ -42,7 +42,7 @@ def write(path, collection_name, meshes=1, rig=False, extra=False, node_group=Fa
ob = mesh_object(f"GEO_{collection_name}_{index}") ob = mesh_object(f"GEO_{collection_name}_{index}")
ob.parent = parent ob.parent = parent
collection.objects.link(ob) collection.objects.link(ob)
for lod in range(lods): for lod in lods:
lod_collection = bpy.data.collections.new(f"{collection_name}_LOD{lod}") lod_collection = bpy.data.collections.new(f"{collection_name}_LOD{lod}")
collection.children.link(lod_collection) collection.children.link(lod_collection)
ob = mesh_object(f"GEO_{collection_name}_LOD{lod}") ob = mesh_object(f"GEO_{collection_name}_LOD{lod}")
@@ -62,9 +62,9 @@ def write(path, collection_name, meshes=1, rig=False, extra=False, node_group=Fa
lib = os.path.join(WORKDIR, "lib") lib = os.path.join(WORKDIR, "lib")
write(os.path.join(lib, "bob", "v001", "rig.blend"), "COL_bob", meshes=1, rig=True, extra=True) write(os.path.join(lib, "bob", "v001", "rig.blend"), "COL_bob", meshes=1, 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, "bob", "v002", "rig.blend"), "COL_bob", meshes=2, rig=True, extra=True)
write(os.path.join(lib, "tree", "v001", "model.blend"), "COL_tree", meshes=0, extra=True, lods=2) write(os.path.join(lib, "tree", "v001", "model.blend"), "COL_tree", meshes=0, extra=True, lods=["0", "1"])
write(os.path.join(lib, "tree", "v002", "model.blend"), "COL_tree", meshes=1, extra=True, lods=2) write(os.path.join(lib, "tree", "v002", "model.blend"), "COL_tree", meshes=1, extra=True, lods=["0", "1"])
write(os.path.join(lib, "tree", "v003", "model.blend"), "COL_tree", meshes=1, extra=True) 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, "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=3) write(os.path.join(lib, "car", "v001", "car.blend"), "COL_car", meshes=0, rig=True, lods=["low", "high", "mid"])
print("Test assets written in", lib) print("Test assets written in", lib)
+16 -14
View File
@@ -132,40 +132,42 @@ check("car loaded once downloaded", loaded().get("car", (None,))[0] == "v001", m
print("LOD") print("LOD")
tree_lib, car_lib = bpy.data.libraries["tree"], bpy.data.libraries["car"] tree_lib, car_lib = bpy.data.libraries["tree"], bpy.data.libraries["car"]
tree, car = lib.instances(tree_lib)[0], lib.instances(car_lib)[0] tree, car = lib.instances(tree_lib)[0], lib.instances(car_lib)[0]
check("instance shows the default LOD", lib.current_lod(tree_lib, tree) == 0 and tree.instance_collection.name == "COL_tree_LOD0") check("numbered LODs: instance shows the first", lib.current_lod(tree_lib, tree) == "0" and tree.instance_collection.name == "COL_tree_LOD0")
check("override shows the default LOD", lib.current_lod(car_lib, car) == 0) check("named LODs in config order", list(lib.lods(car_lib, car)) == ["high", "mid", "low"], str(list(lib.lods(car_lib, car))))
check("named LODs: override shows the first in config order", lib.current_lod(car_lib, car) == "high")
hidden = {c.name: (c.hide_viewport, c.hide_render) for c in lib.lods(car_lib, car).values()} hidden = {c.name: (c.hide_viewport, c.hide_render) for c in lib.lods(car_lib, car).values()}
check( check(
"other LODs hidden (viewport and render)", "other LODs hidden (viewport and render)",
hidden == {"COL_car_LOD0": (False, False), "COL_car_LOD1": (True, True), "COL_car_LOD2": (True, True)}, hidden == {"COL_car_LODhigh": (False, False), "COL_car_LODmid": (True, True), "COL_car_LODlow": (True, True)},
str(hidden), str(hidden),
) )
evaluated = {ob.name for ob in bpy.context.evaluated_depsgraph_get().objects} evaluated = {ob.name for ob in bpy.context.evaluated_depsgraph_get().objects}
check("hidden LODs not evaluated", "GEO_COL_car_LOD0" in evaluated and not evaluated & {"GEO_COL_car_LOD1", "GEO_COL_car_LOD2"}) check("hidden LODs not evaluated", "GEO_COL_car_LODhigh" in evaluated and not evaluated & {"GEO_COL_car_LODmid", "GEO_COL_car_LODlow"})
bpy.ops.wtd_scene.set_lod(lod=2, library="car", instance=car.name) bpy.ops.wtd_scene.set_lod(lod="low", library="car", instance=car.name)
check("override switched to LOD 2", lib.current_lod(car_lib, car) == 2) check("override switched to low", lib.current_lod(car_lib, car) == "low")
for ob in bpy.context.view_layer.objects: for ob in bpy.context.view_layer.objects:
ob.select_set(ob in (tree, bpy.data.objects["RIG_COL_car"])) 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("LOD of selected", bpy.ops.wtd_scene.set_lod(lod="1", selected=True) == {"FINISHED"})
check("both switched to LOD 1", (lib.current_lod(tree_lib, tree), lib.current_lod(car_lib, car)) == (1, 1)) 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") == {"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) second = lib.add_instance(scene, bpy.context.view_layer, car_lib)
check( check(
"second override: LODs found by their linked names", "second override: LODs found by their linked names",
list(lib.lods(car_lib, second)) == [0, 1, 2] and lib.current_lod(car_lib, second) == 0, list(lib.lods(car_lib, second)) == ["high", "mid", "low"] and lib.current_lod(car_lib, second) == "high",
str([c.name for c in second.children]), str([c.name for c in second.children]),
) )
lib.remove_instance(second) lib.remove_instance(second)
lib.update("tree", {"name": "v002", "path": os.path.join(WORKDIR, "lib", "tree", "v002", "model.blend")}) lib.update("tree", {"name": "v002", "path": os.path.join(WORKDIR, "lib", "tree", "v002", "model.blend")})
tree = lib.instances(bpy.data.libraries["tree"])[0] tree = lib.instances(bpy.data.libraries["tree"])[0]
check("LOD kept by an update", lib.current_lod(bpy.data.libraries["tree"], tree) == 1) 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") == {"CANCELLED"})
lib.update("tree", {"name": "v003", "path": os.path.join(WORKDIR, "lib", "tree", "v003", "model.blend")}) lib.update("tree", {"name": "v003", "path": os.path.join(WORKDIR, "lib", "tree", "v003", "model.blend")})
tree = lib.instances(bpy.data.libraries["tree"])[0] 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)) check("LODs gone: whole collection instanced", tree.instance_collection.name == "COL_tree" and not lib.lods(bpy.data.libraries["tree"], tree))
lib.update("tree", {"name": "v001", "path": os.path.join(WORKDIR, "lib", "tree", "v001", "model.blend")}) lib.update("tree", {"name": "v001", "path": os.path.join(WORKDIR, "lib", "tree", "v001", "model.blend")})
tree = lib.instances(bpy.data.libraries["tree"])[0] tree = lib.instances(bpy.data.libraries["tree"])[0]
check("LODs back: default LOD", lib.current_lod(bpy.data.libraries["tree"], tree) == 0) check("LODs back: default LOD", lib.current_lod(bpy.data.libraries["tree"], tree) == "0")
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")
@@ -256,7 +258,7 @@ bpy.ops.wm.open_mainfile(filepath=path)
sm._on_load_post() 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", "lamp", "rock", "tree"})
car_lib = bpy.data.libraries["car"] car_lib = bpy.data.libraries["car"]
check("LOD kept after reopening", lib.current_lod(car_lib, lib.instances(car_lib)[0]) == 1) check("LOD kept after reopening", lib.current_lod(car_lib, lib.instances(car_lib)[0]) == "low")
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")
+25 -14
View File
@@ -177,12 +177,24 @@ def add_instance(scene, view_layer, library, parent=None):
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
def _lod_pattern(asset_name): def _lod_pattern(asset_name):
before, _, after = config.SCENE_MANAGER_LOD_COLLECTION.partition("{lod}") before, _, after = config.SCENE_MANAGER_LOD_COLLECTION.partition("{lod}")
return re.compile(re.escape(before.format(asset=asset_name)) + r"(\d+)" + re.escape(after.format(asset=asset_name)) + "$") return re.compile(
re.escape(before.format(asset=asset_name)) + r"(.+?)" + re.escape(after.format(asset=asset_name)) + r"(?:\.\d{3,})?$"
)
def lod_order(name):
"""Sort key of a LOD name: config SCENE_MANAGER_LODS order, then numbers, then names A-Z."""
listed = [str(n) for n in config.SCENE_MANAGER_LODS]
if name in listed:
return (0, listed.index(name), "")
if name.isdigit():
return (1, int(name), "")
return (2, 0, name.lower())
def lods(library, instance): def lods(library, instance):
"""{LOD number: collection}: the LOD child collections of the asset collection, linked """{LOD name: collection} in config order: the LOD child collections of the asset
for a collection instance, their overrides for an override.""" collection, linked for a collection instance, their overrides for an override."""
root = linked_collection(library) if isinstance(instance, bpy.types.Object) else instance root = linked_collection(library) if isinstance(instance, bpy.types.Object) else instance
if root is None: if root is None:
return {} return {}
@@ -193,8 +205,8 @@ def lods(library, instance):
name = child.override_library.reference.name if child.override_library else child.name name = child.override_library.reference.name if child.override_library else child.name
match = pattern.match(name) match = pattern.match(name)
if match: if match:
found[int(match.group(1))] = child found[match.group(1)] = child
return dict(sorted(found.items())) return {name: found[name] for name in sorted(found, key=lod_order)}
def current_lod(library, instance): def current_lod(library, instance):
@@ -211,7 +223,7 @@ def set_lod(library, instance, lod):
they are not evaluated), a collection instance instances the LOD collection.""" they are not evaluated), a collection instance instances the LOD collection."""
found = lods(library, instance) found = lods(library, instance)
if lod not in found: if lod not in found:
available = ", ".join(str(n) for n in found) or "none" available = ", ".join(found) or "none"
raise LoadError(f"{instance.name}: no LOD {lod} (LODs in the file: {available})") raise LoadError(f"{instance.name}: no LOD {lod} (LODs in the file: {available})")
if isinstance(instance, bpy.types.Object): if isinstance(instance, bpy.types.Object):
root = linked_collection(library) root = linked_collection(library)
@@ -223,23 +235,22 @@ def set_lod(library, instance, lod):
) )
instance.instance_collection = found[lod] instance.instance_collection = found[lod]
return return
for number, collection in found.items(): for name, collection in found.items():
collection.hide_viewport = collection.hide_render = number != lod collection.hide_viewport = collection.hide_render = name != lod
def apply_default_lod(library, instance): def apply_default_lod(library, instance):
"""After a load or an update: show config SCENE_MANAGER_DEFAULT_LOD (else the lowest LOD) """After a load or an update: show the first LOD in config order when the instance shows
when the instance shows no single LOD. An instance of a LOD the file no longer has goes no single LOD. An instance of a LOD the file no longer has goes back to the whole asset
back to the whole asset collection first.""" collection first."""
found = lods(library, instance) found = lods(library, instance)
root = linked_collection(library) root = linked_collection(library)
if isinstance(instance, bpy.types.Object) and root and instance.instance_collection not in (root, *found.values()): if isinstance(instance, bpy.types.Object) and root and instance.instance_collection not in (root, *found.values()):
instance.instance_collection = root instance.instance_collection = root
default = config.SCENE_MANAGER_DEFAULT_LOD if not found or current_lod(library, instance) is not None:
if not found or default is None or current_lod(library, instance) is not None:
return return
try: try:
set_lod(library, instance, default if default in found else min(found)) set_lod(library, instance, next(iter(found)))
except LoadError: except LoadError:
pass # a collection instance with content outside its LODs keeps the whole collection pass # a collection instance with content outside its LODs keeps the whole collection
+2 -2
View File
@@ -13,7 +13,7 @@ import time
import bpy import bpy
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
from bpy.props import BoolProperty, EnumProperty, IntProperty, PointerProperty, StringProperty from bpy.props import BoolProperty, EnumProperty, PointerProperty, StringProperty
from .. import config from .. import config
from . import ipc from . import ipc
@@ -737,7 +737,7 @@ class WTD_OT_scene_set_lod(bpy.types.Operator):
bl_description = "Show this level of detail (one of the asset's LOD collections)" bl_description = "Show this level of detail (one of the asset's LOD collections)"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
lod: IntProperty(name="LOD", min=0) lod: StringProperty(name="LOD", description="LOD name (the {lod} part of the LOD collection name)")
library: StringProperty(options={"SKIP_SAVE"}) library: StringProperty(options={"SKIP_SAVE"})
instance: StringProperty(description="Only this instance (object or collection name)", 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"}) selected: BoolProperty(description="The instances of the selected objects", options={"SKIP_SAVE"})
+10 -9
View File
@@ -488,9 +488,9 @@ class _SceneLoadedPanel:
current = sm_library.current_lod(library, instance) current = sm_library.current_lod(library, instance)
sub = col.row(align=True) sub = col.row(align=True)
sub.label(text="LOD", icon="BLANK1") sub.label(text="LOD", icon="BLANK1")
for number in lods: for name in lods:
op = sub.operator(sm.WTD_OT_scene_set_lod.bl_idname, text=str(number), depress=number == current) op = sub.operator(sm.WTD_OT_scene_set_lod.bl_idname, text=name, depress=name == current)
op.lod, op.library, op.instance = number, library.name, instance.name op.lod, op.library, op.instance = name, library.name, instance.name
layout.separator() layout.separator()
row = layout.row(align=True) row = layout.row(align=True)
@@ -502,13 +502,14 @@ class _SceneLoadedPanel:
if sm_library.lods(library, instance) if sm_library.lods(library, instance)
] ]
if selected: if selected:
split = layout.split(factor=0.4, align=True) col = layout.column(align=True)
split.label(text="Selected LOD") col.label(text="LOD of Selected")
row = split.row(align=True) row = col.row(align=True)
shown = {sm_library.current_lod(library, instance) for library, instance in selected} shown = {sm_library.current_lod(library, instance) for library, instance in selected}
for number in sorted({n for library, instance in selected for n in sm_library.lods(library, instance)}): names = {name for library, instance in selected for name in sm_library.lods(library, instance)}
op = row.operator(sm.WTD_OT_scene_set_lod.bl_idname, text=str(number), depress=shown == {number}) for name in sorted(names, key=sm_library.lod_order):
op.lod, op.selected = number, True op = row.operator(sm.WTD_OT_scene_set_lod.bl_idname, text=name, depress=shown == {name})
op.lod, op.selected = name, True
class _SceneAssetizePanel: class _SceneAssetizePanel: