# IfcOpenShell - IFC toolkit and geometry engine # Copyright (C) 2025 Thomas Krijnen # # 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 import ifcopenshell.api.aggregate import ifcopenshell.api.alignment import ifcopenshell.api.geometry import ifcopenshell.api.nest import ifcopenshell.guid import ifcopenshell.util.element import ifcopenshell.util.representation from ifcopenshell import entity_instance from ifcopenshell.api.alignment._add_zero_length_segment import _add_zero_length_segment def _move_vertical_layout_to_child_alignment( file: ifcopenshell.file, parent_alignment: entity_instance, vertical_layout: entity_instance ): """ Creates a new child alignment and aggregates it to the parent alignment. Moves the vertical alignment from the parent alignment to the child alignment. Also moves the "Axis/Curve3D" representation to the child alignment, if present. This function supports the transition of vertical alignment between CT 4.1.4.4.1.1 and 4.1.4.4.1.2 because a subsequent vertical alignment is being added and the Alignment Layout - Reusing Horizontal Layout concept applies. """ # unhook the vertical layout from the parent alignment ifcopenshell.api.nest.unassign_object(file, related_objects=[vertical_layout]) # create the child alignment child_alignment = file.createIfcAlignment( GlobalId=ifcopenshell.guid.new(), Name=f"Child of {parent_alignment.Name}" ) # nest the vertical layout onto the child alignment ifcopenshell.api.nest.assign_object(file, related_objects=[vertical_layout], relating_object=child_alignment) # aggregate the child alignment to the parent alignment ifcopenshell.api.aggregate.assign_object(file, products=[child_alignment], relating_object=parent_alignment) # move all referents positioning segments of the vertical layout to the referent nest of the child alignment child_referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, child_alignment) parent_referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, parent_alignment) for referent in parent_referent_nest.RelatedObjects: for product in referent.Positions[0].RelatedProducts: if product.is_a("IfcAlignmentSegment") and product.Nests[0].RelatingObject == vertical_layout: # ifcopenshell.api.nest.change_nest(file,referent,child_alignment) - this doesn't work because referent is assigned to child_alignment.IsNestedBy[0].RelatedObjects # and it needs to be assigned to child_alignment.IsNestedBy[1].RelatedObjects # move the referent manually - unassign it and add it to the child alignment's referent nest ifcopenshell.api.nest.unassign_object(file, [referent]) child_referent_nest.RelatedObjects += (referent,) # if the parent alignment has a representation, move the Axis/Curve3D represention to the child alignment base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment) if base_curve: representations = ifcopenshell.util.representation.get_representations_iter(parent_alignment) for representation in representations: if representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve3D": ifcopenshell.api.geometry.unassign_representation(file, parent_alignment, representation) ifcopenshell.api.geometry.assign_representation(file, child_alignment, representation) child_alignment.ObjectPlacement = parent_alignment.ObjectPlacement break def add_vertical_layout(file: ifcopenshell.file, parent_alignment: entity_instance) -> entity_instance: """ Adds a vertical layout to a previously created alignment. If this is the first vertical layout assigned to the parent_alignment the IFC CT 4.1.4.4.1.1 Alignment Layout - Horizontal, Vertical and Cant is followed. If this is the second or subsequent vertical layout assigned to the parent_alignment the IFC CT 4.1.4.4.1.2 Alignment Layout - Reusing Horizontal Layout is followed. When the second vertical layout is added, the structure of the IFC model must transition from one concept template to the other. Specifically, the following occurs: 1) The first child IfcAlignment is created and is IfcRelAggregates with the parent alignment. 2) The first vertical layout is unassigned from the IfcRelNests of the parent alignment and is IfcRelNests to the new child alignment. 3) A second child IfcAlignment is created and it is IfcRelAggregates with the parent alignment. 4) The vertical layout is IfcRelNests to the second child alignment For the third and subsequent vertical layouts, a new child alignment is created and aggregated to the parent alignment. A zero segment length terminated IfcGradientCurve is created for the new vertical layout :param parent_alignment: The parent alignment :return: The new vertical layout, including the manditory zero length segment """ vertical_layout = file.createIfcAlignmentVertical(GlobalId=ifcopenshell.guid.new()) # get all the child alignments under alignment child_alignments = [ c for c in ifcopenshell.util.element.get_decomposition(parent_alignment) if c.is_a("IfcAlignment") ] # Get all the IfcAlignmentVertical that are nesting alignment (there should be 0 or 1) # if 0, alignment is just horizontal and we are adding the first vertical so it will nest to the alignment, # or there are multiple vertical and they nest to the aggregated child alignments # if 1, there is one vertical alignments. Move it to a child alignment vertical_layouts_nesting_alignment = [ c for c in ifcopenshell.util.element.get_components(parent_alignment) if c.is_a("IfcAlignmentVertical") ] # move the vertical layout to a child alignment because there is going to be more than one vertical assert len(vertical_layouts_nesting_alignment) == 0 or len(vertical_layouts_nesting_alignment) == 1 for vertical_layout_nesting_alignment in vertical_layouts_nesting_alignment: _move_vertical_layout_to_child_alignment(file, parent_alignment, vertical_layout_nesting_alignment) if len(child_alignments) == 0 and len(vertical_layouts_nesting_alignment) == 0: # this is the first vertical layout so nest it into the parent alignment (IFC CT 4.1.4.4.1.1) ifcopenshell.api.nest.assign_object(file, related_objects=[vertical_layout], relating_object=parent_alignment) base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment) # the parent alignment has a Representation so create a representation for the vertical gradient_curve = file.createIfcGradientCurve( Segments=[], SelfIntersect=False, BaseCurve=base_curve, EndPoint=None ) # Per IFC CT 4.1.7.1.1.1, the shape representation for Horizontal geometry only is # RepresentationIdentifier="Axis" and RepresentationType="Curve2D". # However, per IFC CT 4.1.7.1.1.2 and 3 the shape represenation with Horizontal, Vertical and Cant # is RepresentationIdentifier="FootPrint" and RepresentationType="Curve2D" for the horizontal and # RepresentationIdentifier="Axis" and RepresentationType="Curve3D" for the 2.5D curve. # Since the alignment is transitioning from horizontal only to horizontal+vertical, the # RepresentationIdentifier must change from "Axis" to "FootPrint" representations = ifcopenshell.util.representation.get_representations_iter(parent_alignment) for representation in representations: if representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve2D": representation.RepresentationIdentifier = "FootPrint" break # create the Axis,Curve3D representation axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file) axis3d_shape_representation = file.createIfcShapeRepresentation( ContextOfItems=axis_geom_subcontext, RepresentationIdentifier="Axis", RepresentationType="Curve3D", Items=(gradient_curve,), ) ifcopenshell.api.geometry.assign_representation(file, parent_alignment, axis3d_shape_representation) else: # there are multiple vertical reusing the horizontal (IFC CT 4.1.4.4.1.2) # this is the second or subsequent vertical reusing the horizontal # create a new child alignment for the new vertical child_alignment = file.createIfcAlignment( GlobalId=ifcopenshell.guid.new(), OwnerHistory=None, Name=f"Child of {parent_alignment.Name}", Description=None, ObjectType=None, ObjectPlacement=None, Representation=None, PredefinedType=None, ) # Aggregate the child alignment to the parent alignment ifcopenshell.api.aggregate.assign_object(file, (child_alignment,), parent_alignment) # nest the vertical under the child alignment ifcopenshell.api.nest.assign_object(file, related_objects=[vertical_layout], relating_object=child_alignment) child_alignment.ObjectPlacement = parent_alignment.ObjectPlacement # the parent alignment has a Representation so create a representation for the vertical base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment) gradient_curve = file.createIfcGradientCurve( Segments=[], SelfIntersect=False, BaseCurve=base_curve, EndPoint=None ) axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file) # create the Curve3D representation axis3d_shape_representation = file.createIfcShapeRepresentation( ContextOfItems=axis_geom_subcontext, RepresentationIdentifier="Axis", RepresentationType="Curve3D", Items=(gradient_curve,), ) # add the representation to the child alignment ifcopenshell.api.geometry.assign_representation(file, child_alignment, axis3d_shape_representation) # All alignment layouts must end with a zero length segment. Their geometric representations must also end with a zero length segment. # Now that all the geometry is setup, add the zero length segment to the layout, which also adds a zero length segment to the representation _add_zero_length_segment(file, vertical_layout) return vertical_layout