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.
This commit is contained in:
Bruno Postle
2026-03-21 07:49:54 +00:00
parent b45cbefdd9
commit b52a23cb93
3 changed files with 226 additions and 0 deletions
@@ -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",
@@ -0,0 +1,82 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2026 Dion Moult <dion@thinkmoult.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/>.
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
@@ -0,0 +1,142 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2026 Dion Moult <dion@thinkmoult.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/>.
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