diff --git a/src/ifcpatch/ifcpatch/recipes/FixDoubleTypedPredefinedType.py b/src/ifcpatch/ifcpatch/recipes/FixDoubleTypedPredefinedType.py new file mode 100644 index 0000000000..0cf03446af --- /dev/null +++ b/src/ifcpatch/ifcpatch/recipes/FixDoubleTypedPredefinedType.py @@ -0,0 +1,101 @@ +# IfcPatch - IFC patching utiliy +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of IfcPatch. +# +# IfcPatch is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcPatch is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcPatch. If not, see . + +# This file was generated with the assistance of an AI coding tool. + +from logging import Logger +from typing import Union + +import ifcopenshell +import ifcopenshell.util.element +import ifcopenshell.util.selector + +import ifcpatch + + +class Patcher(ifcpatch.BasePatcher): + def __init__(self, file: ifcopenshell.file, logger: Union[Logger, None] = None, query: str = ""): + """Clear an occurrence's PredefinedType/ObjectType when its type is already concrete + + IFC forbids "double typing": if an IfcTypeObject's own PredefinedType + is a concrete value (i.e. not NOTDEFINED), the PredefinedType and + ObjectType of its typed occurrences must be left empty, since the + type already carries that information (buildingSMART rule OJT001). + + Legacy files, or files typed before their type's PredefinedType was + set, commonly carry a stray PredefinedType (often NOTDEFINED) or + ObjectType on the occurrence anyway. This recipe finds every + occurrence typed by a type with a concrete PredefinedType and clears + the occurrence's own PredefinedType and ObjectType attributes, + without touching untyped occurrences or occurrences of a type that is + itself NOTDEFINED (as those may still carry meaningful data of their + own). + + The "is the type concrete" check mirrors + ifcopenshell.api.type.assign_type and + ifcopenshell.api.attribute.edit_attributes: a type whose + PredefinedType is USERDEFINED only counts as concrete if it also has + a custom type name (e.g. ElementType) to fall back on. This keeps the + recipe non-destructive: it will never remove the only description an + occurrence has. + + :param query: A query to select the subset of IFC elements to + restrict the patch to, optional. If not provided, patch will be + applied to all typed occurrences in the model. See + ifcopenshell.util.selector for query syntax. + + Example: + + .. code:: python + + # Fix all double-typed occurrences in the model. + ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "FixDoubleTypedPredefinedType"}) + + # Only fix double-typed walls. + ifcpatch.execute({ + "input": "input.ifc", + "file": model, + "recipe": "FixDoubleTypedPredefinedType", + "arguments": ["IfcWall"], + }) + """ + super().__init__(file, logger) + self.query = query + + def patch(self): + occurrences = None + if self.query: + occurrences = ifcopenshell.util.selector.filter_elements(self.file, self.query) + + cleared = 0 + for rel in self.file.by_type("IfcRelDefinesByType"): + predefined_type = ifcopenshell.util.element.get_predefined_type(rel.RelatingType) + if predefined_type is None or predefined_type == "NOTDEFINED": + continue + for occurrence in rel.RelatedObjects: + if occurrences is not None and occurrence not in occurrences: + continue + has_predefined_type = hasattr(occurrence, "PredefinedType") + if not occurrence.ObjectType and (not has_predefined_type or not occurrence.PredefinedType): + continue # Already clean. + occurrence.ObjectType = None + if has_predefined_type: + occurrence.PredefinedType = None + cleared += 1 + + self.logger.info(f"Cleared PredefinedType/ObjectType on {cleared} double-typed occurrence(s).") diff --git a/src/ifcpatch/test/test_FixDoubleTypedPredefinedType.py b/src/ifcpatch/test/test_FixDoubleTypedPredefinedType.py new file mode 100644 index 0000000000..6da47a8aff --- /dev/null +++ b/src/ifcpatch/test/test_FixDoubleTypedPredefinedType.py @@ -0,0 +1,187 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2026 Petru Conduraru +# +# This file is part of IfcOpenShell. +# +# IfcOpenShell is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcOpenShell is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcOpenShell. If not, see . + +# This file was generated with the assistance of an AI coding tool. + +import ifcopenshell.api.root +import ifcopenshell.api.type + +import ifcpatch +import test.bootstrap + + +class TestFixDoubleTypedPredefinedType(test.bootstrap.IFC4): + def test_clears_occurrence_under_concrete_type(self): + wall_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType", name="WT1") + wall_type.PredefinedType = "MOVABLE" + wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + wall.PredefinedType = "NOTDEFINED" + wall.ObjectType = "Some legacy value" + ifcopenshell.api.type.assign_type( + self.file, related_objects=[wall], relating_type=wall_type, should_map_representations=False + ) + # assign_type would already clear these; force them back to simulate + # a legacy file where the occurrence was typed before this cleanup + # existed. + wall.PredefinedType = "NOTDEFINED" + wall.ObjectType = "Some legacy value" + + output = ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": []}) + + assert wall.PredefinedType is None + assert wall.ObjectType is None + assert output == self.file + + def test_leaves_occurrence_under_notdefined_type_untouched(self): + wall_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType", name="WT1") + wall_type.PredefinedType = "NOTDEFINED" + wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + wall.PredefinedType = "MOVABLE" + wall.ObjectType = "Legitimate own value" + ifcopenshell.api.type.assign_type( + self.file, related_objects=[wall], relating_type=wall_type, should_map_representations=False + ) + wall.PredefinedType = "MOVABLE" + wall.ObjectType = "Legitimate own value" + + ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": []}) + + assert wall.PredefinedType == "MOVABLE" + assert wall.ObjectType == "Legitimate own value" + + def test_leaves_untyped_element_untouched(self): + wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + wall.PredefinedType = "NOTDEFINED" + wall.ObjectType = "Untyped value" + + ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": []}) + + assert wall.PredefinedType == "NOTDEFINED" + assert wall.ObjectType == "Untyped value" + + def test_userdefined_type_with_custom_description_clears_occurrence(self): + # Mirrors ifcopenshell.api.type.assign_type: a USERDEFINED type only + # counts as "concrete" if it also has a custom type name to fall + # back on (here IfcWallType.ElementType). + wall_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType", name="WT1") + wall_type.PredefinedType = "USERDEFINED" + wall_type.ElementType = "MyCustomWall" + wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + wall.PredefinedType = "NOTDEFINED" + wall.ObjectType = "Some legacy value" + ifcopenshell.api.type.assign_type( + self.file, related_objects=[wall], relating_type=wall_type, should_map_representations=False + ) + wall.PredefinedType = "NOTDEFINED" + wall.ObjectType = "Some legacy value" + + ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": []}) + + assert wall.PredefinedType is None + assert wall.ObjectType is None + + def test_userdefined_type_without_custom_description_untouched(self): + # A USERDEFINED type with no ElementType/ObjectType of its own + # carries no real type information, so assign_type leaves the + # occurrence alone. This recipe mirrors that. + wall_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType", name="WT1") + wall_type.PredefinedType = "USERDEFINED" + wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + wall.PredefinedType = "NOTDEFINED" + wall.ObjectType = "Kept because type has no description" + ifcopenshell.api.type.assign_type( + self.file, related_objects=[wall], relating_type=wall_type, should_map_representations=False + ) + wall.PredefinedType = "NOTDEFINED" + wall.ObjectType = "Kept because type has no description" + + ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": []}) + + assert wall.PredefinedType == "NOTDEFINED" + assert wall.ObjectType == "Kept because type has no description" + + def test_already_clean_file_is_a_no_op(self): + wall_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType", name="WT1") + wall_type.PredefinedType = "MOVABLE" + wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + ifcopenshell.api.type.assign_type( + self.file, related_objects=[wall], relating_type=wall_type, should_map_representations=False + ) + assert wall.PredefinedType is None + assert wall.ObjectType is None + before = self.file.to_string() + + ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": []}) + + assert self.file.to_string() == before + + def test_query_restricts_which_occurrences_are_touched(self): + wall_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType", name="WT1") + wall_type.PredefinedType = "MOVABLE" + wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + wall.PredefinedType = "NOTDEFINED" + column_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcColumnType", name="CT1") + column_type.PredefinedType = "COLUMN" + column = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcColumn") + column.PredefinedType = "NOTDEFINED" + ifcopenshell.api.type.assign_type( + self.file, related_objects=[wall], relating_type=wall_type, should_map_representations=False + ) + ifcopenshell.api.type.assign_type( + self.file, related_objects=[column], relating_type=column_type, should_map_representations=False + ) + wall.PredefinedType = "NOTDEFINED" + column.PredefinedType = "NOTDEFINED" + + ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": ["IfcWall"]}) + + assert wall.PredefinedType is None + assert column.PredefinedType == "NOTDEFINED" + + +class TestFixDoubleTypedPredefinedTypeIFC2X3(test.bootstrap.IFC2X3): + def test_ifc2x3_occurrence_without_predefined_type_attribute_is_guarded(self): + # IFC2X3 IfcWallStandardCase has no PredefinedType attribute at all; + # only ObjectType may be cleared there. + wall_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType", name="WT1") + wall_type.PredefinedType = "STANDARD" + wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallStandardCase") + wall.ObjectType = "Some legacy value" + assert not hasattr(wall, "PredefinedType") + ifcopenshell.api.type.assign_type( + self.file, related_objects=[wall], relating_type=wall_type, should_map_representations=False + ) + wall.ObjectType = "Some legacy value" + + ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": []}) + + assert wall.ObjectType is None + + def test_ifc2x3_userdefined_type_without_elementtype_untouched(self): + wall_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType", name="WT1") + wall_type.PredefinedType = "USERDEFINED" + wall = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallStandardCase") + wall.ObjectType = "Kept because type has no description" + ifcopenshell.api.type.assign_type( + self.file, related_objects=[wall], relating_type=wall_type, should_map_representations=False + ) + wall.ObjectType = "Kept because type has no description" + + ifcpatch.execute({"file": self.file, "recipe": "FixDoubleTypedPredefinedType", "arguments": []}) + + assert wall.ObjectType == "Kept because type has no description"