BIM: expose full active-schema IfcClass list for native IFC objects (#28994)

* BIM: expose full active-schema IfcClass list for native IFC objects

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Ayaan Ahmad
2026-04-08 22:44:26 +05:30
committed by GitHub
parent 918e335ef3
commit 2537a81a28
2 changed files with 108 additions and 2 deletions
+70
View File
@@ -44,6 +44,7 @@ from . import ifc_layers
from . import ifc_psets
from . import ifc_objects
from . import ifc_generator
from . import ifc_types
IFC_FILE_PATH = None # downloaded IFC file path
FCSTD_FILE_PATH = None # saved FreeCAD file
@@ -90,6 +91,18 @@ def compare(file1, file2):
return res
def get_schema_descendant_names(schema_name, root_name):
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name)
declaration = schema.declaration_by_name(root_name)
descendants = []
stack = list(declaration.subtypes())
while stack:
current = stack.pop()
descendants.append(current.name())
stack.extend(current.subtypes())
return sorted(set(descendants))
class NativeIFCTest(unittest.TestCase):
def setUp(self):
@@ -105,6 +118,13 @@ class NativeIFCTest(unittest.TestCase):
FreeCAD.closeDocument("IfcTest")
pass
def assertClassEnumMatchesFamily(self, obj, root_name):
ifcfile = ifc_tools.get_ifcfile(obj)
schema_name = ifcfile.wrapped_data.schema_name()
expected = get_schema_descendant_names(schema_name, root_name)
actual = sorted(obj.getEnumerationsOfProperty("Class"))
self.assertEqual(actual, expected)
def test01_ImportCoinSingle(self):
FreeCAD.Console.PrintMessage("NativeIFC 01: Importing single object, coin mode...")
clearObjects()
@@ -225,6 +245,56 @@ class NativeIFCTest(unittest.TestCase):
FreeCAD.getDocument("IfcTest").recompute()
self.assertTrue(obj.StepId != oldid, "ChangeIFCSchema failed")
def test08b_ClassListUsesActiveSchema(self):
FreeCAD.Console.PrintMessage("NativeIFC 08b: IFC class list uses full schema...")
clearObjects()
fp = getIfcFilePath()
ifc_import.insert(
fp,
"IfcTest",
strategy=2,
shapemode=0,
switchwb=0,
silent=True,
singledoc=SINGLEDOC,
)
wall = next(
o
for o in FreeCAD.getDocument("IfcTest").Objects
if getattr(o, "IfcClass", "") in ("IfcWall", "IfcWallStandardCase")
)
self.assertClassEnumMatchesFamily(wall, "IfcProduct")
def test08c_IFC2X3TypeClassListUsesTypeFamily(self):
FreeCAD.Console.PrintMessage("NativeIFC 08c: IFC2X3 type class list uses full schema...")
clearObjects()
doc = FreeCAD.ActiveDocument
proj = ifc_tools.create_document(doc, silent=True)
proj.Proxy.silent = True
proj.Schema = "IFC2X3"
doc.recompute()
site = ifc_tools.aggregate(Arch.makeSite(), proj)
building = ifc_tools.aggregate(Arch.makeBuilding(), site)
storey = ifc_tools.aggregate(Arch.makeFloor(), building)
wall = Arch.makeWall(None, 200, 400, 20)
wall = ifc_tools.aggregate(wall, storey)
doc.recompute()
self.assertTrue(ifc_types.is_typable(wall), "IFC2X3 wall should be typable")
ask_again = PARAMS.GetBool("ConvertTypeAskAgain", True)
keep_original = PARAMS.GetBool("ConvertTypeKeepOriginal", False)
try:
PARAMS.SetBool("ConvertTypeAskAgain", False)
PARAMS.SetBool("ConvertTypeKeepOriginal", True)
ifc_types.convert_to_type(wall, keep_object=True)
doc.recompute()
finally:
PARAMS.SetBool("ConvertTypeAskAgain", ask_again)
PARAMS.SetBool("ConvertTypeKeepOriginal", keep_original)
self.assertTrue(
getattr(wall, "Type", None), "Wall type conversion did not create a type object"
)
self.assertClassEnumMatchesFamily(wall.Type, "IfcTypeProduct")
def test09_CreateBIMObjects(self):
FreeCAD.Console.PrintMessage("NativeIFC 09: Creating BIM objects...")
doc = FreeCAD.ActiveDocument
+38 -2
View File
@@ -846,8 +846,36 @@ def remove_unused_properties(obj):
obj.removeProperty(prop)
def _iter_schema_subtypes(declaration):
"""Yield all descendants of a schema declaration."""
for subtype in declaration.subtypes():
yield subtype
yield from _iter_schema_subtypes(subtype)
def _inherits_from(declaration, ancestor_name):
"""Tell if a declaration inherits from a given ancestor."""
current = declaration
while current:
if current.name() == ancestor_name:
return True
current = current.supertype()
return False
def _get_class_family_root(schema, declaration):
"""Return the broadest reassignable family root for a declaration."""
for root_name in ("IfcTypeProduct", "IfcProduct", "IfcGroup"):
if _inherits_from(declaration, root_name):
return schema.declaration_by_name(root_name)
return None
def get_ifc_classes(obj, baseclass):
"""Returns a list of sibling classes from a given FreeCAD object"""
"""Returns the active-schema IFC classes that can reclassify this object."""
# this function can become pure IFC
@@ -859,7 +887,15 @@ def get_ifc_classes(obj, baseclass):
classes = []
schema = ifcfile.wrapped_data.schema_name()
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema)
declaration = schema.declaration_by_name(baseclass)
try:
declaration = schema.declaration_by_name(baseclass)
except RuntimeError:
return [baseclass]
family_root = _get_class_family_root(schema, declaration)
if family_root:
classes = {sub.name() for sub in _iter_schema_subtypes(family_root)}
classes.add(baseclass)
return sorted(classes)
if "StandardCase" in baseclass:
declaration = declaration.supertype()
if declaration.supertype():