mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-16 21:42:19 +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
|
same file with a different filter gets its own cache; both filters survive
|
||||||
save → reopen → reload.
|
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
|
### External styles + layerset slicing in the linked loader
|
||||||
|
|
||||||
`LoadLinkedProject.get_external_material(style_id)` resolves a style id → appended
|
`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[1] = ",".join([str(o) for o in np.eye(4).flatten().tolist()])
|
||||||
reference.Location = filepath.replace("\\", "/")
|
reference.Location = filepath.replace("\\", "/")
|
||||||
# Persist the filter per reference (Description is IFC4+ only).
|
# 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"):
|
if description and hasattr(reference, "Description"):
|
||||||
reference.Description = description
|
reference.Description = description
|
||||||
new.ifc_definition_id = reference.id()
|
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:
|
if tool.Ifc.get() and link.ifc_definition_id:
|
||||||
reference = tool.Ifc.get().by_id(link.ifc_definition_id)
|
reference = tool.Ifc.get().by_id(link.ifc_definition_id)
|
||||||
if hasattr(reference, "Description"):
|
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)
|
bpy.ops.bim.unload_link(link_index=self.link_index)
|
||||||
return bpy.ops.bim.load_link(
|
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
|
# gizmo polls gate on each preview's is_active flag, and a stuck flag
|
||||||
# persisted through the save would silently hide them on reload.
|
# persisted through the save would silently hide them on reload.
|
||||||
preview_base.discard_pending_previews(context.scene)
|
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
|
# 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
|
# info isn't immediately overwritten by the success message in Blender's
|
||||||
# status bar (only the latest self.report({"INFO"}, ...) sticks).
|
# status bar (only the latest self.report({"INFO"}, ...) sticks).
|
||||||
|
|||||||
@@ -113,29 +113,55 @@ class Project(bonsai.core.tool.Project):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def encode_link_filter(cls, query: str, exclude: str) -> Union[str, None]:
|
def encode_link_filter(cls, query: str, exclude: str, loaded: bool = False) -> Union[str, None]:
|
||||||
"""Serialize a link's filter for IfcDocumentReference.Description.
|
"""Serialize a link's filter and state for IfcDocumentReference.Description.
|
||||||
|
|
||||||
A plain include query is stored as-is (backwards compatible); an
|
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:
|
if exclude or loaded:
|
||||||
return json.dumps({"include": query, "exclude": exclude})
|
return json.dumps({"include": query, "exclude": exclude, "loaded": loaded})
|
||||||
return query or None
|
return query or None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def decode_link_filter(cls, description: Union[str, None]) -> tuple[str, str]:
|
def decode_link_filter(cls, description: Union[str, None]) -> tuple[str, str, bool]:
|
||||||
"""Get (query, exclude) from a reference Description written by encode_link_filter."""
|
"""Get (query, exclude, loaded) from a Description written by encode_link_filter."""
|
||||||
if not description:
|
if not description:
|
||||||
return "", ""
|
return "", "", False
|
||||||
if description.startswith("{"):
|
if description.startswith("{"):
|
||||||
try:
|
try:
|
||||||
data = json.loads(description)
|
data = json.loads(description)
|
||||||
if isinstance(data, dict):
|
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:
|
except json.JSONDecodeError:
|
||||||
pass
|
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
|
@classmethod
|
||||||
def calculate_link_matrix(cls, link: Link) -> Matrix:
|
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)
|
location_counts: defaultdict[str, int] = defaultdict(int)
|
||||||
for reference in references:
|
for reference in references:
|
||||||
location_counts[reference.Location] += 1
|
location_counts[reference.Location] += 1
|
||||||
|
autoload_indices: list[int] = []
|
||||||
for reference in references:
|
for reference in references:
|
||||||
filepath = reference.Location
|
filepath = reference.Location
|
||||||
link = links.add()
|
link = links.add()
|
||||||
@@ -476,7 +503,7 @@ class Project(bonsai.core.tool.Project):
|
|||||||
# The selector filter used at link time is persisted per
|
# The selector filter used at link time is persisted per
|
||||||
# reference in its Description (IFC4+); restore it so
|
# reference in its Description (IFC4+); restore it so
|
||||||
# Reload/Load replay the filter.
|
# 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:
|
if not query and not exclude and location_counts[filepath] == 1:
|
||||||
# Fall back to the legacy sidecar cache JSON where older
|
# Fall back to the legacy sidecar cache JSON where older
|
||||||
# versions persisted the query. Only unambiguous: with
|
# versions persisted the query. Only unambiguous: with
|
||||||
@@ -490,6 +517,15 @@ class Project(bonsai.core.tool.Project):
|
|||||||
pass
|
pass
|
||||||
link.query = query
|
link.query = query
|
||||||
link.exclude = exclude
|
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
|
@classmethod
|
||||||
def get_project_library_elements(
|
def get_project_library_elements(
|
||||||
|
|||||||
Reference in New Issue
Block a user