2025-07-20 15:32:26 -07:00
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.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/>.
2025-12-19 18:53:04 +05:00
import math
import numpy as np
2025-07-20 15:32:26 -07:00
import ifcopenshell
import ifcopenshell . api . alignment
import ifcopenshell . api . nest
2025-09-15 09:52:30 +05:00
import ifcopenshell . geom
2025-07-20 15:32:26 -07:00
import ifcopenshell . ifcopenshell_wrapper as wrapper
2025-12-19 18:53:04 +05:00
import ifcopenshell . util . alignment
2025-09-15 09:52:30 +05:00
import ifcopenshell . util . unit
2025-12-19 18:53:04 +05:00
from ifcopenshell import entity_instance
from ifcopenshell . api . alignment . _get_segment_start_point_label import (
_get_segment_start_point_label ,
)
from ifcopenshell . api . alignment . _map_alignment_horizontal_segment import (
_map_alignment_horizontal_segment ,
)
from ifcopenshell . api . alignment . _map_alignment_vertical_segment import (
_map_alignment_vertical_segment ,
)
from ifcopenshell . api . alignment . _update_curve_segment_transition_code import (
_update_curve_segment_transition_code ,
)
2025-07-20 15:32:26 -07:00
2025-07-28 11:45:12 +05:00
def add_zero_length_segment ( file : ifcopenshell . file , layout : entity_instance , include_referent : bool = True ) - > bool :
2025-07-20 15:32:26 -07:00
"""
Adds a zero length segment to the end of a layout.
If the layout already has a zero length segment, nothing is changed.
:param layout: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant, IfcCompositeCurve, IfcGradientCurve, IfcSegmentedReferenceCurve
:param include_referent: If True, an IfcReferent representing the ending point of the layout is included for IfcLinearElement layouts (i.e. business logic)
:return: True if segment is added
"""
2025-07-28 11:45:12 +05:00
2025-07-25 17:31:50 -07:00
# These are valid curve types for alignment, but don't have the zero-length segment
if layout . is_a ( " IfcOffsetCurveByDistances " ) or layout . is_a ( " IfcPolyline " ) or layout . is_a ( " IfcIndexedPolyCurve " ) :
return
2025-07-28 11:45:12 +05:00
expected_types = [
" IfcAlignmentHorizontal " ,
" IfcAlignmentVertical " ,
" IfcAlignmentCant " ,
" IfcCompositeCurve " ,
" IfcGradientCurve " ,
" IfcSegmentedReferenceCurve " ,
]
2025-07-20 15:32:26 -07:00
if not layout . is_a ( ) in expected_types :
raise TypeError (
f " Expected layout type to be one of { [ _ for _ in expected_types ] } , instead received { layout . is_a ( ) } "
)
if ifcopenshell . api . alignment . has_zero_length_segment ( layout ) :
return False
2025-07-28 11:45:12 +05:00
2025-07-20 15:32:26 -07:00
if layout . is_a ( " IfcCompositeCurve " ) or layout . is_a ( " IfcGradientCurve " ) or layout . is_a ( " IfcSegmentedReferenceCurve " ) :
2025-07-28 11:45:12 +05:00
x = 0.0
y = 0.0
dx = 1.0
dy = 0.0
segment_start = 0.0
2025-07-20 15:32:26 -07:00
2025-07-23 16:31:42 -07:00
last_segment = None
2025-07-20 15:32:26 -07:00
if layout . Segments and 0 < len ( layout . Segments ) :
# If there are segments, get the last segment and compute the end point and tangent direction
# because this becomes of placement of the zero length segment
last_segment = layout . Segments [ - 1 ]
settings = ifcopenshell . geom . settings ( )
2025-07-28 11:45:12 +05:00
fn = wrapper . map_shape ( settings , last_segment . wrapped_data )
eval = wrapper . function_item_evaluator ( settings , fn )
2025-07-20 15:32:26 -07:00
e = np . array ( eval . evaluate ( fn . end ( ) ) )
unit_scale = ifcopenshell . util . unit . calculate_unit_scale ( file )
2025-07-28 11:45:12 +05:00
e [ : 3 , 3 ] / = unit_scale
x = float ( e [ 0 , 3 ] )
y = float ( e [ 1 , 3 ] )
dx = float ( e [ 0 , 0 ] )
dy = float ( e [ 1 , 0 ] )
2025-07-20 15:32:26 -07:00
parent_curve = file . createIfcLine (
Pnt = file . createIfcCartesianPoint ( Coordinates = ( ( 0.0 , 0.0 ) ) ) ,
Dir = file . createIfcVector (
Orientation = file . createIfcDirection ( DirectionRatios = ( ( 1.0 , 0.0 ) ) ) ,
Magnitude = 1.0 ,
) ,
)
2025-07-23 16:31:42 -07:00
zero_length_curve_segment = file . createIfcCurveSegment (
2025-07-20 15:32:26 -07:00
Transition = " DISCONTINUOUS " ,
Placement = file . createIfcAxis2Placement2D (
Location = file . createIfcCartesianPoint ( ( x , y ) ) ,
RefDirection = file . createIfcDirection ( ( dx , dy ) ) ,
) ,
2025-07-21 16:24:20 -07:00
SegmentStart = file . createIfcLengthMeasure ( 0.0 ) ,
2025-07-20 15:32:26 -07:00
SegmentLength = file . createIfcLengthMeasure ( 0.0 ) ,
ParentCurve = parent_curve ,
)
2025-07-23 16:31:42 -07:00
layout . Segments + = ( zero_length_curve_segment , )
if last_segment :
2025-07-28 11:45:12 +05:00
_update_curve_segment_transition_code ( last_segment , zero_length_curve_segment )
2025-07-20 15:32:26 -07:00
# add zero length segments to base curves
if layout . is_a ( " IfcSegmentedReferenceCurve " ) :
2025-07-28 11:45:12 +05:00
ifcopenshell . api . alignment . add_zero_length_segment ( file , layout . BaseCurve )
2025-07-20 15:32:26 -07:00
elif layout . is_a ( " IfcGradientCurve " ) :
2026-03-12 16:56:33 -06:00
if layout . BaseCurve is not None :
ifcopenshell . api . alignment . add_zero_length_segment ( file , layout . BaseCurve )
2025-07-20 15:32:26 -07:00
else :
2025-07-23 16:31:42 -07:00
zero_length_curve_segment = None
2025-07-20 15:32:26 -07:00
if layout . is_a ( " IfcAlignmentHorizontal " ) :
2025-07-28 11:45:12 +05:00
x = 0.0
y = 0.0
dx = 1.0
dy = 0.0
2025-07-21 16:24:20 -07:00
last_segment = None
for rel in layout . IsNestedBy :
if 0 < len ( rel . RelatedObjects ) :
last_segment = rel . RelatedObjects [ - 1 ]
break
if last_segment :
2025-07-28 11:45:12 +05:00
file . begin_transaction ( ) # use a transaction so we can discard any temporary IFC entities created
2025-07-21 16:24:20 -07:00
settings = ifcopenshell . geom . settings ( )
2025-07-28 11:45:12 +05:00
mapped_segments = _map_alignment_horizontal_segment ( file , last_segment )
2025-07-21 16:24:20 -07:00
geometry_segment = mapped_segments [ 0 ] if mapped_segments [ 1 ] == None else mapped_segments [ 1 ]
2025-07-28 11:45:12 +05:00
fn = wrapper . map_shape ( settings , geometry_segment . wrapped_data )
eval = wrapper . function_item_evaluator ( settings , fn )
2025-07-21 16:24:20 -07:00
e = np . array ( eval . evaluate ( fn . end ( ) ) )
unit_scale = ifcopenshell . util . unit . calculate_unit_scale ( file )
2025-07-28 11:45:12 +05:00
x = float ( e [ 0 , 3 ] ) / unit_scale
y = float ( e [ 1 , 3 ] ) / unit_scale
dx = float ( e [ 0 , 0 ] )
dy = float ( e [ 1 , 0 ] )
2025-07-21 16:24:20 -07:00
file . discard_transaction ( )
2025-07-28 11:45:12 +05:00
angle_unit_scale = ifcopenshell . util . unit . calculate_unit_scale ( file , " PLANEANGLEUNIT " )
2025-07-20 15:32:26 -07:00
design_parameters = file . createIfcAlignmentHorizontalSegment (
2025-07-28 11:45:12 +05:00
StartPoint = file . createIfcCartesianPoint ( ( x , y ) ) ,
StartDirection = math . atan2 ( dy , dx ) / angle_unit_scale ,
2025-07-20 15:32:26 -07:00
StartRadiusOfCurvature = 0.0 ,
EndRadiusOfCurvature = 0.0 ,
SegmentLength = 0.0 ,
PredefinedType = " LINE " ,
)
2025-07-28 11:45:12 +05:00
zero_length_curve_segment = file . createIfcAlignmentSegment (
GlobalId = ifcopenshell . guid . new ( ) , DesignParameters = design_parameters
)
2025-07-20 15:32:26 -07:00
elif layout . is_a ( " IfcAlignmentVertical " ) :
last_segment_dist_along = 0.0
2025-08-14 13:56:36 -07:00
last_segment_height = 0.0
2025-07-20 15:32:26 -07:00
last_segment_end_gradient = 0.0
2025-08-14 13:56:36 -07:00
last_segment = None
2025-07-20 15:32:26 -07:00
for rel in layout . IsNestedBy :
if 0 < len ( rel . RelatedObjects ) :
2025-07-21 16:24:20 -07:00
last_segment = rel . RelatedObjects [ - 1 ]
break
2025-07-20 15:32:26 -07:00
2025-08-14 13:56:36 -07:00
if last_segment :
file . begin_transaction ( )
last_segment_dist_along = (
last_segment . DesignParameters . StartDistAlong + last_segment . DesignParameters . HorizontalLength
)
last_segment_end_gradient = last_segment . DesignParameters . EndGradient
settings = ifcopenshell . geom . settings ( )
mapped_segments = _map_alignment_vertical_segment ( file , last_segment )
geometry_segment = mapped_segments [ 0 ] if mapped_segments [ 1 ] == None else mapped_segments [ 1 ]
fn = wrapper . map_shape ( settings , geometry_segment . wrapped_data )
eval = wrapper . function_item_evaluator ( settings , fn )
e = np . array ( eval . evaluate ( fn . end ( ) ) )
unit_scale = ifcopenshell . util . unit . calculate_unit_scale ( file )
last_segment_height = float ( e [ 1 , 3 ] ) / unit_scale
file . discard_transaction ( )
2025-07-20 15:32:26 -07:00
design_parameters = file . createIfcAlignmentVerticalSegment (
StartDistAlong = last_segment_dist_along ,
HorizontalLength = 0.0 ,
2025-08-14 13:56:36 -07:00
StartHeight = last_segment_height ,
2025-07-20 15:32:26 -07:00
StartGradient = last_segment_end_gradient ,
EndGradient = last_segment_end_gradient ,
PredefinedType = " CONSTANTGRADIENT " ,
)
2025-07-28 11:45:12 +05:00
zero_length_curve_segment = file . createIfcAlignmentSegment (
GlobalId = ifcopenshell . guid . new ( ) , DesignParameters = design_parameters
)
2025-07-20 15:32:26 -07:00
elif layout . is_a ( " IfcAlignmentCant " ) :
last_segment_dist_along = 0.0
last_segment_cant_left = 0.0
last_segment_cant_right = 0.0
for rel in layout . IsNestedBy :
if 0 < len ( rel . RelatedObjects ) :
2025-07-21 16:24:20 -07:00
last_segment = rel . RelatedObjects [ - 1 ]
2025-07-20 15:32:26 -07:00
last_segment_dist_along = (
last_segment . DesignParameters . StartDistAlong + last_segment . DesignParameters . HorizontalLength
)
last_segment_cant_left = (
last_segment . DesignParameters . EndCantLeft
if last_segment . DesignParameters . EndCantLeft != None
else last_segment . DesignParameters . StartCantLeft
)
last_segment_cant_right = (
last_segment . DesignParameters . EndCantRight
if last_segment . DesignParameters . EndCantRight != None
else last_segment . DesignParameters . StartCantRight
)
2025-07-21 16:24:20 -07:00
break
2025-07-20 15:32:26 -07:00
design_parameters = file . createIfcAlignmentCantSegment (
StartDistAlong = last_segment_dist_along ,
HorizontalLength = 0.0 ,
StartCantLeft = last_segment_cant_left ,
StartCantRight = last_segment_cant_right ,
PredefinedType = " CONSTANTCANT " ,
)
2025-07-28 11:45:12 +05:00
zero_length_curve_segment = file . createIfcAlignmentSegment (
GlobalId = ifcopenshell . guid . new ( ) , DesignParameters = design_parameters
)
2025-07-20 15:32:26 -07:00
2025-07-23 16:31:42 -07:00
ifcopenshell . api . nest . assign_object ( file , related_objects = [ zero_length_curve_segment ] , relating_object = layout )
2025-07-20 15:32:26 -07:00
if include_referent :
alignment = ifcopenshell . api . alignment . get_alignment ( layout )
2025-08-29 12:48:03 -07:00
station = ifcopenshell . api . alignment . get_alignment_start_station ( file , alignment )
2025-07-25 17:31:50 -07:00
name = f " { _get_segment_start_point_label ( zero_length_curve_segment , None ) } ( { ifcopenshell . util . alignment . station_as_string ( file , station ) } ) "
2025-08-29 14:37:17 -07:00
referent = ifcopenshell . api . alignment . add_stationing_referent (
file , alignment , 0.0 , station , name , zero_length_curve_segment
)
2025-08-29 12:48:03 -07:00
referent . Description = f " Positions zero length segment { zero_length_curve_segment . id ( ) } "
2025-07-28 11:45:12 +05:00
return True