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
This commit is contained in:
Furgo
2025-06-03 11:19:07 +02:00
committed by GitHub
parent 9a80921d74
commit 94a4639bc3
4 changed files with 236 additions and 9 deletions

2
.gitignore vendored
View File

@@ -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

View File

@@ -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.

File diff suppressed because one or more lines are too long

View File

@@ -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