From f3f5e380ff1bab98c97aad99cb0e77294c4e0f51 Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Tue, 3 Jun 2025 11:19:07 +0200 Subject: [PATCH] BIM: ignore FreeCAD groups for IFC export, controlled by a user preference (#21583) * Add .db extension to git ignore list * BIM: new feature - add ignore groups option on IFC exports * BIM: create generic get_architectural_contents function --- .gitignore | 2 + src/Mod/BIM/ArchCommands.py | 135 ++++++++++++++++++ .../Resources/ui/preferences-ifc-export.ui | 47 +++++- src/Mod/BIM/importers/exportIFC.py | 61 +++++++- 4 files changed, 236 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 37c93765d7..d12651dca5 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,8 @@ *.o *.orig *.output +# Ignore .db files created by Qt Designer when editing .ui files +*.db qrc_*.cpp BuildLog.htm cmake_install.cmake diff --git a/src/Mod/BIM/ArchCommands.py b/src/Mod/BIM/ArchCommands.py index 8db79ecd2b..9595af1a1a 100644 --- a/src/Mod/BIM/ArchCommands.py +++ b/src/Mod/BIM/ArchCommands.py @@ -40,6 +40,7 @@ import DraftVecUtils from FreeCAD import Vector from draftutils import params +from draftutils.groups import is_group if FreeCAD.GuiUp: from PySide import QtGui,QtCore @@ -822,6 +823,140 @@ def getAllChildren(objectlist): obs.append(c) return obs +def get_architectural_contents( + initial_objects, + recursive=True, + discover_hosted_elements=True, + include_components_from_additions=False, + include_initial_objects_in_result=True +): + """ + Retrieves a flat list of unique architectural objects that are considered "contents" of or are + related to the given initial_objects. + + This includes: + - Children from .Group properties (hierarchical traversal if recursive=True). + - Architecturally hosted elements (e.g., windows in walls, rebars in structures) + if discover_hosted_elements=True. + - Optionally, components from .Additions properties. + + The traversal uses a queue and ensures objects are processed only once. + + Parameters: + ----------- + initial_objects : App::DocumentObject or list of App::DocumentObject + The starting object(s) from which to discover contents. + recursive : bool, optional + If True (default), recursively find contents of discovered group-like + objects (those with a .Group property and identified by draftutils.groups.is_group()). + discover_hosted_elements : bool, optional + If True (default), try to find elements like windows, doors, rebars + that are architecturally hosted by other elements encountered during traversal. + This relies on Draft.getType() and common Arch properties like .Hosts or .Host. + include_components_from_additions : bool, optional + If False (default), objects found in .Additions lists are not added. + Set to True if these components should be part of the discovered contents. + include_initial_objects_in_result : bool, optional + If True (default), the objects in initial_objects themselves will be + included in the output list (if not already discovered as a child of another). + If False, only their "descendant" contents are returned. + + Returns: + -------- + list of App::DocumentObject + A flat list of unique architectural document objects. + """ + + final_contents_list = [] + queue = [] + + if not isinstance(initial_objects, list): + initial_objects_list = [initial_objects] + else: + initial_objects_list = list(initial_objects) # Make a copy + + queue.extend(initial_objects_list) + + # Set to keep track of object names already added to the queue or fully processed + # This prevents duplicates in the queue and reprocessing. + processed_or_queued_names = set() + for item in initial_objects_list: # Pre-populate for initial items if they are to be added later + processed_or_queued_names.add(item.Name) + + + idx = 0 # Use an index for iterating the queue, as it can grow + while idx < len(queue): + obj = queue[idx] + idx += 1 + + # Add the current object to the final list if it's not already there. + # The decision to include initial_objects is handled by how they are first added to queue + # and this check. + if obj not in final_contents_list: + is_initial = obj in initial_objects_list + if (is_initial and include_initial_objects_in_result) or not is_initial: + final_contents_list.append(obj) + + children_to_add_to_queue_next = [] + + # 1. Hierarchical children from .Group (if recursive) + if recursive and is_group(obj) and hasattr(obj, "Group") and obj.Group: + for child in obj.Group: + if child.Name not in processed_or_queued_names: + children_to_add_to_queue_next.append(child) + processed_or_queued_names.add(child.Name) # Mark as queued + + # 2. Architecturally-hosted elements (if discover_hosted_elements) + if discover_hosted_elements: + host_types = ["Wall", "Structure", "CurtainWall", "Precast", "Panel", "Roof"] + if Draft.getType(obj) in host_types: + # Hosted elements are typically in the host's InList + for item_in_inlist in obj.InList: + element_to_check = item_in_inlist + if hasattr(item_in_inlist, "getLinkedObject"): # Resolve App::Link + linked = item_in_inlist.getLinkedObject() + if linked: + element_to_check = linked + + if element_to_check.Name in processed_or_queued_names: + continue + + is_confirmed_hosted = False + element_type = Draft.getType(element_to_check) + + if element_type == "Window": # This covers Arch Windows and Arch Doors + if hasattr(element_to_check, "Hosts") and obj in element_to_check.Hosts: + is_confirmed_hosted = True + elif element_type == "Rebar": + if hasattr(element_to_check, "Host") and obj == element_to_check.Host: + is_confirmed_hosted = True + + if is_confirmed_hosted: + children_to_add_to_queue_next.append(element_to_check) + processed_or_queued_names.add(element_to_check.Name) # Mark as queued + + # 3. Components from .Additions list (e.g., walls added to a main wall) + if include_components_from_additions and hasattr(obj, "Additions") and obj.Additions: + for addition_comp in obj.Additions: + actual_addition = addition_comp + if hasattr(addition_comp, "getLinkedObject"): # Resolve if Addition is an App::Link + linked_add = addition_comp.getLinkedObject() + if linked_add: + actual_addition = linked_add + + if actual_addition.Name not in processed_or_queued_names: + children_to_add_to_queue_next.append(actual_addition) + processed_or_queued_names.add(actual_addition.Name) # Mark as queued + + if children_to_add_to_queue_next: + # Add newly-discovered children to the end of the queue. This function uses an index + # (idx) to iterate through the queue, and queue.extend() adds new items to the end. This + # results in a breadth-first-like traversal for objects discovered at the same "depth" + # from different parent branches. Children of the current 'obj' will be processed after + # 'obj's current siblings in the queue. + queue.extend(children_to_add_to_queue_next) + + return final_contents_list def survey(callback=False): """survey(): starts survey mode, where you can click edges and faces to get their lengths or area. diff --git a/src/Mod/BIM/Resources/ui/preferences-ifc-export.ui b/src/Mod/BIM/Resources/ui/preferences-ifc-export.ui index 50be02aa3f..255eb65040 100644 --- a/src/Mod/BIM/Resources/ui/preferences-ifc-export.ui +++ b/src/Mod/BIM/Resources/ui/preferences-ifc-export.ui @@ -17,7 +17,16 @@ 6 - + + 9 + + + 9 + + + 9 + + 9 @@ -398,22 +407,46 @@ However, at FreeCAD, we believe having a building should not be mandatory, and t - + - In FreeCAD, it is possible to nest groups inside buildings or storeys. If this option is disabled, FreeCAD groups will be saved as IfcGroups and aggregated to the building structure. Aggregating non-building elements such as IfcGroups is however not recommended by the IFC standards. It is therefore also possible to export these groups as IfcElementAssemblies, which produces an IFC-compliant file. However, at FreeCAD, we believe nesting groups inside structures should be possible, and this option is there to have a chance to demonstrate our point of view. + If not checked, standard FreeCAD groups (App::DocumentObjectGroup) will not be exported as IfcGroup or IfcElementAssembly.\nTheir children will be re-parented to the container of the skipped group in the IFC structure. - - Export nested groups as assemblies + + Export FreeCAD groups - + true + + false + - IfcGroupsAsAssemblies + IfcExportStdGroups Mod/Arch + + + + + In FreeCAD, it is possible to nest groups inside buildings or storeys. If this option is disabled, FreeCAD groups will be saved as IfcGroups and aggregated to the building structure. Aggregating non-building elements such as IfcGroups is however not recommended by the IFC standards. It is therefore also possible to export these groups as IfcElementAssemblies, which produces an IFC-compliant file. However, at FreeCAD, we believe nesting groups inside structures should be possible, and this option is there to have a chance to demonstrate our point of view. + + + Export nested groups as assemblies + + + true + + + IfcGroupsAsAssemblies + + + Mod/Arch + + + + diff --git a/src/Mod/BIM/importers/exportIFC.py b/src/Mod/BIM/importers/exportIFC.py index 43740f8805..54746cce72 100644 --- a/src/Mod/BIM/importers/exportIFC.py +++ b/src/Mod/BIM/importers/exportIFC.py @@ -109,6 +109,57 @@ ENDSEC; END-ISO-10303-21; """ +def _prepare_export_list_skipping_std_groups(initial_export_list, preferences_dict): + """ + Builds the list of objects for IFC export. This function is called when the preference to skip + standard groups is active. Standard FreeCAD groups (App::DocumentObjectGroup that would become + IfcGroup) are omitted from the returned list, and their children are processed. This includes + children from their .Group property and also architecturally hosted elements (like windows in + walls) if a child of the skipped group is a host. + + The re-parenting of children of a skipped FreeCAD group in the resulting IFC file is achieved + implicitly: + + 1. The skipped FreeCAD group itself is not converted into an IFC product. It will not exist as + an IfcGroup or IfcElementAssembly in the IFC file. + 2. Children of the skipped group (and architecturally hosted elements like windows within walls + that were part of the skipped group's content) are processed and converted into their + respective IFC products. + 3. These IFC products, initially "orphaned" from the skipped FreeCAD group's potential IFC + representation, are then handled by the exporter's subsequent spatial relationship logic. + This logic typically assigns such "untreated" elements to the current or default IFC spatial + container (e.g., an IfcBuildingStorey if the skipped group was under a Level). + + The net effect is that the children appear directly contained within the IFC representation of + the skipped group's parent container. + """ + all_potential_objects = Arch.get_architectural_contents( + initial_export_list, + recursive=True, + discover_hosted_elements=True, + include_components_from_additions=True, + include_initial_objects_in_result=True + ) + + final_objects_for_processing = [] + + for obj in all_potential_objects: + is_std_group_to_skip = False + # Determine if the current object is a standard FreeCAD group that should be skipped + if obj.isDerivedFrom("App::DocumentObjectGroup"): + # Check its potential IFC type; only skip if it would become a generic IfcGroup + potential_ifc_type = getIfcTypeFromObj(obj) + if potential_ifc_type == "IfcGroup": + is_std_group_to_skip = True + + if not is_std_group_to_skip: + if obj not in final_objects_for_processing: # Ensure uniqueness + final_objects_for_processing.append(obj) + elif preferences_dict['DEBUG']: + print(f"DEBUG: IFC Exporter: StdGroup '{obj.Label}' ({obj.Name}) " + "was identified by get_architectural_contents but is now being filtered out.") + + return final_objects_for_processing def getPreferences(): """Retrieve the IFC preferences available in import and export.""" @@ -162,6 +213,7 @@ def getPreferences(): 'GET_STANDARD': params.get_param_arch("getStandardType"), 'EXPORT_MODEL': ['arch', 'struct', 'hybrid'][params.get_param_arch("ifcExportModel")], 'GROUPS_AS_ASSEMBLIES': params.get_param_arch("IfcGroupsAsAssemblies"), + 'IGNORE_STD_GROUPS': not params.get_param_arch("IfcExportStdGroups"), } # get ifcopenshell version @@ -274,8 +326,13 @@ def export(exportList, filename, colors=None, preferences=None): else: # IFC4 allows one to not write any history history = None - objectslist = Draft.get_group_contents(exportList, walls=True, - addgroups=True) + + if preferences['IGNORE_STD_GROUPS']: + if preferences['DEBUG']: + print("IFC Export: Skipping standard FreeCAD groups and processing their children.") + objectslist = _prepare_export_list_skipping_std_groups(exportList, preferences) + else: + objectslist = Draft.get_group_contents(exportList, walls=True, addgroups=True) # separate 2D and special objects. Special objects provide their own IFC export method