mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 07:51:47 +00:00
ifcpatch: add FixDoubleTypedPredefinedType recipe for OJT001 cleanup
Existing IFC files (Revit exports, or Bonsai models typed before a type's PredefinedType was set) commonly carry an explicit PredefinedType/ObjectType on occurrences whose type already has a concrete PredefinedType, violating buildingSMART rule OJT001. PR #8829 stops this happening going forward at the authoring layer; this recipe is the one-click cleanup for files that already have the problem. The "is the type concrete" check mirrors ifcopenshell.api.type.assign_type and ifcopenshell.api.attribute.edit_attributes exactly, including the USERDEFINED nuance (only counts as concrete if the type also carries a custom description to fall back on), so behaviour stays consistent between authoring and cleanup. Bonsai's IFC Patch UI discovers recipes by globbing ifcpatch/recipes/*.py, so no Bonsai UI changes are needed. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -0,0 +1,101 @@
|
|||||||
|
# IfcPatch - IFC patching utiliy
|
||||||
|
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# 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).")
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
# IfcOpenShell - IFC toolkit and geometry engine
|
||||||
|
# Copyright (C) 2026 Petru Conduraru <petru@bimvoice.com>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# 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"
|
||||||
Reference in New Issue
Block a user