From b52a23cb939ec95cfbd485ad880afb35c314b95c Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Sat, 21 Mar 2026 07:49:54 +0000 Subject: [PATCH] Add geometry.copy_representation API Deep-copies the named representation from a source element to a target element. Generated with the assistance of an AI coding tool. --- .../ifcopenshell/api/geometry/__init__.py | 2 + .../api/geometry/copy_representation.py | 82 ++++++++++ .../api/geometry/test_copy_representation.py | 142 ++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/geometry/copy_representation.py create mode 100644 src/ifcopenshell-python/test/api/geometry/test_copy_representation.py diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py index 5ce20c3803..a8a5ca609b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py @@ -52,6 +52,7 @@ from .disconnect_element import disconnect_element from .disconnect_path import disconnect_path from .edit_object_placement import edit_object_placement from .map_representation import map_representation +from .copy_representation import copy_representation from .regenerate_wall_representation import regenerate_wall_representation from .remove_boolean import remove_boolean from .remove_representation import remove_representation @@ -65,6 +66,7 @@ __all__ = [ "add_boolean", "clip_solid", "clip_solid_bounded", + "copy_representation", "add_door_representation", "add_footprint_representation", "add_mesh_representation", diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/copy_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/copy_representation.py new file mode 100644 index 0000000000..21843ab22a --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/copy_representation.py @@ -0,0 +1,82 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2026 Dion Moult +# +# 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 . + +from __future__ import annotations + +from typing import Optional + +import ifcopenshell.api.geometry +import ifcopenshell.util.element +import ifcopenshell.util.representation + + +def copy_representation( + file: ifcopenshell.file, + source: ifcopenshell.entity_instance, + target: ifcopenshell.entity_instance, + context_identifier: str = "Body", +) -> Optional[ifcopenshell.entity_instance]: + """Copy a geometric representation from one element to another. + + Finds the named representation on ``source``, deep-copies its entity + graph (geometry items, profiles, placements, etc.), and assigns the copy + to ``target``. Representation contexts are shared rather than copied. + If ``target`` already has a matching representation it is removed and + replaced. + + If no matching representation is found on ``source``, returns ``None`` + and leaves ``target`` unchanged. + + :param source: The element to copy the representation from. + :param target: The element to assign the copied representation to. + :param context_identifier: The RepresentationIdentifier to look up on + ``source`` (e.g. ``"Body"``, ``"Axis"``, ``"Box"``). + Defaults to ``"Body"``. + :return: The newly created IfcShapeRepresentation, or None if no + matching representation was found on ``source``. + + Example: + + .. code:: python + + wall_a = model.by_id(1) + wall_b = model.by_id(2) + + # Give wall_b the same body geometry as wall_a. + ifcopenshell.api.geometry.copy_representation(model, + source=wall_a, target=wall_b) + """ + source_rep = ifcopenshell.util.representation.get_representation( + source, "Model", context_identifier + ) + if source_rep is None: + return None + + new_rep = ifcopenshell.util.element.copy_deep( + file, source_rep, exclude=["IfcGeometricRepresentationContext"] + ) + + existing_rep = ifcopenshell.util.representation.get_representation( + target, "Model", context_identifier + ) + if existing_rep: + ifcopenshell.api.geometry.unassign_representation(file, product=target, representation=existing_rep) + ifcopenshell.api.geometry.remove_representation(file, representation=existing_rep) + + ifcopenshell.api.geometry.assign_representation(file, product=target, representation=new_rep) + return new_rep diff --git a/src/ifcopenshell-python/test/api/geometry/test_copy_representation.py b/src/ifcopenshell-python/test/api/geometry/test_copy_representation.py new file mode 100644 index 0000000000..2f7e81b4af --- /dev/null +++ b/src/ifcopenshell-python/test/api/geometry/test_copy_representation.py @@ -0,0 +1,142 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2026 Dion Moult +# +# 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 . + +import ifcopenshell.api.geometry +import ifcopenshell.api.root +import ifcopenshell.util.representation +import ifcopenshell.util.shape_builder +import test.bootstrap + + +class TestCopyRepresentation(test.bootstrap.IFC4): + def _body_context(self): + body = ifcopenshell.util.representation.get_context(self.file, "Model", "Body", "MODEL_VIEW") + if body is None: + model = self.file.createIfcGeometricRepresentationContext( + ContextType="Model", CoordinateSpaceDimension=3, Precision=1e-5, + WorldCoordinateSystem=self.file.createIfcAxis2Placement3D( + self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)) + ), + ) + body = self.file.createIfcGeometricRepresentationSubContext( + ContextIdentifier="Body", ContextType="Model", + TargetView="MODEL_VIEW", ParentContext=model, + ) + return body + + def _add_body_rep(self, element): + body = self._body_context() + rep = ifcopenshell.api.geometry.add_wall_representation( + self.file, context=body, length=5.0, height=3.0, thickness=0.2 + ) + ifcopenshell.api.geometry.assign_representation(self.file, product=element, representation=rep) + return rep + + def test_copy_to_empty_target(self): + wall_a = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + wall_b = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + self._add_body_rep(wall_a) + + result = ifcopenshell.api.geometry.copy_representation( + self.file, source=wall_a, target=wall_b + ) + + assert result is not None + assert result.is_a("IfcShapeRepresentation") + target_rep = ifcopenshell.util.representation.get_representation(wall_b, "Model", "Body") + assert target_rep is not None + assert target_rep == result + + def test_source_rep_entities_are_distinct(self): + wall_a = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + wall_b = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + source_rep = self._add_body_rep(wall_a) + + new_rep = ifcopenshell.api.geometry.copy_representation( + self.file, source=wall_a, target=wall_b + ) + + assert new_rep.id() != source_rep.id() + assert new_rep.Items[0].id() != source_rep.Items[0].id() + + def test_context_is_shared_not_copied(self): + wall_a = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + wall_b = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + source_rep = self._add_body_rep(wall_a) + + new_rep = ifcopenshell.api.geometry.copy_representation( + self.file, source=wall_a, target=wall_b + ) + + assert new_rep.ContextOfItems.id() == source_rep.ContextOfItems.id() + + def test_replaces_existing_target_rep(self): + wall_a = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + wall_b = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + self._add_body_rep(wall_a) + old_rep = self._add_body_rep(wall_b) + old_rep_id = old_rep.id() + + ifcopenshell.api.geometry.copy_representation( + self.file, source=wall_a, target=wall_b + ) + + try: + self.file.by_id(old_rep_id) + assert False, "old representation still exists" + except RuntimeError: + pass # entity was removed, as expected + + def test_source_unchanged_after_copy(self): + wall_a = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + wall_b = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + source_rep = self._add_body_rep(wall_a) + source_rep_id = source_rep.id() + + ifcopenshell.api.geometry.copy_representation( + self.file, source=wall_a, target=wall_b + ) + + assert self.file.by_id(source_rep_id) is not None # source must still exist + assert ifcopenshell.util.representation.get_representation(wall_a, "Model", "Body") is not None + + def test_returns_none_when_no_matching_rep(self): + wall_a = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + wall_b = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + + result = ifcopenshell.api.geometry.copy_representation( + self.file, source=wall_a, target=wall_b + ) + + assert result is None + + def test_custom_context_identifier(self): + wall_a = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + wall_b = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + self._add_body_rep(wall_a) + + # "Axis" doesn't exist on wall_a, so should return None + result = ifcopenshell.api.geometry.copy_representation( + self.file, source=wall_a, target=wall_b, context_identifier="Axis" + ) + + assert result is None + + +class TestCopyRepresentationIFC2X3(test.bootstrap.IFC2X3, TestCopyRepresentation): + pass