Skip to content

Commit b246998

Browse files
falken10vdlMoult
andauthored
Linked IFC projects enhancement (multiple links to same project file) (#7607)
* Linked IFC projects enhancement (multiple links to same project file) - Implement link management system using UUIDs as identifiers to support multiple links to the same IFC file - Add georeferencing compatibility detection and UI display (NONE, NOT_COMPATIBLE, PARTIAL_COMPATIBLE, FULL_COMPATIBLE) - Support for duplicate link creation with Shift+D shortcut and automatic position offset - Add false origin and project north calculation from 3D cursor for MANUAL mode - Only store one cache per file, regardless of the amount of links - Prevent duplicate links based on filepath and position comparison - Improve error handling for missing files and loading failures - Update tests * Remove duplicate georef UI I try to avoid duplicate UI (especially for one that can be as sophisticated as georef - e.g. missing is WCS) as it means double the code, double the tests, potential user confusion. BTW the note about vertical datum isn't quite accurate as it may be included in the CRS definition so vertical datum is optional. * Remove depsgraph_update_post handler for update_link_ui_on_transform as per core developer feedback * Move get_projected_crs to geolocation module * Refactor get_projected_crs to simplify as per core developer feedback * Remove unused import of bonsai.tool from project module * Use IfcDocumentInformation per linked file and IfcDocumentReference for locaiton information * Refactor SaveBlendMetadataFile operator to remove try-except blocks and remove linked projects collections since they are recreated by bonsai * Cleanup removing empty collection instances for linked models in metadata.blend file and call determine_georeferencing_compatibility on link reload * Add locking mechanism for linked models and update UI to reflect lock status * Update logic that track IFC to execute_ifc_duplicate_operator instead of having it in execute() which does not track IFC undo/redo * Refactor link handling to use get_link_empty_handle and set_link_empty_handle methods which in turn use the standard blender-ifc integrations patters (tool.Ifc.get_object(doc_reference) and tool.Ifc.link(doc_reference, empty_handle) * remove operator.DuplicateLink and move it to tool.Project.duplicate_link() * Refactor link handling to use sequential identifiers (no need for STEP ID DocRef) * Refactor IFC linking logic to handle cases without a parent IFC file loaded. Firts link flase origin becomes parent origin * Lock should not affect selection. This makes it consistent with grid / spatial lock, and also toggle selectability is already implemented. * Remove unnecessary check for loaded library as Blender seems to do this internally already * Rename util to get_crs because in IFC4X3 you can also have geographic CRS not just projected * Remove unnecessary call to determine_georeferencing_compatibility This function is already always called prior to calculate_link_position so shouldn't be called here. It's also a very expensive function: as it currently stands, just to link a single IFC, ifcopenshell.open() is called 3 times. This reduces it to 2. * Store CRS as metadata for linked models, and compare metadata when indicating georeferencing compatibility Previously, to check georeferencing compatibility, ifcopenshell.open() was used. When linking large models, this adds considerable time and memory usage. This instead captures the georef as standard metadata in our .cache.json. This now reduces the ifcopenshell.open() calls back down to only 1 as necessary (see previous commit). * Use link index instead of link name to fetch link collection item Link name runs into issues with name uniqueness. This is why you created a function for "get next link ID". After this refactoring, we can no longer worry about uniqueness and that function may be removed. * Simplify reloadlink into just unload and reload (with cache disabled) This function should not be responsible for editing any data. * Remove unnecessary get_next_link_id as names no longer need uniqueness This now frees up the name variable to track a more meaningful, human name like IfcDocumentInformation's Name attribute. * Rewrite get / set link_empty_handle to just use the link directly This prevents needless logic to fetch the link and also removes issues related to duplicate names. * Temporarily remove logic in prop callback Right now, pretty much all the logic is done in a prop callback. In general logic in prop callbacks should be minimised, since it's hard to test and easily triggered as a domino effect of another change, and may also impact undo/redo. * Remove code that unnecessarily removes cache This code removes cache, which means any project unlinking an IFC auto clears the cache for any other project which doesn't make sense, and also breaks the ability to readd it quickly. * Rewrite link, unlink, load, and unload IFC There were a few issues tackled here: - Operators that change any IFC data must use tool.Ifc.Operator and _execute, otherwise undo/redo will break. That's one of the risks of using prop callbacks, as it is not explicit when an IFC edit happens. - The usage of IfcDocumentReference was not correct. The Location should store the URL, _not_ the position. The position should be in the Identification attribute. - The URL was stored in IfcDocumentInformation location, which does not work in IFC2X3. There are a few changes here to make it IFC2X3 compatible. - Generally move logic in operators, not prop callback. * Remove restriction around manual mode. Users should be able to use manual mode if they want. * Restore AUTOMATIC mode to identical behaviour to file open This is the first step to reusing cache files agnostic of the host. * Revert tests for a fresh start for updating tests * Revert "test_feature - clean up .ifc.cache. files after test was executed" This reverts commit 99ae768. * Update tests and reimplement calculations for matrix of empty handle Previously, the empty would always be placed at the origin, unless a "position" offset was present. This is a problem, because the "position" is simply a local offset relative to the Blender cache! If the cache was regenerated, the offsets would be outdated. Also, the cache appeared in different locations depending on the false origin mode, so the offset would mean different things to different people. Instead, a more robust method is: 1. When you link a file, a Blender cache is generated. The Blender origin of this cache is arbitrary! It depends on the user's false origin mode and is purely a Blender session specific thing. 2. When you load a link, a link is _always_ loaded into the correct location with regards to IFC global coordinates. All math is done from the perspective of IFC. 3. If you choose to transform (move / rotate / scale!?) this link from its correct location, that gets recorded as a 4x4 transformation matrix. Note: I haven't implemented this properly yet. Tests all pass, with a minor modification to the new behaviour that false origin mode now won't affect the location it ends up in, only the generation of the cache. * Remove arbitrary convention around display name Not needed anymore now that A/M/D is a detail and not significant on actual coordinates, and also that the UUID is no longer needed. * Simplify implementation of loading linked models when opening an IFC * Move link matrix calculation from operator to tool for reuse * Implement editing link location and calculation of transformation matrix I changed my mind on the is_locked thing, since it isn't clear to the user that locking need to be done to save changes. * Remove old is_locked, prop update callback no longer needed (dedicated operator instead), remove old calculation code * Simplify code related to placed_as_per_georef * For now, simple skip for duplicate / delete IMO duplicate / delete / move a link are very rare and explicit operations. * Update tests * Remove host_model coordinate data as cache is no longer host model dependent * Move icons outside list because there are too many * Minor tweaks --------- Co-authored-by: Dion Moult <dionmoult@gmail.com> Co-authored-by: Dion Moult <dion@thinkmoult.com>
1 parent a88c593 commit b246998

File tree

20 files changed

+639
-448
lines changed

20 files changed

+639
-448
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ src/bonsai/bonsai/translations.py
8282

8383
# bonsai test temp files
8484
src/bonsai/test/files/temp
85+
src/bonsai/test/files/basic.ifc.cache.blend
86+
src/bonsai/test/files/basic.ifc.cache.sqlite
8587

8688
# bonsai data
8789
src/bonsai/bonsai/bim/data/build/

src/bonsai/bonsai/bim/export_ifc.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ def export(self):
5252
self.set_header()
5353
IfcStore.update_cache()
5454
self.sync_all_objects()
55-
tool.Project.save_linked_models_to_ifc()
5655
extension = self.ifc_export_settings.output_file.split(".")[-1].lower()
5756
if extension == "ifczip":
5857
with tempfile.TemporaryDirectory() as unzipped_path:

src/bonsai/bonsai/bim/module/drawing/operator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -928,7 +928,7 @@ def generate_linework(self, context: bpy.types.Context) -> Union[str, None]:
928928

929929
props = tool.Project.get_project_props()
930930
for link in props.get_loaded_links_for_drawings():
931-
files[link.name] = self.get_linked_file(link)
931+
files[link.filepath] = self.get_linked_file(link)
932932

933933
target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"]
934934
self.setup_serialiser(target_view)
@@ -1374,7 +1374,7 @@ def is_manifold(self, obj) -> bool:
13741374
return True
13751375

13761376
def get_linked_file(self, link: "Link") -> ifcopenshell.file:
1377-
link_path = link.name
1377+
link_path = link.filepath
13781378
ifc_file = IfcStore.session_files.get(link_path, None)
13791379
if ifc_file is not None:
13801380
return ifc_file

src/bonsai/bonsai/bim/module/drawing/ui.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def draw(self, context):
8989
for link in links:
9090
row = panel.row(align=True)
9191
split = row.split(factor=0.9)
92-
split.label(text=link.name, icon="FILE")
92+
split.label(text=link.filepath, icon="FILE")
9393
split.prop(link, "include_in_drawings", text="")
9494
else:
9595
panel.label(text="No IFC projects linked and loaded.")

src/bonsai/bonsai/bim/module/geometry/operator.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,7 @@ def _execute(self, context: bpy.types.Context):
902902

903903
if not is_valid_data_block:
904904
continue
905+
905906
element = tool.Ifc.get_entity(obj)
906907
if element:
907908
if tool.Geometry.is_locked(element):
@@ -912,6 +913,9 @@ def _execute(self, context: bpy.types.Context):
912913
if ifcopenshell.util.element.get_pset(element, "BBIM_Array"):
913914
self.report({"INFO"}, "Elements that are part of an array cannot be deleted.")
914915
continue
916+
if element.is_a("IfcDocumentReference"):
917+
self.report({"INFO"}, "Linked models cannot be deleted.")
918+
continue
915919
if element.is_a("IfcGridAxis"):
916920
# Deleting the last W axis is OK
917921
if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
@@ -1208,6 +1212,11 @@ def execute_ifc_duplicate_operator(operator: bpy.types.Operator, context: bpy.ty
12081212
operator.report({"ERROR"}, f"Drawing '{obj.name}' not duplicated.")
12091213
continue
12101214

1215+
if element.is_a("IfcDocumentReference"):
1216+
objects_to_remove.add(obj)
1217+
operator.report({"ERROR"}, f"Linked model '{obj.name}' not duplicated.")
1218+
continue
1219+
12111220
if tool.Geometry.is_locked(element):
12121221
objects_to_remove.add(obj)
12131222
operator.report({"ERROR"}, lock_error_message(obj.name))

src/bonsai/bonsai/bim/module/georeference/operator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class RemoveGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):
5151
bl_description = "Remove the georeferencing"
5252

5353
def _execute(self, context):
54-
core.remove_georeferencing(tool.Ifc)
54+
core.remove_georeferencing(tool.Ifc, tool.Georeference)
5555

5656

5757
class EditGeoreferencing(bpy.types.Operator, tool.Ifc.Operator):

src/bonsai/bonsai/bim/module/georeference/prop.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -224,16 +224,12 @@ class BIMGeoreferenceProperties(PropertyGroup):
224224
x_axis_ordinate: StringProperty(name="X Axis Ordinate", update=update_grid_north_vector)
225225
x_axis_is_null: BoolProperty(name="X Axis Is Null")
226226

227-
# These are only for reference to capture data about a host model from a linked model
228-
# If you relink a model from a new host origin, we can autodetect it in theory with this
229-
host_model_origin: StringProperty(name="Host Model Origin")
230-
host_model_origin_si: StringProperty(name="Host Model Origin SI")
231-
host_model_project_north: StringProperty(name="Host Model Angle to Grid North")
232-
233227
# This is the ENH in project units and SI units of the Blender session's 0,0,0.
234228
# These are only for reference, using tool.Georeference.set_model_origin on
235229
# project load, project create, and when linking for the first time from an
236230
# empty Blender session.
231+
model_is_georeferenced: BoolProperty(name="Model Is Georeferenced")
232+
model_crs: StringProperty(name="Model CRS")
237233
model_origin: StringProperty(name="Model Origin")
238234
model_origin_si: StringProperty(name="Model Origin SI")
239235
model_project_north: StringProperty(name="Model Angle to Grid North")
@@ -275,10 +271,6 @@ class BIMGeoreferenceProperties(PropertyGroup):
275271
x_axis_ordinate: str
276272
x_axis_is_null: bool
277273

278-
host_model_origin: str
279-
host_model_origin_si: str
280-
host_model_project_north: str
281-
282274
model_origin: str
283275
model_origin_si: str
284276
model_project_north: str

src/bonsai/bonsai/bim/module/project/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,27 +31,31 @@
3131
operator.BIM_OT_load_clipping_planes,
3232
operator.BIM_OT_save_clipping_planes,
3333
operator.ChangeLibraryElement,
34+
operator.ClearMeasurement,
3435
operator.ClearRecentIFCProjects,
3536
operator.CreateClippingPlane,
3637
operator.CreateProject,
3738
operator.DisableCulling,
3839
operator.DisableEditingHeader,
40+
operator.DisableEditingLink,
3941
operator.EditHeader,
42+
operator.EditLink,
4043
operator.EditProjectLibrary,
4144
operator.EnableCulling,
4245
operator.EnableEditingHeader,
46+
operator.EnableEditingLink,
4347
operator.ExportIFC,
4448
operator.FlipClippingPlane,
4549
operator.IFCFileHandlerOperator,
4650
operator.ImageScalingTool,
4751
operator.LinkIfc,
52+
operator.LoadBlendMetadataAndIFC,
4853
operator.LoadLink,
4954
operator.LoadLinkedProject,
5055
operator.LoadProject,
5156
operator.LoadProjectElements,
52-
operator.MeasureTool,
5357
operator.MeasureFaceAreaTool,
54-
operator.ClearMeasurement,
58+
operator.MeasureTool,
5559
operator.NewProject,
5660
operator.QueryLinkedElement,
5761
operator.RefreshClippingPlanes,
@@ -69,7 +73,6 @@
6973
operator.UnassignLibraryDeclaration,
7074
operator.UnlinkIfc,
7175
operator.UnloadLink,
72-
operator.LoadBlendMetadataAndIFC,
7376
workspace.ExploreHotkey,
7477
prop.LibraryBreadcrumb,
7578
prop.LibraryElement,

0 commit comments

Comments
 (0)