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