diff --git a/.specstory/history/2025-08-20_22-43Z-creating-a-blender-extension-for-asset-replacement.md b/.specstory/history/2025-08-20_22-43Z-creating-a-blender-extension-for-asset-replacement.md index 120789c..e0b5abe 100644 --- a/.specstory/history/2025-08-20_22-43Z-creating-a-blender-extension-for-asset-replacement.md +++ b/.specstory/history/2025-08-20_22-43Z-creating-a-blender-extension-for-asset-replacement.md @@ -16232,3 +16232,54 @@ Try it: --- +_**User**_ + +that relocator works fantastically. now we just need to be able to detect indirect links that are missing, make their parent library blend status indirect, and we're golden. the indirect status should only apply to library blends that have an indirectly linked blend that is missing. + +--- + +_**Assistant**_ + +
Thought Process**Exploring Blender libraries** + +I’m thinking about how to use Blender’s `bpy.data.libraries` to list not just the direct libraries in a scene but also the indirect ones. Normally, these indirect libraries aren’t loaded in the main file, but after relocation or reload, Blender can have instances of both. In the `outliner`, the indirectly linked libraries show up under "Libraries." I want to make a safe algorithm to build a set of unique library filepaths, covering both direct and indirect libraries. This could be really useful! +**Marking indirect libraries** + +I need to mark a library item as 'indirect' only if it has a missing indirectly linked dependency. They mentioned that if a top-level library L is directly linked in the main file but internally links to a missing library M, I should mark L as indirect, sort of like a warning. Earlier, they wanted missing libraries to be highlighted in red, but now they're focusing only on those missing indirect dependencies. I wonder if I can detect this by trying to open the library file path with `bpy.data.libraries.load`. It’s a bit risky since deleted data might cause issues, but using `bpy.types.BlendDataLibraries` could help with loading. +**Detecting indirect libraries** + +I'm looking into using `bpy.data.libraries.load` as a context manager that might give sets of datablocks and possibly a `lazy` option. In Blender 4.5, asset reading is a thing, but earlier there was data loss. Alternatively, I could run `bpy.ops.file.find_missing_files()`, although it seems heavy for the system. A better option might be scanning `bpy.data.libraries` to find libraries not directly used by any ID. There's a property `Library.indirect`, and I recall attributes of `bpy.types.Library` like `filepath`, `version`, and `id`. I could also check the `ID.is_library_indirect` property on datablocks to detect indirect links for libraries. +**Marking indirect dependencies** + +They want to mark a parent as indirect if it has missing indirect dependencies. I need to scan all IDs where `is_library_indirect` is true and collect their source library file paths, then map them to the direct parent library referencing them. But which parent? It’s the direct library that links to the indirect one. In the current file, objects referencing an indirect library won’t have their library property equal to L. This means the main scene shouldn't reference M directly — only L's internal IDs should reference M. So, I can’t use `is_library_indirect` on the scene's IDs to detect this. Instead, I should try to reload libraries and catch any errors to identify missing indirect libraries. +**Detecting indirect library dependencies** + +I want to figure out how to mark a parent as indirect when it has missing indirect dependencies. A heuristic I think could work is to scan all IDs with `is_library_indirect` set to true and collect their source library file paths. Then, I need to detect if any direct libraries link to these indirectly linked libraries. I might attempt to reload direct libraries to find any missing dependencies. + +To keep it efficient, I'll implement a lightweight binary parser to read the .blend file headers for 'LI' blocks. This way, I can avoid heavy scans that have caused crashes before. During the `find_libraries_in_folders` operation, I can determine which direct library references a missing library, helping me mark the relevant parent libraries as indirect if they have unresolved dependencies. +**Implementing indirect library detection** + +I'm thinking of a pragmatic strategy to detect and mark indirect libraries as missing. Using `bpy.ops.outliner.lib_operation(type='RELOCATE')` seems complex, so I might analyze console errors like "Cannot find lib 'path'" to identify which parent library references a missing library. If a relocation attempt fails, I can set a flag on that parent library item to mark it as indirect. + +Additionally, I can implement a new operator that reads the .blend file for library paths to identify missing references without altering the scene. I'll limit the read size for efficiency and avoid previous crashes. Once done, the UI can update to reflect any indirect statuses correctly. I'll adjust the code in `operators.py` to support this logic.
+ +--- + + + +--- + + + +--- + +- I added a context-free relocate button that lets you pick a new .blend and reloads that specific library. Great to hear it works well. + +- I’ve now implemented missing-indirect detection per your rule: + - A library is marked INDIRECT only if it appears to reference at least one missing indirectly linked .blend. + - Implementation scans each library’s .blend bytes for plausible .blend paths and checks existence; relative paths (//) are ignored; any missing absolute .blend reference marks the parent as INDIRECT. + +- Sorting and UI remain the same; only the parent’s `is_indirect` flag is set when a missing indirect child is detected. + +--- + diff --git a/operators.py b/operators.py index d44af79..d7061d8 100644 --- a/operators.py +++ b/operators.py @@ -101,6 +101,76 @@ class DLM_OT_scan_linked_assets(Operator): return "Unknown" return os.path.basename(filepath) + # Helper: naive scan of a .blend file to find referenced library paths + def scan_blend_for_missing_indirects(blend_path: str) -> bool: + """Return True if the .blend likely references at least one missing + library (.blend) path. This uses a conservative byte-string scan that + never loads data into Blender and is thus context-free and safe. + + Strategy: look for substrings ending in .blend inside the file bytes + and expand left/right to the nearest NUL byte or line break to + reconstruct a plausible filesystem path. If any such path does not + exist on disk, we consider it an indirect missing dependency. + """ + try: + if not blend_path or not os.path.exists(blend_path): + return False + with open(blend_path, 'rb') as f: + data = f.read() + except Exception: + return False + + missing_found = False + needle = b'.blend' + start = 0 + max_hits = 100 # safety cap + hits = 0 + data_len = len(data) + while True: + idx = data.find(needle, start) + if idx == -1: + break + hits += 1 + if hits > max_hits: + break + # expand left to a separator or NUL + left = idx + while left > 0 and data[left-1] not in (0, 10, 13): # not NUL/\n/\r + # allow typical path chars; stop on unlikely byte + if data[left-1] in b'\t': + break + left -= 1 + # keep paths bounded + if idx - left > 512: + break + # expand right a little (after .blend there should be NUL/space) + right = idx + len(needle) + while right < data_len and data[right] not in (0, 10, 13): + if data[right] in b' \t': + break + right += 1 + if right - idx > 512: + break + try: + raw = data[left:right] + # decode best-effort + s = raw.decode('utf-8', errors='ignore') + # heuristic: must contain a slash or backslash to be a path + if ('/' in s) or ('\\' in s): + # trim any trailing quotes/spaces + s = s.strip().strip('"\'') + # relative paths are considered unknown; skip + if s.startswith('//'): + pass + else: + if not os.path.exists(s): + missing_found = True + break + except Exception: + pass + start = idx + len(needle) + return missing_found + # Scan all data collections for linked items all_libraries = set() @@ -144,38 +214,31 @@ class DLM_OT_scan_linked_assets(Operator): # Store results in scene properties context.scene.dynamic_link_manager.linked_assets_count = len(all_libraries) - # Create library items for the UI with better indirect detection + # Create library items for the UI, and compute parent-indirect status + # Rule: A library is flagged 'indirect' ONLY if it has at least one + # missing indirect dependency. library_items = [] for filepath in sorted(all_libraries): - if filepath: - lib_item = context.scene.dynamic_link_manager.linked_libraries.add() - lib_item.filepath = filepath - lib_item.name = get_library_name(filepath) - lib_item.is_missing = is_file_missing(filepath) - - # Better indirect link detection - # A library is indirect if it's linked from within another linked library - is_indirect = False - - # Check if this library filepath appears in any other library's linked datablocks - for other_lib in all_libraries: - if other_lib != filepath: # Don't check against self - # Look for datablocks that are linked from this other library - for obj in bpy.data.objects: - if (hasattr(obj, 'library') and obj.library and - obj.library.filepath == other_lib and - hasattr(obj, 'data') and obj.data and - hasattr(obj.data, 'library') and obj.data.library and - obj.data.library.filepath == filepath): - is_indirect = True - break - if is_indirect: - break - - lib_item.is_indirect = is_indirect - - # Store for sorting - library_items.append((lib_item, filepath)) + if not filepath: + continue + lib_item = context.scene.dynamic_link_manager.linked_libraries.add() + lib_item.filepath = filepath + lib_item.name = get_library_name(filepath) + lib_item.is_missing = is_file_missing(filepath) + + # Determine if this library should be marked indirect because it + # appears to reference at least one missing child library. + is_indirect_due_to_missing_child = False + try: + abspath = bpy.path.abspath(filepath) if filepath else filepath + is_indirect_due_to_missing_child = scan_blend_for_missing_indirects(abspath) + except Exception: + is_indirect_due_to_missing_child = False + + lib_item.is_indirect = bool(is_indirect_due_to_missing_child) + + # Store for sorting + library_items.append((lib_item, filepath)) # Sort libraries: missing first, then by name library_items.sort(key=lambda x: (not x[0].is_missing, get_library_name(x[1]).lower()))