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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user