mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
Bonsai: auto-load links that were loaded and visible at save time
At IFC save time each link reference Description gains a loaded flag (is_loaded and not is_hidden), extending the same JSON blob that carries the include/exclude filter; plain legacy strings decode as no-autoload. On project open, load_linked_models_from_ifc replays flagged links via load_link after restoring the list, warning and skipping missing files so they cannot break the open. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -134,6 +134,19 @@ default set when empty) − exclude, applied in `LoadLinkedProject` and per link
|
||||
same file with a different filter gets its own cache; both filters survive
|
||||
save → reopen → reload.
|
||||
|
||||
### Auto-load on open
|
||||
|
||||
Links that were **loaded and visible** at IFC save time auto-load when the project
|
||||
is reopened. `ExportIFC` calls `tool.Project.update_linked_models_state()`, which
|
||||
rewrites each reference's `Description` with a `loaded` flag
|
||||
(`is_loaded and not is_hidden`); `load_linked_models_from_ifc` replays flagged
|
||||
links via `load_link` after restoring the list (missing files warn and skip so
|
||||
they can't break project open). The flag extends the same JSON blob as the
|
||||
exclude — plain legacy strings decode as no-autoload. Trade-off: project open
|
||||
pays the link-load cost up front (fast on cache hit; a missing cache rebuilds in
|
||||
a background Blender, same as clicking Load). Verified headless: loaded+visible
|
||||
auto-loads; unloaded and loaded-but-hidden links stay unloaded.
|
||||
|
||||
### External styles + layerset slicing in the linked loader
|
||||
|
||||
`LoadLinkedProject.get_external_material(style_id)` resolves a style id → appended
|
||||
|
||||
@@ -1438,7 +1438,7 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
|
||||
reference[1] = ",".join([str(o) for o in np.eye(4).flatten().tolist()])
|
||||
reference.Location = filepath.replace("\\", "/")
|
||||
# Persist the filter per reference (Description is IFC4+ only).
|
||||
description = tool.Project.encode_link_filter(self.query, self.exclude)
|
||||
description = tool.Project.encode_link_filter(self.query, self.exclude, loaded=True)
|
||||
if description and hasattr(reference, "Description"):
|
||||
reference.Description = description
|
||||
new.ifc_definition_id = reference.id()
|
||||
@@ -1812,7 +1812,7 @@ class ReloadLink(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if tool.Ifc.get() and link.ifc_definition_id:
|
||||
reference = tool.Ifc.get().by_id(link.ifc_definition_id)
|
||||
if hasattr(reference, "Description"):
|
||||
reference.Description = tool.Project.encode_link_filter(link.query, link.exclude)
|
||||
reference.Description = tool.Project.encode_link_filter(link.query, link.exclude, loaded=True)
|
||||
|
||||
bpy.ops.bim.unload_link(link_index=self.link_index)
|
||||
return bpy.ops.bim.load_link(
|
||||
@@ -2162,6 +2162,8 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
|
||||
# gizmo polls gate on each preview's is_active flag, and a stuck flag
|
||||
# persisted through the save would silently hide them on reload.
|
||||
preview_base.discard_pending_previews(context.scene)
|
||||
# Links loaded and visible right now auto-load on the next open.
|
||||
tool.Project.update_linked_models_state()
|
||||
# Suffix is appended to the IFC save-success report below so the auto-commit
|
||||
# info isn't immediately overwritten by the success message in Blender's
|
||||
# status bar (only the latest self.report({"INFO"}, ...) sticks).
|
||||
|
||||
@@ -113,29 +113,55 @@ class Project(bonsai.core.tool.Project):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def encode_link_filter(cls, query: str, exclude: str) -> Union[str, None]:
|
||||
"""Serialize a link's filter for IfcDocumentReference.Description.
|
||||
def encode_link_filter(cls, query: str, exclude: str, loaded: bool = False) -> Union[str, None]:
|
||||
"""Serialize a link's filter and state for IfcDocumentReference.Description.
|
||||
|
||||
A plain include query is stored as-is (backwards compatible); an
|
||||
exclude promotes the value to a small JSON blob.
|
||||
exclude or a loaded state promotes the value to a small JSON blob.
|
||||
The loaded flag makes the link auto-load on the next project open.
|
||||
"""
|
||||
if exclude:
|
||||
return json.dumps({"include": query, "exclude": exclude})
|
||||
if exclude or loaded:
|
||||
return json.dumps({"include": query, "exclude": exclude, "loaded": loaded})
|
||||
return query or None
|
||||
|
||||
@classmethod
|
||||
def decode_link_filter(cls, description: Union[str, None]) -> tuple[str, str]:
|
||||
"""Get (query, exclude) from a reference Description written by encode_link_filter."""
|
||||
def decode_link_filter(cls, description: Union[str, None]) -> tuple[str, str, bool]:
|
||||
"""Get (query, exclude, loaded) from a Description written by encode_link_filter."""
|
||||
if not description:
|
||||
return "", ""
|
||||
return "", "", False
|
||||
if description.startswith("{"):
|
||||
try:
|
||||
data = json.loads(description)
|
||||
if isinstance(data, dict):
|
||||
return data.get("include", "") or "", data.get("exclude", "") or ""
|
||||
return (
|
||||
data.get("include", "") or "",
|
||||
data.get("exclude", "") or "",
|
||||
bool(data.get("loaded", False)),
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return description, ""
|
||||
return description, "", False
|
||||
|
||||
@classmethod
|
||||
def update_linked_models_state(cls) -> None:
|
||||
"""Persist each link's loaded/visible state onto its document reference.
|
||||
|
||||
Called at IFC save time so links that were loaded and visible
|
||||
auto-load the next time the project is opened.
|
||||
"""
|
||||
if not tool.Ifc.get():
|
||||
return
|
||||
for link in cls.get_project_props().links:
|
||||
if not link.ifc_definition_id:
|
||||
continue
|
||||
try:
|
||||
reference = tool.Ifc.get().by_id(link.ifc_definition_id)
|
||||
except RuntimeError:
|
||||
continue
|
||||
if hasattr(reference, "Description"):
|
||||
reference.Description = cls.encode_link_filter(
|
||||
link.query, link.exclude, loaded=link.is_loaded and not link.is_hidden
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def calculate_link_matrix(cls, link: Link) -> Matrix:
|
||||
@@ -463,6 +489,7 @@ class Project(bonsai.core.tool.Project):
|
||||
location_counts: defaultdict[str, int] = defaultdict(int)
|
||||
for reference in references:
|
||||
location_counts[reference.Location] += 1
|
||||
autoload_indices: list[int] = []
|
||||
for reference in references:
|
||||
filepath = reference.Location
|
||||
link = links.add()
|
||||
@@ -476,7 +503,7 @@ class Project(bonsai.core.tool.Project):
|
||||
# The selector filter used at link time is persisted per
|
||||
# reference in its Description (IFC4+); restore it so
|
||||
# Reload/Load replay the filter.
|
||||
query, exclude = cls.decode_link_filter(getattr(reference, "Description", None))
|
||||
query, exclude, loaded = cls.decode_link_filter(getattr(reference, "Description", None))
|
||||
if not query and not exclude and location_counts[filepath] == 1:
|
||||
# Fall back to the legacy sidecar cache JSON where older
|
||||
# versions persisted the query. Only unambiguous: with
|
||||
@@ -490,6 +517,15 @@ class Project(bonsai.core.tool.Project):
|
||||
pass
|
||||
link.query = query
|
||||
link.exclude = exclude
|
||||
if loaded:
|
||||
autoload_indices.append(len(links) - 1)
|
||||
|
||||
# Links that were loaded and visible at save time load automatically.
|
||||
for i in autoload_indices:
|
||||
if not Path(tool.Ifc.resolve_uri(links[i].filepath)).exists():
|
||||
print(f"WARNING: Not auto-loading missing linked model: {links[i].filepath}")
|
||||
continue
|
||||
bpy.ops.bim.load_link(link_index=i)
|
||||
|
||||
@classmethod
|
||||
def get_project_library_elements(
|
||||
|
||||
Reference in New Issue
Block a user