diff --git a/src/bonsai/bonsai/bim/__init__.py b/src/bonsai/bonsai/bim/__init__.py index 5985459e2a..930e067c24 100644 --- a/src/bonsai/bonsai/bim/__init__.py +++ b/src/bonsai/bonsai/bim/__init__.py @@ -83,6 +83,7 @@ modules = { "covering": None, "web": None, "light": None, + "alignment": None, # Uncomment this line to enable loading of the demo module. Happy hacking! # The name "demo" must correlate to a folder name in `bim/module/`. # "demo": None, diff --git a/src/bonsai/bonsai/bim/module/alignment/__init__.py b/src/bonsai/bonsai/bim/module/alignment/__init__.py new file mode 100644 index 0000000000..3c49e0b4fe --- /dev/null +++ b/src/bonsai/bonsai/bim/module/alignment/__init__.py @@ -0,0 +1,36 @@ +# Bonsai - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of Bonsai. +# +# Bonsai is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Bonsai 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Bonsai. If not, see . + +import bpy + +# from . import ui, prop, operator +from . import operator + +classes = (operator.ImportAlignmentCSV,) + + +def menu_func_import(self, context): + self.layout.operator(operator.ImportAlignmentCSV.bl_idname, text="Alignment (.csv)") + + +def register(): + bpy.types.TOPBAR_MT_file_import.append(menu_func_import) + + +def unregister(): + bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) diff --git a/src/bonsai/bonsai/bim/module/alignment/operator.py b/src/bonsai/bonsai/bim/module/alignment/operator.py new file mode 100644 index 0000000000..1fcbdd52c8 --- /dev/null +++ b/src/bonsai/bonsai/bim/module/alignment/operator.py @@ -0,0 +1,113 @@ +# Bonsai - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult , 2022 Yassine Oualid +# +# This file is part of Bonsai. +# +# Bonsai is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Bonsai 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Bonsai. If not, see . + +# pyright: reportUnnecessaryTypeIgnoreComment=error + +import os + +import ifcopenshell.api.alignment +import ifcopenshell.api.alignment.add_stationing_to_alignment + +import bpy +import json +import time +import calendar +import isodate +import bonsai.core.sequence as core +import bonsai.tool as tool +import bonsai.bim.module.sequence.helper as helper +import ifcopenshell.util.sequence +import ifcopenshell.util.selector +from datetime import datetime +from dateutil import parser, relativedelta +from bpy_extras.io_utils import ImportHelper +from typing import get_args, TYPE_CHECKING +from typing_extensions import assert_never + + +class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): + bl_idname = "bim.import_alignment_csv" + bl_label = "Import Alignment CSV" + bl_options = {"REGISTER", "UNDO"} + filename_ext = ".csv" + filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"}) + + @classmethod + def poll(cls, context): + ifc_file = tool.Ifc.get() + if ifc_file is None: + cls.poll_message_set("No IFC file is loaded.") + return False + elif ifc_file.schema != "IFC4X3": + cls.poll_message_set("Schema must be IFC4x3.") + return False + return True + + def _execute(self, context): + import ifcopenshell.api.alignment + + self.file = tool.Ifc.get() + start = time.time() + alignment = ifcopenshell.api.alignment.create_alignment_from_csv(self.file, self.filepath) + ifcopenshell.api.alignment.create_geometric_representation(self.file, alignment) + ifcopenshell.api.alignment.add_stationing_to_alignment(self.file, alignment=alignment, start_station=0.0) + + # IFC 4.1.5.1 alignments cannot be contained in spatial structures, but can be referenced into them + sites = self.file.by_type("IfcSite") + for site in sites: + ifcopenshell.api.spatial.reference_structure(self.file, products=[alignment], relating_structure=site) + + # process the generated IfcReferent for the alignment + for rel in alignment.IsNestedBy: + for referent in rel.RelatedObjects: + if referent.is_a("IfcReferent"): + referent_obj = bpy.data.objects.new(tool.Loader.get_name(referent), None) + tool.Geometry.link(referent, referent_obj) + tool.Collector.assign(referent_obj, should_clean_users_collection=False) + + # an alignment can be an aggregation of multiple child alignments (ie. multiple verticals for a single horizontal) + # get all the alignment curves + curves = [] + for rel in alignment.IsDecomposedBy: + for agg in rel.RelatedObjects: + if agg.is_a("IfcAlignment"): + curves.append(ifcopenshell.api.alignment.get_curve(agg)) # 3D curve + + # if there aren't any curves from aggregation, then there is only a single vertical or no vertical + if len(curves) == 0: + curves.append(ifcopenshell.api.alignment.get_curve(alignment)) + + settings = ifcopenshell.geom.settings() + for curve in curves: + shape = ifcopenshell.geom.create_shape(settings, curve) + + # create a new Blender mesh + mesh_name = tool.Loader.get_mesh_name_from_shape(shape) + mesh = bpy.data.meshes.new(mesh_name) + m = tool.Loader.convert_geometry_to_mesh(shape, mesh) + + # create a new Blender object + alignment_obj = bpy.data.objects.new(tool.Loader.get_name(alignment), m) + + # link the blender object to with the alignment element + tool.Geometry.link(alignment, alignment_obj) + + # assign the object to the blender collections + tool.Collector.assign(alignment_obj, should_clean_users_collection=False) + + self.report({"INFO"}, "Imported in %s seconds" % (time.time() - start)) diff --git a/src/bonsai/bonsai/bim/module/sequence/operator.py b/src/bonsai/bonsai/bim/module/sequence/operator.py index 8e75097993..b7730f2b93 100644 --- a/src/bonsai/bonsai/bim/module/sequence/operator.py +++ b/src/bonsai/bonsai/bim/module/sequence/operator.py @@ -19,6 +19,7 @@ # pyright: reportUnnecessaryTypeIgnoreComment=error import os + import bpy import json import time @@ -653,7 +654,6 @@ class DisableEditingWorkCalendar(bpy.types.Operator): core.disable_editing_work_calendar(tool.Sequence) return {"FINISHED"} - class ImportCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.import_csv" bl_label = "Import CSV" diff --git a/src/bonsai/docs/guides/alignment.rst b/src/bonsai/docs/guides/alignment.rst new file mode 100644 index 0000000000..279fd11437 --- /dev/null +++ b/src/bonsai/docs/guides/alignment.rst @@ -0,0 +1,37 @@ +Road and Rail Alignments +======================== + +.. Note:: + + Bonsai lacks modeling features for road and rail alignments. This feature is intended to be a stop-gap measure to allow alignments + to be defined and imported into an IFC model. This feature is most likely temporary and will be phased out as robust alignment + modeling capabilities are developed. + +Alignments may be defined by the PI method in a CSV file for import into an IFC4X3 Bonsai project. The format of the CSV file is as follows: + +.. csv-table:: Alignment by PI Method + + "X1","Y1","R1","X2","Y2","R2","...,","Xn-1","Yn-1","Rn-1","Xn","Yn","Rn" + "D1","Z1","L1","D2","Z2","L2","...,","Dn-1","Zn-1","Ln-1","Dn","Zn","Ln" + "D1","Z1","L1","D2","Z2","L2","...,","Dn-1","Zn-1","Ln-1","Dn","Zn","Ln" + + +where: + Xi,Yi are horizontal alignment PI points + Ri are horizontal curve radii. + Di,Zi are vertical alignment PI points as Distance_Along,Elevation + Li are the horizontal length of parabolic vertical transition curves + +R1 and Rn, as well as L1 and Ln, are placeholder values and should be set to 0.0 + +The CSV file must contain exactly one horizontal alignment definition with a minimum of three points. +X1,Y1 is the Point of Beginning (POB). Xn,Yn is the Point of Ending (POE). + +The CSV file may contain zero, one or more vertical alignment definitions. + +Alignments with a single horizontal layout and zero or one vertical layout are modeled per `IFC Concept Template 4.1.4.4.1.1, Alignment Layout - Horizontal, Vertical, and Cant, `_. Alignments with multiple vertical layouts are modeled per `IFC Concept Template 4.1.4.4.1.2, Alignment Layout - Reusing Horizontal Layout, `_. + +Example based on the `FHWA Bridge Geometry Manual `_: + +500,2500,0.0,3340,660,1000,4340,5000,1250,7600,4560,950,8480,2010,0 +0,100,0,2000,135,1600,5000,105,1200,7400,153,2000,9800,105,800,12800,90,0 \ No newline at end of file diff --git a/src/bonsai/docs/index.rst b/src/bonsai/docs/index.rst index f9a8c490ef..664a619e72 100644 --- a/src/bonsai/docs/index.rst +++ b/src/bonsai/docs/index.rst @@ -56,6 +56,7 @@ and data-rich OpenBIM with Blender :) guides/authoring/georeferencing guides/authoring/git_support guides/development/index + guides/alignment guides/authoring/other_addons guides/troubleshooting guides/debugging diff --git a/src/bonsai/docs/reference/topbar.rst b/src/bonsai/docs/reference/topbar.rst index 555c413757..8744e55809 100644 --- a/src/bonsai/docs/reference/topbar.rst +++ b/src/bonsai/docs/reference/topbar.rst @@ -98,3 +98,4 @@ Imports data from external sources into the Blender session or IFC model. - **P6 (.xer)**: Imports a P6 XER file containing a work schedule into the active IFC model. - **Powerproject (.pp)**: Imports a Powerproject file containing a work schedule into the active IFC model. - **Microsoft Project (.xml)**: Imports a Microsoft Project XML file containing a work schedule into the active IFC model. +- **Alignment (.csv)**: Imports a CSV containing horizontal and vertical alignments defined by the PI method into the active IFC model. \ No newline at end of file diff --git a/src/ifcgeom/ConversionSettings.h b/src/ifcgeom/ConversionSettings.h index ed172cad69..8902a8de26 100644 --- a/src/ifcgeom/ConversionSettings.h +++ b/src/ifcgeom/ConversionSettings.h @@ -365,6 +365,12 @@ namespace ifcopenshell { static constexpr bool defaultvalue = false; }; + struct ComputeCurvature : public SettingBase { + static constexpr const char* const name = "compute-curvature"; + static constexpr const char* const description = "Specifies whether function_item_evaluator.evaluate() computes curvature."; + static constexpr bool defaultvalue = false; + }; + enum FunctionStepMethod { MAXSTEPSIZE, MINSTEPS }; @@ -504,7 +510,7 @@ namespace ifcopenshell { }; class IFC_GEOM_API Settings : public SettingsContainer< - std::tuple + std::tuple > {}; } diff --git a/src/ifcgeom/function_item_evaluator.cpp b/src/ifcgeom/function_item_evaluator.cpp index 1d5c0d186a..b4dade6ab8 100644 --- a/src/ifcgeom/function_item_evaluator.cpp +++ b/src/ifcgeom/function_item_evaluator.cpp @@ -5,12 +5,21 @@ using namespace ifcopenshell::geometry; -double ifcopenshell::geometry::polynomial_length(double A, double B, double C, double horizontal_length) { - auto fn = [A, B, C](double x) -> double { return sqrt(pow(B + 2 * C * x, 2.0) + 1.0); }; - auto l = boost::math::quadrature::trapezoidal(fn, 0.0, horizontal_length); - return l; -} +std::vector ifcopenshell::geometry::helmert_curve_point(double A0, double A1, double A2, double s) { + auto theta = [A0, A1, A2](double t) -> double { + auto a0 = A0 ? t / A0 : 0.0; + auto a1 = A1 ? A1 * std::pow(t, 2) / (2 * fabs(std::pow(A1, 3))) : 0.0; + auto a2 = A2 ? std::pow(t, 3) / (3 * std::pow(A2, 3)) : 0.0; + return a0 + a1 + a2; + }; + auto fn_x = [theta](double t) -> double { return cos(theta(t)); }; + auto fn_y = [theta](double t) -> double { return sin(theta(t)); }; + auto x = boost::math::quadrature::trapezoidal(fn_x, 0.0, s); + auto y = boost::math::quadrature::trapezoidal(fn_y, 0.0, s); + auto angle = theta(x); + return {x, y, angle}; +} struct functor_fn_evaluator : public fn_evaluator { functor_fn_evaluator(taxonomy::functor_item::const_ptr fn, const ifcopenshell::geometry::Settings& settings) : fn_evaluator(settings), @@ -97,12 +106,27 @@ struct gradient_fn_evaluator : public fn_evaluator { auto xy = horizontal_evaluator_.evaluate(u + start_); auto uz = vertical_evaluator_.evaluate(u); - uz.col(3)(0) = 0.0; // x is distance along. zero it out so it doesn't add to the x from horizontal + // curvature is stored in row 3 - capture it and remove it from the xy and uz matrices + // so the matrix operations (ie multiplication) works correct.y + auto horizontal_curvature = xy.row(3); + xy.row(3) = Eigen::Vector4d(0, 0, 0, 1); + + auto vertical_curvature = uz.row(3); + uz.row(3) = Eigen::Vector4d(0, 0, 0, 1); + + uz(0, 3) = 0.0; // x is distance along. zero it out so it doesn't add to the x from horizontal uz.col(1).swap(uz.col(2)); // uz is 2D in distance along - y plane, swap y and z so elevations become z uz.row(1).swap(uz.row(2)); Eigen::Matrix4d m; m = xy * uz; // combine horizontal and vertical + + // Put curvature back into the solution matrix + // curvature for vertical is in column 0, need it to be in column 1 + // so it doesn't add to curvature for horizontal + std::swap(vertical_curvature(3, 0), vertical_curvature(3, 1)); + m.row(3) = horizontal_curvature + vertical_curvature; + return m; } @@ -129,6 +153,15 @@ struct cant_fn_evaluator : public fn_evaluator { auto g = gradient_evaluator_.evaluate(u + start_); auto c = cant_evaluator_.evaluate(u); + + // curvature is stored in row 3 - capture it and remove it from the xy and uz matrices + // so the matrix operations (ie multiplication) works correctly + auto gradient_curvature = g.row(3); + g.row(3) = Eigen::Vector4d(0, 0, 0, 1); + + auto cant_curvature = c.row(3); + c.row(3) = Eigen::Vector4d(0, 0, 0, 1); + // Need to multiply g and c so the axis vectors // from cant have the correct rotation applied so // they are relative to the gradient curve coordinate system @@ -155,6 +188,11 @@ struct cant_fn_evaluator : public fn_evaluator { m(1, 3) = y; m(2, 3) = z + s; + // reinstate values for curvature. + // cant_curvature is cant alone. this needs to be combined with gradient in column 3 + gradient_curvature[3] = gradient_curvature[2] + cant_curvature[3]; + m.row(3) = gradient_curvature; + return m; } @@ -274,5 +312,9 @@ taxonomy::item::ptr function_item_evaluator::evaluate(const std::vector& } Eigen::Matrix4d function_item_evaluator::evaluate(double u) const { - return fn_evaluator_->evaluate(u); + Eigen::Matrix4d m = fn_evaluator_->evaluate(u); + if (!fn_evaluator_->settings_.get().get()) { + m.row(3) = Eigen::Vector4d(0, 0, 0, 1); + } + return m; } diff --git a/src/ifcgeom/function_item_evaluator.h b/src/ifcgeom/function_item_evaluator.h index 3107b615dc..a936bfbe89 100644 --- a/src/ifcgeom/function_item_evaluator.h +++ b/src/ifcgeom/function_item_evaluator.h @@ -7,16 +7,9 @@ namespace ifcopenshell { namespace geometry { -/// @brief Computes the curve length of a polynomial of the form y = A + Bx + Cx^2 -/// This function is needed on the python side. To do this computation, a large library like scipy -/// is needed. That is too much overhead. For this reason, a simple function is here on the C++ side -/// that the python side can call -/// @param A constant term -/// @param B linear term -/// @param C quadradic term -/// @param horizontal_length length of the polynomal projected onto the horizontal axis -/// @return curve length -double polynomial_length(double A, double B, double C,double horizontal_length); +/// @brief Computes a point on a helmert curve at s. +/// Returns (x,y,theta) at L/2. The results are in a vector so they can be returned to python +std::vector helmert_curve_point(double A0, double A1, double A2, double s); /// @brief Abstract class for evaluating a function_item. This class is specialized for each of the function_item types. struct fn_evaluator { @@ -66,7 +59,7 @@ class function_item_evaluator { /// @brief evaluates the function at u /// @param u u is constrained to be between start_ and start_+length - /// @return 4x4 placement matrix + /// @return 4x4 placement matrix. Curvature values for horizontal, vertical, and vertical + cant are stored in the last row. Eigen::Matrix4d evaluate(double u) const; private: diff --git a/src/ifcgeom/mapping/IfcCurveSegment.cpp b/src/ifcgeom/mapping/IfcCurveSegment.cpp index c7e4ca1cfa..bfe8a191ca 100644 --- a/src/ifcgeom/mapping/IfcCurveSegment.cpp +++ b/src/ifcgeom/mapping/IfcCurveSegment.cpp @@ -100,18 +100,15 @@ typedef boost::mpl::vector< struct parent_curve_function { parent_curve_function() = default; parent_curve_function(const parent_curve_function&) = default; - parent_curve_function(std::function fn) : fn_(fn) { - } - - parent_curve_function& operator=(std::function fn) { - fn_ = fn; - return *this; + parent_curve_function(std::function fn, std::function cfn) : fn_(fn), cfn_(cfn) { } virtual Eigen::Matrix4d operator()(double u) const { return fn_(u); } + virtual Eigen::Matrix4d curvature(double u) const { return cfn_(u); } private: std::function fn_; + std::function cfn_; }; struct polynomial_parent_curve : public parent_curve_function { @@ -142,7 +139,7 @@ struct curve_segment_function { Eigen::Matrix4d operator()(double u) const { Eigen::Matrix4d parent_curve_point = (*parent_curve_fn_)(u); Eigen::Matrix4d curve_segment_point = curve_segment_placement_ * remove_parent_curve_rotation_ * remove_parent_curve_translation_ * parent_curve_point; - return curve_segment_point; + return curve_segment_point + parent_curve_fn_->curvature(u); } private: @@ -167,7 +164,7 @@ struct cant_curve_segment_function { Eigen::Matrix4d parent_curve_point = (*parent_curve_fn_)(u); Eigen::Matrix4d cant_increment = parent_curve_point - parent_curve_start_point_; Eigen::Matrix4d curve_segment_point = curve_segment_placement_ + cant_increment; - return curve_segment_point; + return curve_segment_point + parent_curve_fn_->curvature(u); } private: @@ -246,7 +243,7 @@ class curve_segment_evaluator { Logger::Error(std::runtime_error("multiple uses of IfcSegmentCurve not supported"), inst_); } - segment_type_ = is_horizontal ? ST_HORIZONTAL : is_vertical ? ST_VERTICAL : ST_CANT; + segment_type_ = is_horizontal ? ST_HORIZONTAL : is_vertical ? ST_VERTICAL : is_cant ? ST_CANT : ST_HORIZONTAL; start_ = translate_if_param_value(inst->ParentCurve(), inst->SegmentStart()) * length_unit; @@ -336,7 +333,7 @@ class curve_segment_evaluator { } } - void set_spiral_function(double s, std::function fnX, std::function fnY) { + void set_spiral_function(double s, std::function fnX, std::function fnY, std::function curvature) { if (segment_type_ == ST_HORIZONTAL || segment_type_ == ST_VERTICAL) { // start of trimmed curve double pcStartX = 0.0, pcStartY = 0.0; @@ -381,24 +378,32 @@ class curve_segment_evaluator { }; } - parent_curve_fn_ = std::make_shared([start=start_, s, convert_u, fnX, fnY](double u) { - u = convert_u(u+start); + parent_curve_fn_ = std::make_shared( + [start=start_, s, convert_u, fnX, fnY](double u)->Eigen::Matrix4d { + u = convert_u(u+start); - // integration limits, integrate from a to b - auto b = s ? u / s : 0.0; + // integration limits, integrate from a to b + auto b = s ? u / s : 0.0; - // point on parent curve - auto x = boost::math::quadrature::trapezoidal(fnX, 0.0, b); - auto y = boost::math::quadrature::trapezoidal(fnY, 0.0, b); - auto dx = s ? fnX(b) / s : 1.0; - auto dy = s ? fnY(b) / s : 0.0; + // point on parent curve + auto x = boost::math::quadrature::trapezoidal(fnX, 0.0, b); + auto y = boost::math::quadrature::trapezoidal(fnY, 0.0, b); + auto dx = s ? fnX(b) / s : 1.0; + auto dy = s ? fnY(b) / s : 0.0; - Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); - m.col(0) = Eigen::Vector4d(dx, dy, 0, 0); - m.col(1) = Eigen::Vector4d(-dy, dx, 0, 0); - m.col(3) = Eigen::Vector4d(x, y, 0, 1); - return m; - }); + Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); + m.col(0) = Eigen::Vector4d(dx, dy, 0, 0); + m.col(1) = Eigen::Vector4d(-dy, dx, 0, 0); + m.col(3) = Eigen::Vector4d(x, y, 0, 1); + return m; + }, + [start = start_, convert_u, curvature](double u) -> Eigen::Matrix4d { + u = convert_u(u + start); + Eigen::Matrix4d c = Eigen::Matrix4d::Zero(); + c(3, 0) = curvature(u); + return c; + } + ); if (segment_type_ == ST_VERTICAL) { // for vertical, the input curve length is measured along the spiral. @@ -428,10 +433,16 @@ class curve_segment_evaluator { } } else if (segment_type_ == ST_CANT) { Logger::Error(std::runtime_error("Unexpected segment type encountered - cant is handled in set_cant_spiral_function - should never get here")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); } + ); } else { Logger::Error(std::runtime_error("Unexpected segment type encountered")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); } + ); } } @@ -450,33 +461,40 @@ class curve_segment_evaluator { auto end_cant = Cant(/* start_ + */ length_); auto delta_cant = end_cant - start_cant; - parent_curve_fn_ = std::make_shared([start_angle,delta_angle,start_cant,delta_cant,Superelevation, SuperelevationSlope, Cant](double u) -> Eigen::Matrix4d { - // departure of the curve segment from the base curve (superelevation) - auto super_elevation = Superelevation(u); - auto slope = SuperelevationSlope(u); + parent_curve_fn_ = std::make_shared( + [start_angle,delta_angle,start_cant,delta_cant,Superelevation, SuperelevationSlope, Cant](double u) -> Eigen::Matrix4d { + // departure of the curve segment from the base curve (superelevation) + auto super_elevation = Superelevation(u); + auto slope = SuperelevationSlope(u); - // direction along curve segment - auto angle = atan(slope); - auto dx = cos(angle); - auto dy = sin(angle); - Eigen::Vector4d ref_dir(dx, dy, 0.0, 0.0); + // direction along curve segment + auto angle = atan(slope); + auto dx = cos(angle); + auto dy = sin(angle); + Eigen::Vector4d ref_dir(dx, dy, 0.0, 0.0); - // tilt angle in the plane of the cross section - auto cant = Cant(u); - auto tilt_angle = start_angle + delta_angle * (cant - start_cant) / delta_cant; - Eigen::Vector4d z(0.0, cos(tilt_angle), sin(tilt_angle), 0.0); + // tilt angle in the plane of the cross section + auto cant = Cant(u); + auto tilt_angle = start_angle + delta_angle * (cant - start_cant) / delta_cant; + Eigen::Vector4d z(0.0, cos(tilt_angle), sin(tilt_angle), 0.0); - // compute axis direction - Eigen::Vector4d y = z.cross3(ref_dir); - Eigen::Vector4d axis = ref_dir.cross3(y); + // compute axis direction + Eigen::Vector4d y = z.cross3(ref_dir); + Eigen::Vector4d axis = ref_dir.cross3(y); - Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); - m.col(0) = ref_dir; - m.col(1) = y; - m.col(2) = axis; - m.col(3) = Eigen::Vector4d(u, super_elevation, 0.0, 1.0); - return m; - }); + Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); + m.col(0) = ref_dir; + m.col(1) = y; + m.col(2) = axis; + m.col(3) = Eigen::Vector4d(u, super_elevation, 0.0, 1.0); + return m; + }, + [Cant](double u) -> Eigen::Matrix4d { + Eigen::Matrix4d c = Eigen::Matrix4d::Zero(); + c(3, 0) = Cant(u); + return c; + } + ); parent_curve_start_point_ = (*parent_curve_fn_)(0.0); } @@ -526,7 +544,8 @@ class curve_segment_evaluator { auto s = fabs(A * sqrt(PI)); // curve length when u = 1.0 auto fn_x = [A, s](double t) -> double { return A ? s * cos(PI * A * t * t / (2 * fabs(A))) : 0.0; }; auto fn_y = [A, s](double t) -> double { return A ? s * sin(PI * A * t * t / (2 * fabs(A))) : 0.0; }; - set_spiral_function(s, fn_x, fn_y); + auto curvature = [A](double t) -> double { return A ? A * t / fabs(A * A * A) : 0.0; }; + set_spiral_function(s, fn_x, fn_y, curvature); } } #endif @@ -548,8 +567,13 @@ class curve_segment_evaluator { }; auto fn_x = [theta](double t) -> double { return cos(theta(t)); }; auto fn_y = [theta](double t) -> double { return sin(theta(t)); }; + auto curvature = [constant_term, cosine_term, L](double t) -> double { + auto a0 = constant_term.has_value() ? L / constant_term.value() : 0.0; + auto a1 = (L / cosine_term) * cos((PI / L) * t); + return a0 + a1; + }; double s = 1.0; - set_spiral_function(s, fn_x, fn_y); + set_spiral_function(s, fn_x, fn_y, curvature); } else if (segment_type_ == ST_CANT) { boost::optional> super, slope; std::tie(super, slope) = get_superelevation_functions(); @@ -574,10 +598,16 @@ class curve_segment_evaluator { set_cant_spiral_function(*super, *slope, cant); } else if (segment_type_ == ST_VERTICAL) { Logger::Error(std::runtime_error("IfcCosineSpiral cannot be used for vertical alignment")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); } + ); } else { Logger::Error(std::runtime_error("Unexpected segment type encountered")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); } + ); } } #endif @@ -603,8 +633,14 @@ class curve_segment_evaluator { }; auto fn_x = [theta](double t) -> double { return cos(theta(t)); }; auto fn_y = [theta](double t) -> double { return sin(theta(t)); }; + auto curvature = [constant_term, linear_term, sine_term, L](double t) -> double { + auto a0 = constant_term.has_value() ? L / constant_term.value() : 0.0; + auto a1 = linear_term.has_value() ? sign(linear_term.value()) * pow(L / linear_term.value(), 2.0)*(t/L) : 0.0; + auto a2 = (L / sine_term) * sin(2 * PI * t / L); + return a0 + a1 + a2; + }; double s = 1.0; - set_spiral_function(s, fn_x, fn_y); + set_spiral_function(s, fn_x, fn_y, curvature); } else if (segment_type_ == ST_CANT) { boost::optional> super, slope; std::tie(super, slope) = get_superelevation_functions(); @@ -631,16 +667,20 @@ class curve_segment_evaluator { set_cant_spiral_function(*super, *slope, cant); } else if (segment_type_ == ST_VERTICAL) { Logger::Error(std::runtime_error("IfcSineSpiral cannot be used for vertical alignment")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); } else { Logger::Error(std::runtime_error("Unexpected segment type encountered")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); } } #endif void polynomial_spiral(boost::optional A0, boost::optional A1, boost::optional A2, boost::optional A3, boost::optional A4, boost::optional A5, boost::optional A6, boost::optional A7) { - auto theta = [A0, A1, A2, A3, A4, A5, A6, A7, start = start_ * length_unit_, lu = length_unit_](double t) { + auto theta = [A0, A1, A2, A3, A4, A5, A6, A7, start = start_ * length_unit_, lu = length_unit_](double t) -> double { auto a0 = A0.has_value() ? t / (A0.value() * lu) : 0.0; auto a1 = A1.has_value() ? A1.value() * lu * std::pow(t, 2) / (2 * fabs(std::pow(A1.value() * lu, 3))) : 0.0; auto a2 = A2.has_value() ? std::pow(t, 3) / (3 * std::pow(A2.value() * lu, 3)) : 0.0; @@ -655,15 +695,31 @@ class curve_segment_evaluator { auto fn_x = [theta](double t) -> double { return cos(theta(t)); }; auto fn_y = [theta](double t) -> double { return sin(theta(t)); }; + + // this is same as cant function in polynomial_cant_spiral + auto curvature = [A0, A1, A2, A3, A4, A5, A6, A7, start = start_, L = length_, lu = length_unit_, length = length_](double t) -> double { + t += start; + auto a0 = A0.has_value() ? 1 / (A0.value() * lu) : 0.0; + auto a1 = A1.has_value() ? A1.value() * lu * t / fabs(std::pow(A1.value() * lu, 3)) : 0.0; + auto a2 = A2.has_value() ? std::pow(t, 2) / std::pow(A2.value() * lu, 3) : 0.0; + auto a3 = A3.has_value() ? A3.value() * lu * std::pow(t, 3) / fabs(std::pow(A3.value() * lu, 5)) : 0.0; + auto a4 = A4.has_value() ? std::pow(t, 4) / std::pow(A4.value() * lu, 5) : 0.0; + auto a5 = A5.has_value() ? A5.value() * lu * std::pow(t, 5) / fabs(std::pow(A5.value() * lu, 7)) : 0.0; + auto a6 = A6.has_value() ? std::pow(t, 6) / std::pow(A6.value() * lu, 7) : 0.0; + auto a7 = A7.has_value() ? A7.value() * lu * std::pow(t, 7) / fabs(std::pow(A7.value() * lu, 9)) : 0.0; + return L * (a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7); + }; + + double s = 1.0; - set_spiral_function(s, fn_x, fn_y); + set_spiral_function(s, fn_x, fn_y, curvature); } void polynomial_cant_spiral(boost::optional A0, boost::optional A1, boost::optional A2, boost::optional A3, boost::optional A4, boost::optional A5, boost::optional A6, boost::optional A7) { boost::optional> super, slope; std::tie(super, slope) = get_superelevation_functions(); - auto cant = [A0, A1, A2, A3, A4, A5, A6, A7, start = start_, L = length_, lu = length_unit_, length = length_](double t) { + auto cant = [A0, A1, A2, A3, A4, A5, A6, A7, start = start_, L = length_, lu = length_unit_, length = length_](double t) -> double { t += start; auto a0 = A0.has_value() ? 1 / (A0.value() * lu) : 0.0; auto a1 = A1.has_value() ? A1.value() * lu * t / fabs(std::pow(A1.value() * lu, 3)) : 0.0; @@ -681,7 +737,7 @@ class curve_segment_evaluator { } if (!slope.has_value()) { - slope = [A1, A2, A3, A4, A5, A6, A7, start = start_, L = length_, lu = length_unit_, length = length_](double t) { + slope = [A1, A2, A3, A4, A5, A6, A7, start = start_, L = length_, lu = length_unit_, length = length_](double t) -> double { t += start; auto a1 = A1.has_value() ? A1.value() * lu / fabs(std::pow(A1.value() * lu, 3)) : 0.0; auto a2 = A2.has_value() ? 2 * t / std::pow(A2.value() * lu, 3) : 0.0; @@ -813,29 +869,36 @@ class curve_segment_evaluator { }; } - parent_curve_fn_ = std::make_shared([segment_type = segment_type_, R, pcCenterX, pcCenterY, start_angle, sign_l, convert_u](double u) { - u = convert_u(u); + parent_curve_fn_ = std::make_shared( + [segment_type = segment_type_, R, pcCenterX, pcCenterY, start_angle, sign_l, convert_u](double u)->Eigen::Matrix4d { + u = convert_u(u); - // u is measured along the circle - // angle from the X=0 axis to the current point - auto delta = R ? sign_l * u / R : 0.0; - auto sweep_angle = start_angle + delta; - auto cos_sweep_angle = cos(sweep_angle); - auto sin_sweep_angle = sin(sweep_angle); + // u is measured along the circle + // angle from the X=0 axis to the current point + auto delta = R ? sign_l * u / R : 0.0; + auto sweep_angle = start_angle + delta; + auto cos_sweep_angle = cos(sweep_angle); + auto sin_sweep_angle = sin(sweep_angle); - // point on the parent curve - auto pcX = R * cos_sweep_angle + pcCenterX; - auto pcY = R * sin_sweep_angle + pcCenterY; + // point on the parent curve + auto pcX = R * cos_sweep_angle + pcCenterX; + auto pcY = R * sin_sweep_angle + pcCenterY; - auto pcDx = -sign_l * sin_sweep_angle; - auto pcDy = sign_l * cos_sweep_angle; + auto pcDx = -sign_l * sin_sweep_angle; + auto pcDy = sign_l * cos_sweep_angle; - Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); - m.col(0) = Eigen::Vector4d(pcDx, pcDy, 0, 0); - m.col(1) = Eigen::Vector4d(-pcDy, pcDx, 0, 0); - m.col(3) = Eigen::Vector4d(pcX, pcY, 0.0, 1.0); - return m; - }); + Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); + m.col(0) = Eigen::Vector4d(pcDx, pcDy, 0, 0); + m.col(1) = Eigen::Vector4d(-pcDy, pcDx, 0, 0); + m.col(3) = Eigen::Vector4d(pcX, pcY, 0.0, 1.0); + return m; + }, + [R](double) -> Eigen::Matrix4d { + Eigen::Matrix4d c = Eigen::Matrix4d::Zero(); + c(3, 0) = 1 / R; + return c; + } + ); if (segment_type_ == ST_HORIZONTAL) { parent_curve_start_point_ = (*parent_curve_fn_)(start_); @@ -865,10 +928,14 @@ class curve_segment_evaluator { } else if (segment_type_ == ST_CANT) { Logger::Warning(std::runtime_error("Use of IfcCircle for cant is not supported")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); } else { Logger::Error(std::runtime_error("Unexpected segment type encountered")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); } } @@ -916,23 +983,32 @@ class curve_segment_evaluator { convert_u = [pcDx](double u) { return u/pcDx; }; } - parent_curve_fn_ = std::make_shared([pcX, pcY, pcDx, pcDy, convert_u](double u) { - u = convert_u(u); + parent_curve_fn_ = std::make_shared( + [pcX, pcY, pcDx, pcDy, convert_u](double u)->Eigen::Matrix4d { + u = convert_u(u); - auto x = pcX + pcDx * u; - auto y = pcY + pcDy * u; + auto x = pcX + pcDx * u; + auto y = pcY + pcDy * u; - Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); - m.col(0) = Eigen::Vector4d(pcDx, pcDy, 0, 0); - m.col(1) = Eigen::Vector4d(-pcDy, pcDx, 0, 0); - m.col(3) = Eigen::Vector4d(x, y, 0.0, 1.0); - return m; - }); + Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); + m.col(0) = Eigen::Vector4d(pcDx, pcDy, 0, 0); + m.col(1) = Eigen::Vector4d(-pcDy, pcDx, 0, 0); + m.col(3) = Eigen::Vector4d(x, y, 0.0, 1.0); + return m; + }, + [](double /*u*/) -> Eigen::Matrix4d { + // curvature is zero for a line. identity initializes c(3,0) = 0 + Eigen::Matrix4d c = Eigen::Matrix4d::Zero(); + return c; + } + ); parent_curve_start_point_ = (*parent_curve_fn_)(start_); } else { - Logger::Warning(std::runtime_error("Unexpected segment type encountered")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + Logger::Warning(std::runtime_error("Unexpected segment type encountered")); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); } } @@ -1022,50 +1098,62 @@ class curve_segment_evaluator { } // This functor evaluates the polynomial at a distance u along the curve - parent_curve_fn_ = std::make_shared([start = start_, lu = length_unit_, coeffX, coeffY, convert_u](double u)->Eigen::Matrix4d { - auto x = convert_u(u + start); // find x for u - // evaluate the polynomial at x - std::array*, 2> coefficients{&coeffX, &coeffY}; - std::array position{0.0, 0.0}; // = SUM(coeff*u^pos) - std::array slope{0.0, 0.0}; // slope is derivative of the curve = SUM( coeff*pos*u^(pos-1) ) - for (int i = 0; i < 2; i++) { // loop over X and Y - auto begin = coefficients[i]->cbegin(); - auto end = coefficients[i]->cend(); - for (auto iter = begin; iter != end; iter++) { - auto exp = std::distance(begin, iter); - auto coeff = (*iter); - position[i] += coeff * pow(lu, 1-exp) * pow(x, exp); + parent_curve_fn_ = std::make_shared( + [start = start_, lu = length_unit_, coeffX, coeffY, convert_u](double u)->Eigen::Matrix4d { + auto x = convert_u(u + start); // find x for u + // evaluate the polynomial at x + std::array*, 2> coefficients{&coeffX, &coeffY}; + std::array position{0.0, 0.0}; // = SUM(coeff*u^pos) + std::array slope{0.0, 0.0}; // slope is derivative of the curve = SUM( coeff*pos*u^(pos-1) ) + for (int i = 0; i < 2; i++) { // loop over X and Y + auto begin = coefficients[i]->cbegin(); + auto end = coefficients[i]->cend(); + for (auto iter = begin; iter != end; iter++) { + auto exp = std::distance(begin, iter); + auto coeff = (*iter); + position[i] += coeff * pow(lu, 1-exp) * pow(x, exp); - if (iter != begin) { - slope[i] += exp * coeff * pow(lu, 1-exp) * pow(x, exp - 1); - } - } + if (iter != begin) { + slope[i] += exp * coeff * pow(lu, 1-exp) * pow(x, exp - 1); + } + } + } + + auto X = position[0]; + auto Y = position[1]; + + auto Dx = slope[0]; + auto Dy = slope[1]; + + auto angle = atan2(Dy, Dx); + Dx = cos(angle); + Dy = sin(angle); + + Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); + m.col(0) = Eigen::Vector4d(Dx, Dy, 0, 0); + m.col(1) = Eigen::Vector4d(-Dy, Dx, 0, 0); + m.col(3) = Eigen::Vector4d(X, Y, 0.0, 1.0); + return m; + }, + [start = start_, lu = length_unit_, coeffX, coeffY, convert_u](double u) -> Eigen::Matrix4d { + auto x = convert_u(u + start); // find x for u + Eigen::Matrix4d c = Eigen::Matrix4d::Zero(); + c(3, 0) = coeffY[2]; // this may need a unit conversion (also assume there is only 3 coefficients) + return c; } - - auto X = position[0]; - auto Y = position[1]; - - auto Dx = slope[0]; - auto Dy = slope[1]; - - auto angle = atan2(Dy, Dx); - Dx = cos(angle); - Dy = sin(angle); - - Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); - m.col(0) = Eigen::Vector4d(Dx, Dy, 0, 0); - m.col(1) = Eigen::Vector4d(-Dy, Dx, 0, 0); - m.col(3) = Eigen::Vector4d(X, Y, 0.0, 1.0); - return m; - }); + ); parent_curve_start_point_ = (*parent_curve_fn_)(0.0); // start is added to u in parent_curve_fn_, so use 0.0 here } else if (segment_type_ == ST_CANT) { Logger::Warning(std::runtime_error("Use of IfcPolynomialCurve for cant is not supported")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); } else { Logger::Error(std::runtime_error("Unexpected segment type encountered")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); } } }; diff --git a/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp b/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp index df790d2738..e6d3afdfce 100644 --- a/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp +++ b/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp @@ -39,7 +39,12 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOffsetCurveByDistances* inst auto first_offset_value = *(offset_values->begin()); auto basis_curve = inst->BasisCurve(); - auto curve = taxonomy::dcast(map(basis_curve)); + auto curve = taxonomy::dcast(map(basis_curve)); + if (!curve) { + // Only implement on alignment curves + Logger::Warning("IfcOffsetCurveByDistances is only implemented for BasisCurves curves based on taxonomy::function_item", inst); + return nullptr; + } double start = curve->start(); double basis_curve_length = curve->length(); diff --git a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp index 52187d7e5e..640b5a24f0 100644 --- a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp +++ b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp @@ -43,7 +43,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in auto csps = inst->CrossSectionPositions(); std::vector faces; - // The PointByDistanceExpressesions are factored out into (a) a cartesian offset relative to the + // The PointByDistanceExpressions are factored out into (a) a cartesian offset relative to the // reference frame along a certain curve location (b) the longitude. // The longitudes determine the range of the sweep and the offsets are interpolated in between diff --git a/src/ifcopenshell-python/ifcopenshell/alignment.py b/src/ifcopenshell-python/ifcopenshell/alignment.py deleted file mode 100644 index 45eb8bc054..0000000000 --- a/src/ifcopenshell-python/ifcopenshell/alignment.py +++ /dev/null @@ -1,1158 +0,0 @@ -# IfcOpenShell - IFC toolkit and geometry engine -# Copyright (C) 2021 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 math -from typing import Sequence - -import numpy as np - -import ifcopenshell -import ifcopenshell.geom -import ifcopenshell.guid -import ifcopenshell.template -from ifcopenshell import entity_instance -from ifcopenshell import ifcopenshell_wrapper -import ifcopenshell.util -import ifcopenshell.util.stationing - - -def evaluate_representation(shape_rep: entity_instance, dist_along: float) -> np.ndarray: - """ - Calculate the 4x4 geometric transform at a point on an alignment segment - @param shape_rep: The representation shape (composite curve, gradient curve, or segmented reference curve) to evaluate - @param dist_along: The distance along this representation at the point of interest (point to be calculated) - """ - supported_rep_types = ["IFCCOMPOSITECURVE", "IFCGRADIENTCURVE", "IFCSEGMENTEDREFERENCECURVE"] - shape_rep_type = shape_rep.is_a().upper() - if not shape_rep_type in supported_rep_types: - raise NotImplementedError( - f"Expected entity type to be one of {[_ for _ in supported_rep_types]}, got '{shape_rep_type}" - ) - - # TODO: confirm point is not beyond limits of alignment - - s = ifcopenshell.geom.settings() - function_item = ifcopenshell_wrapper.map_shape(s, shape_rep.wrapped_data) - evaluator = ifcopenshell_wrapper.function_item_evaluator(s, function_item) - - trans_matrix = evaluator.evaluate(dist_along) - - return np.array(trans_matrix, dtype=np.float64).T - - -def evaluate_segment(segment: entity_instance, dist_along: float) -> np.ndarray: - """ - Calculate the 4x4 geometric transform at a point on an alignment segment - @param segment: The segment containing the point that we would like to - @param dist_along: The distance along this segment at the point of interest (point to be calculated) - """ - supported_segment_types = ["IFCCURVESEGMENT"] - segment_type = segment.is_a().upper() - if not segment_type in supported_segment_types: - raise NotImplementedError(f"Expected entity type 'IFCCURVESEGMENT', got '{segment_type}") - if dist_along > segment.SegmentLength: - raise ValueError(f"Provided value {dist_along=} is beyond the end of the segment ({segment.SegmentLength}).") - - s = ifcopenshell.geom.settings() - function_item = ifcopenshell_wrapper.map_shape(s, segment.wrapped_data) - evaluator = ifcopenshell_wrapper.function_item_evaluator(s, function_item) - - trans_matrix = evaluator.evaluate(dist_along) - - return np.array(trans_matrix, dtype=np.float64).T - - -def generate_vertices(rep_curve: entity_instance, distance_interval: float = 5.0) -> np.ndarray: - """ - Generate vertices along an alignment - - @param rep_curve: The alignment's representation curve to use to generate vertices. - - Note: rep_curve must be IfcCompositeCurve, IfcGradientCurve, or IfcSegmentedReferenceCurve - - @param distance_interval: The distance between points along the alignment at which to generate the points - """ - if rep_curve is None: - raise ValueError("Alignment representation not found.") - - s = ifcopenshell.geom.settings() - s.set("piecewise-step-type", 0) # 0 = step-size is maximum step size, 1 = step-size is mininimum number of steps - s.set("piecewise-step-size", distance_interval) - shape = ifcopenshell.geom.create_shape(s, rep_curve) - vertices = shape.verts - if len(vertices) == 0: - msg = f"[ERROR] No vertices generated by ifcopenshell.geom.create_shape()." - raise ValueError(msg) - return np.array(vertices).reshape((-1, 3)) - - -def print_structure(alignment, indent=0): - """ - Debugging function to print alignment decomposition - """ - print(" " * indent, str(alignment)[0:100]) - for rel in alignment.IsNestedBy: - for child in rel.RelatedObjects: - print_structure(child, indent + 2) - - -def name_segments(prefix: str, segments: Sequence[entity_instance]) -> None: - """ - Sets the segment name like ("H1" for horizontal, "V1" for vertical, "C1" for cant) - """ - for i, segment in enumerate(segments): - segment.Name = f"{prefix}{i + 1}" - - -class IfcAlignmentHelper: - """ - Create a new IfcAlignment including horizontal and vertical alignments by PI points. - - Currently only supports horizontal lines and circular arcs (no spirals or other transitions) - Currently only supports parabolic vertical curves. - Does not yet accommodate cant alignment considerations. - """ - - # TODO: add missing functionality noted in the docstring - - def __init__( - self, - file: ifcopenshell.file = None, - filename: str = None, - creator: str = None, - organization: str = None, - application: str = None, - project_globalid=None, - project_name: str = None, - ): - """ - @param file: An existing model that the alignment will be added to - @param filename: Name for a new model to be created that will contain the alignment - @param creator: Name of the actor creating the file - @param organization: Name of the creator's organization - @param application: Name of the authoring application - @param project_globalid: value for the file's IfcProject.GlobalId attribute - @param project_name: value for the file's IfcProject.Name attribute - """ - if file is None: - self._file = ifcopenshell.template.create( - filename=filename, - creator=creator, - organization=organization, - application=application, - project_globalid=project_globalid, - project_name=project_name, - schema_identifier="IFC4X3_ADD2", - ) - else: - self._file = file - - self._geom_context = self._file.by_type("IfcGeometricRepresentationContext")[0] - self._axis_geom_subcontext = self._file.createIfcGeometricRepresentationSubContext( - ContextIdentifier="Axis", ContextType="Model", ParentContext=self._geom_context, TargetView="GRAPH_VIEW" - ) - - def _create_segment_representations( - self, - global_placement: entity_instance, - curve_segments: Sequence[entity_instance], - segments: Sequence[entity_instance], - ): - for curve_segment, alignment_segment in zip(curve_segments, segments): - axis_representation = self._file.create_entity( - type="IfcShapeRepresentation", - ContextOfItems=self._axis_geom_subcontext, - RepresentationIdentifier="Axis", - RepresentationType="Segment", - Items=(curve_segment,), - ) - product = self._file.create_entity( - type="IfcProductDefinitionShape", Name=None, Description=None, Representations=(axis_representation,) - ) - alignment_segment.ObjectPlacement = global_placement - alignment_segment.Representation = product - - def _map_alignment_vertical_segment(self, segment: entity_instance) -> Sequence[entity_instance]: - segment_type = segment.is_a().upper() - expected_type = "IFCALIGNMENTVERTICALSEGMENT" - if not segment_type == expected_type: - raise TypeError(f"Expected to see type '{expected_type}', instead received '{segment_type}'.") - - start_distance_along = segment.StartDistAlong - horizontal_length = segment.HorizontalLength - start_height = segment.StartHeight - start_gradient = segment.StartGradient - end_gradient = segment.EndGradient - radius_of_curvature = segment.RadiusOfCurvature - - if math.isclose(horizontal_length, 0): - # set transition value based on whether this is the final zero-length segment - transition = "DISCONTINUOUS" - else: - transition = "CONTSAMEGRADIENTSAMECURVATURE" - - _type = segment.PredefinedType - - match _type: - case "CONSTANTGRADIENT": - parent_curve = self._file.create_entity( - type="IfcLine", - Pnt=self._file.create_entity( - type="IfcCartesianPoint", - Coordinates=(0.0, 0.0), - ), - Dir=self._file.create_entity( - type="IfcVector", - Orientation=self._file.create_entity( - type="IfcDirection", - DirectionRatios=(1.0, 0.0), - ), - Magnitude=1.0, - ), - ) - - dx = math.cos(math.atan(start_gradient)) - dy = math.sin(math.atan(start_gradient)) - curve_segment_length = horizontal_length / dx - - curve_segment = self._file.create_entity( - type="IfcCurveSegment", - Transition=transition, - Placement=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=self._file.create_entity( - type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height) - ), - RefDirection=self._file.createIfcDirection((dx, dy)), - ), - SegmentStart=self._file.createIfcLengthMeasure(0.0), - SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length), - ParentCurve=parent_curve, - ) - result = (curve_segment, None) - - case "PARABOLICARC": - A = start_height - B = start_gradient - C = (end_gradient - start_gradient) / (2.0 * horizontal_length) - - parent_curve = self._file.create_entity( - type="IfcPolynomialCurve", - Position=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=self._file.create_entity(type="IfcCartesianPoint", Coordinates=(0.0, 0.0)), - RefDirection=self._file.createIfcDirection( - (1.0, 0.0), - ), - ), - CoefficientsX=(0.0, 1.0), - CoefficientsY=(A, B, C), - ) - - dx = math.cos(math.atan(start_gradient)) - dy = math.sin(math.atan(start_gradient)) - curve_segment_length = ifcopenshell_wrapper.polynomial_length(A, B, C, horizontal_length) - - curve_segment = self._file.create_entity( - type="IfcCurveSegment", - Transition=transition, - Placement=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=self._file.create_entity( - type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height) - ), - RefDirection=self._file.createIfcDirection((dx, dy)), - ), - SegmentStart=self._file.createIfcLengthMeasure(0.0), - SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length), - ParentCurve=parent_curve, - ) - result = (curve_segment, None) - - case "CIRCULARARC": - start_angle = math.atan(start_gradient) - end_angle = math.atan(end_gradient) - if start_angle < end_angle: - radius = horizontal_length / (math.sin(end_angle) - math.sin(start_angle)) - else: - radius = horizontal_length / (math.sin(start_angle) - math.sin(end_angle)) - - parent_curve = self._file.create_entity( - type="IfcCircle", - Position=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=self._file.create_entity(type="IfcCartesianPoint", Coordinates=(0.0, 0.0)), - RefDirection=self._file.createIfcDirection( - (1.0, 0.0), - ), - ), - Radius=radius, - ) - - segment_curve_length = radius * math.fabs(end_angle - start_angle) - - curve_segment = self._file.create_entity( - type="IfcCurveSegment", - Transition=transition, - Placement=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=self._file.create_entity( - type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height) - ), - RefDirection=self._file.createIfcDirection( - (1.0, 0.0), - ), - ), - SegmentStart=self._file.createIfcLengthMeasure(0.0), - SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length), - ParentCurve=parent_curve, - ) - result = (curve_segment, None) - - case _: - result = (None, None) - - return result - - def _map_alignment_horizontal_segment(self, segment: entity_instance) -> Sequence[entity_instance]: - segment_type = segment.is_a().upper() - expected_type = "IFCALIGNMENTHORIZONTALSEGMENT" - if not segment_type == expected_type: - raise TypeError(f"Expected to see type '{expected_type}', instead received '{segment_type}'.") - - start_point = segment.StartPoint - start_direction = segment.StartDirection - start_radius = segment.StartRadiusOfCurvature - length = segment.SegmentLength - _type = segment.PredefinedType - - if math.isclose(length, 0): - # set transition value based on whether this is the final zero-length segment - transition = "DISCONTINUOUS" - else: - transition = "CONTSAMEGRADIENTSAMECURVATURE" - - if _type == "LINE": - parent_curve = self._file.create_entity( - type="IfcLine", - Pnt=self._file.create_entity( - type="IfcCartesianPoint", - Coordinates=(0.0, 0.0), - ), - Dir=self._file.create_entity( - type="IfcVector", - Orientation=self._file.create_entity( - type="IfcDirection", - DirectionRatios=(1.0, 0.0), - ), - Magnitude=1.0, - ), - ) - curve_segment = self._file.create_entity( - type="IfcCurveSegment", - Transition=transition, - Placement=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=start_point, - RefDirection=self._file.createIfcDirection( - (math.cos(start_direction), math.sin(start_direction)), - ), - ), - SegmentStart=self._file.createIfcLengthMeasure(0.0), - SegmentLength=self._file.createIfcLengthMeasure(length), - ParentCurve=parent_curve, - ) - result = (curve_segment, None) - elif _type == "CIRCULARARC": - parent_curve = self._file.createIfcCircle( - Position=self._file.createIfcAxis2Placement2D( - Location=self._file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)), - RefDirection=self._file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), - ), - Radius=abs(start_radius), - ) - - curve_segment = self._file.create_entity( - type="IfcCurveSegment", - Transition=transition, - Placement=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=start_point, - RefDirection=self._file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), - ), - SegmentStart=self._file.createIfcLengthMeasure(0.0), - SegmentLength=self._file.createIfcLengthMeasure(length * start_radius / abs(start_radius)), - ParentCurve=parent_curve, - ) - result = (curve_segment, None) - - else: - result = (None, None) - - return result - - def _create_horizontal_alignment( - self, - name: str, - description: str, - points: Sequence[Sequence[float]], - radii: Sequence[float], - include_geometry: bool = True, - ): - """ - Create a horizontal alignment using the PI layout method. - - @param name: value for Name attribute - @param description: value for Description attribute - @param points: (X, Y) pairs denoting the location of the horizontal PIs, including start (POB) and end (POE). - @param radii: radii values to use for transition - @param include_geometry: optionally create the alignment geometric representation as well as the semantic business logic - """ - horizontal_segments = list() # business logic - horizontal_curve_segments = list() # geometry - - xBT, yBT = points[0] - xPI, yPI = points[1] - - i = 1 - - for radius in radii: - # back tangent - dxBT = xPI - xBT - dyBT = yPI - yBT - angleBT = math.atan2(dyBT, dxBT) - lengthBT = math.sqrt(dxBT * dxBT + dyBT * dyBT) - - # forward tangent - i += 1 - xFT, yFT = points[i] - dxFT = xFT - xPI - dyFT = yFT - yPI - angleFT = math.atan2(dyFT, dxFT) - - delta = angleFT - angleBT - - tangent = abs(radius * math.tan(delta / 2)) - - lc = abs(radius * delta) - - radius *= delta / abs(delta) - - xPC = xPI - tangent * math.cos(angleBT) - yPC = yPI - tangent * math.sin(angleBT) - - xPT = xPI + tangent * math.cos(angleFT) - yPT = yPI + tangent * math.sin(angleFT) - - tangent_run = lengthBT - tangent - - # create back tangent run - pt = self._file.create_entity( - type="IfcCartesianPoint", - Coordinates=(xBT, yBT), - ) - design_parameters = self._file.create_entity( - type="IfcAlignmentHorizontalSegment", - StartTag=None, - EndTag=None, - StartPoint=pt, - StartDirection=angleBT, - StartRadiusOfCurvature=0.0, - EndRadiusOfCurvature=0.0, - SegmentLength=tangent_run, - GravityCenterLineHeight=None, - PredefinedType="LINE", - ) - alignment_segment = self._file.create_entity( - type="IfcAlignmentSegment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=None, - Description=None, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - DesignParameters=design_parameters, - ) - horizontal_segments.append(alignment_segment) - - if include_geometry: - horizontal_curve_segments.append(self._map_alignment_horizontal_segment(design_parameters)[0]) - - # create circular curve - pc = self._file.create_entity( - type="IfcCartesianPoint", - Coordinates=(xPC, yPC), - ) - design_parameters = self._file.create_entity( - type="IfcAlignmentHorizontalSegment", - StartTag=None, - EndTag=None, - StartPoint=pc, - StartDirection=angleBT, - StartRadiusOfCurvature=float(radius), - EndRadiusOfCurvature=float(radius), - SegmentLength=lc, - GravityCenterLineHeight=None, - PredefinedType="CIRCULARARC", - ) - alignment_segment = self._file.create_entity( - type="IfcAlignmentSegment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=None, - Description=None, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - DesignParameters=design_parameters, - ) - horizontal_segments.append(alignment_segment) - - if include_geometry: - horizontal_curve_segments.append(self._map_alignment_horizontal_segment(design_parameters)[0]) - - xBT = xPT - yBT = yPT - xPI = xFT - yPI = yFT - - # done processing radii - # create last tangent run - dx = xPI - xBT - dy = yPI - yBT - angleBT = math.atan2(dy, dx) - tangent_run = math.sqrt(dx * dx + dy * dy) - pt = self._file.create_entity(type="IfcCartesianPoint", Coordinates=(xBT, yBT)) - - design_parameters = self._file.create_entity( - type="IfcAlignmentHorizontalSegment", - StartTag=None, - EndTag=None, - StartPoint=pt, - StartDirection=angleBT, - StartRadiusOfCurvature=0.0, - EndRadiusOfCurvature=0.0, - SegmentLength=tangent_run, - GravityCenterLineHeight=None, - PredefinedType="LINE", - ) - alignment_segment = self._file.create_entity( - type="IfcAlignmentSegment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=None, - Description=None, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - DesignParameters=design_parameters, - ) - horizontal_segments.append(alignment_segment) - if include_geometry: - horizontal_curve_segments.append(self._map_alignment_horizontal_segment(design_parameters)[0]) - - # create zero length terminator segment - poe = self._file.create_entity(type="IfcCartesianPoint", Coordinates=(xPI, yPI)) - - design_parameters = self._file.create_entity( - type="IfcAlignmentHorizontalSegment", - StartTag="POE", - EndTag="POE", - StartPoint=poe, - StartDirection=angleBT, - StartRadiusOfCurvature=0.0, - EndRadiusOfCurvature=0.0, - SegmentLength=0.0, - GravityCenterLineHeight=None, - PredefinedType="LINE", - ) - alignment_segment = self._file.create_entity( - type="IfcAlignmentSegment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=None, - Description=None, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - DesignParameters=design_parameters, - ) - horizontal_segments.append(alignment_segment) - if include_geometry: - horizontal_curve_segments.append(self._map_alignment_horizontal_segment(design_parameters)[0]) - - if include_geometry: - composite_curve = self._file.create_entity( - type="IfcCompositeCurve", - Segments=horizontal_curve_segments, - SelfIntersect=False, - ) - else: - composite_curve = None - - return horizontal_segments, horizontal_curve_segments, composite_curve - - def _add_horizontal_alignment( - self, - alignment_name: str, - points: Sequence[Sequence[float]], - radii: Sequence[float], - include_geometry: bool = True, - alignment_description: str = None, - start_station: float = 1000.0, - ): - horizontal_segments, horizontal_curve_segments, composite_curve = self._create_horizontal_alignment( - alignment_name, - alignment_description, - points, - radii, - include_geometry, - ) - - name_segments(prefix="H", segments=horizontal_segments) - - # Create the horizontal alignment (IfcAlignmentHorizontal) and nest alignment segments - horizontal_alignment = self._file.create_entity( - type="IfcAlignmentHorizontal", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=f"{alignment_name} - Horizontal", - Description=alignment_description, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - ) - - nests_horizontal_segments = self._file.create_entity( - type="IfcRelNests", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name="Nests horizontal alignment segments under horizontal alignment", - RelatingObject=horizontal_alignment, - RelatedObjects=horizontal_segments, - ) - - placement = self._file.createIfcLocalPlacement( - PlacementRelTo=None, - RelativePlacement=self._file.createIfcAxis2Placement2D( - Location=self._file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)) - ), - ) - - # create the alignment - alignment = self._file.create_entity( - type="IfcAlignment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=alignment_name, - Description=alignment_description, - ObjectType=None, - ObjectPlacement=placement, - Representation=None, - PredefinedType=None, - ) - - # create geometric representation - if include_geometry: - # create the footprint representation - footprint_shape_representation = self._file.create_entity( - type="IfcShapeRepresentation", - ContextOfItems=self._axis_geom_subcontext, - RepresentationIdentifier="FootPrint", - RepresentationType="Curve2D", - Items=(composite_curve,), - ) - - # create the alignment product definition - product_definition_shape = self._file.create_entity( - type="IfcProductDefinitionShape", - Name="Alignment Product Definition Shape", - Description=None, - Representations=(footprint_shape_representation,), - ) - - # create representations for each segment - self._create_segment_representations(placement, horizontal_curve_segments, horizontal_segments) - - # add the representation to the alignment - alignment.Representation = product_definition_shape - - # create referent for start station - start_station_name = "Start Station ({})".format( - ifcopenshell.util.stationing.station_as_string(start_station) - ) - start_referent = self._file.createIfcReferent( - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=start_station_name, - Description=None, - ObjectType=None, - ObjectPlacement=self._file.createIfcLinearPlacement( - RelativePlacement=self._file.createIfcAxis2PlacementLinear( - Location=self._file.createIfcPointByDistanceExpression( - DistanceAlong=self._file.createIfcLengthMeasure(0.0), - OffsetLateral=None, - OffsetVertical=None, - OffsetLongitudinal=None, - BasisCurve=composite_curve, - ), - ), - CartesianPosition=None, - ), - Representation=None, - PredefinedType="STATION", - ) - pset_stationing = ifcopenshell.api.pset.add_pset(self._file, product=start_referent, name="Pset_Stationing") - ifcopenshell.api.pset.edit_pset(self._file, pset=pset_stationing, properties={"Station": start_station}) - - # nest the horizontal and the referent under the alignment - nesting_of_alignment = self._file.create_entity( - type="IfcRelNests", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name="Nests horizontal alignment and referents under overall alignment", - RelatingObject=alignment, - RelatedObjects=(horizontal_alignment, start_referent), - ) - - # aggregate the horizontal under the project - project = self._file.by_type("IfcProject")[0] - alignment_within_project = self._file.createIfcRelAggregates( - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name="Aggregates alignment under the project", - RelatingObject=project, - RelatedObjects=(alignment,), - ) - - return alignment - - def _create_vertical_alignment( - self, - composite_curve: entity_instance, - vpoints: Sequence[Sequence[float]], - lengths: Sequence[float], - include_geometry: bool = True, - ): - """ - Create a vertical alignment using the PI layout method. - - @param name: value for Name attribute - @param description: value for Description attribute - @param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end. - @param vclengths: horizontal length of parabolic vertical curves - @param include_geometry: optionally create the alignment geometric representation as well as the semantic business logic - """ - vertical_segments = list() # business logic - vertical_curve_segments = list() # geometry - xPBG, yPBG = vpoints[0] - xPVI, yPVI = vpoints[1] - i = 1 - for length in lengths: - # back gradient - dxBG = xPVI - xPBG - dyBG = yPVI - yPBG - start_slope = math.tan(math.atan2(dyBG, dxBG)) - - # forward gradient - i += 1 - xPFG, yPFG = vpoints[i] - dxFG = xPFG - xPVI - dyFG = yPFG - yPVI - end_slope = math.tan(math.atan2(dyFG, dxFG)) - - xEVC = xPVI + length / 2.0 - yEVC = yPVI + end_slope * length / 2.0 - - # create gradient - gradient_length = dxBG - length / 2.0 - design_parameters = self._file.create_entity( - type="IfcAlignmentVerticalSegment", - StartTag=None, - EndTag=None, - StartDistAlong=xPBG, - HorizontalLength=gradient_length, - StartHeight=yPBG, - StartGradient=start_slope, - EndGradient=start_slope, - RadiusOfCurvature=None, - PredefinedType="CONSTANTGRADIENT", - ) - alignment_segment = self._file.create_entity( - type="IfcAlignmentSegment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=None, - Description=None, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - DesignParameters=design_parameters, - ) - vertical_segments.append(alignment_segment) - - if include_geometry: - vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) - - # create vertical curve - k = (end_slope - start_slope) / length - xBVC = xPVI - length / 2.0 - yBVC = yPVI - start_slope * length / 2.0 - - design_parameters = self._file.create_entity( - type="IfcAlignmentVerticalSegment", - StartTag=None, - EndTag=None, - StartDistAlong=xBVC, - HorizontalLength=length, - StartHeight=yBVC, - StartGradient=start_slope, - EndGradient=end_slope, - RadiusOfCurvature=1 / k, - PredefinedType="PARABOLICARC", - ) - alignment_segment = self._file.create_entity( - type="IfcAlignmentSegment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=None, - Description=None, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - DesignParameters=design_parameters, - ) - vertical_segments.append(alignment_segment) - - if include_geometry: - vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) - - # start of next curve is end of this curve - xPBG = xEVC - yPBG = yEVC - xPVI = xPFG - yPVI = yPFG - - # create last gradient run - dx = xPVI - xPBG - dy = yPVI - yPBG - slope = math.tan(math.atan2(dy, dx)) - gradient_length = dx - - design_parameters = self._file.create_entity( - type="IfcAlignmentVerticalSegment", - StartTag=None, - EndTag=None, - StartDistAlong=xPBG, - HorizontalLength=gradient_length, - StartHeight=yPBG, - StartGradient=slope, - EndGradient=slope, - RadiusOfCurvature=None, - PredefinedType="CONSTANTGRADIENT", - ) - alignment_segment = self._file.create_entity( - type="IfcAlignmentSegment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=None, - Description=None, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - DesignParameters=design_parameters, - ) - vertical_segments.append(alignment_segment) - - if include_geometry: - vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) - - # create zero length terminator segment - design_parameters = self._file.create_entity( - type="IfcAlignmentVerticalSegment", - StartTag="VPOE", - EndTag="VPOE", - StartDistAlong=xPVI, - HorizontalLength=0.0, - StartHeight=yPVI, - StartGradient=slope, - EndGradient=slope, - RadiusOfCurvature=None, - PredefinedType="CONSTANTGRADIENT", - ) - alignment_segment = self._file.create_entity( - type="IfcAlignmentSegment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=None, - Description=None, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - DesignParameters=design_parameters, - ) - vertical_segments.append(alignment_segment) - - if include_geometry: - vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) - - if include_geometry: - gradient_curve = self._file.create_entity( - type="IfcGradientCurve", - Segments=vertical_curve_segments, - SelfIntersect=False, - BaseCurve=composite_curve, - EndPoint=None, - ) - else: - gradient_curve = None - - return vertical_segments, vertical_curve_segments, gradient_curve - - def create_alignment_by_pi_method( - self, - alignment_name: str, - points: Sequence[Sequence[float]], - radii: Sequence[float], - vpoints: Sequence[Sequence[float]], - lengths: Sequence[float], - alignment_description: str = None, - start_station: float = 1000.0, - include_geometry: bool = True, - ): - """ - Create an alignment using the PI layout method for both horizontal and vertical alignments. - - @param alignment_name: value for Name attribute - @param alignment_description: value for Description attribute - @param points: (X,Y) pairs denoting the location of the horizontal PIs, including start and end - @param radii: radii values to use for transition - @param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end. - @param lengths: parabolic vertical curve horizontal length values to use for transition - @param start_station: ??? NOT USED AT THIS TIME ??? - @param include_geometry: optionally create the alignment geometric representation as well as the semantic business logic - """ - - horizontal_segments, horizontal_curve_segments, composite_curve = self._create_horizontal_alignment( - alignment_name, alignment_description, points, radii, include_geometry - ) - vertical_segments, vertical_curve_segments, gradient_curve = self._create_vertical_alignment( - composite_curve, vpoints, lengths - ) - - name_segments(prefix="H", segments=horizontal_segments) - name_segments(prefix="V", segments=vertical_segments) - - # Create the horizontal alignment (IfcAlignmentHorizontal) and nest alignment segments - horizontal_alignment = self._file.create_entity( - type="IfcAlignmentHorizontal", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=f"{alignment_name} - Horizontal", - Description=alignment_description, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - ) - - nests_horizontal_segments = self._file.create_entity( - type="IfcRelNests", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name="Nests horizontal alignment segments under horizontal alignment", - RelatingObject=horizontal_alignment, - RelatedObjects=horizontal_segments, - ) - - # Create the vertical alignment (IfcAlignmentVertical) and nest alignment segments - vertical_alignment = self._file.create_entity( - type="IfcAlignmentVertical", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=f"{alignment_name} - Vertical", - Description=alignment_description, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - ) - - nests_vertical_segments = self._file.create_entity( - type="IfcRelNests", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name="Nests vertical alignment segments under vertical alignment", - RelatingObject=vertical_alignment, - RelatedObjects=vertical_segments, - ) - - # create the alignment - placement = self._file.createIfcLocalPlacement( - PlacementRelTo=None, - RelativePlacement=self._file.createIfcAxis2Placement2D( - Location=self._file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)) - ), - ) - - alignment = self._file.create_entity( - type="IfcAlignment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=alignment_name, - Description=alignment_description, - ObjectType=None, - ObjectPlacement=placement, - Representation=None, - PredefinedType=None, - ) - - # create referent for start station - start_station_name = "Start Station ({})".format(ifcopenshell.util.stationing.station_as_string(start_station)) - start_referent = self._file.createIfcReferent( - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=start_station_name, - Description=None, - ObjectType=None, - ObjectPlacement=self._file.createIfcLinearPlacement( - RelativePlacement=self._file.createIfcAxis2PlacementLinear( - Location=self._file.createIfcPointByDistanceExpression( - DistanceAlong=self._file.createIfcLengthMeasure(0.0), - OffsetLateral=None, - OffsetVertical=None, - OffsetLongitudinal=None, - BasisCurve=composite_curve, - ), - ), - CartesianPosition=None, - ), - Representation=None, - PredefinedType="STATION", - ) - pset_stationing = ifcopenshell.api.pset.add_pset(self._file, product=start_referent, name="Pset_Stationing") - ifcopenshell.api.pset.edit_pset(self._file, pset=pset_stationing, properties={"Station": start_station}) - - # nest the horizontal, vertical and the referent under the alignment - nesting_of_alignment = self._file.create_entity( - type="IfcRelNests", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name="Nests horizontal alignment, vertical alginment, and referents under overall alignment", - RelatingObject=alignment, - RelatedObjects=(horizontal_alignment, vertical_alignment, start_referent), - ) - - # aggregate the alignment under the project - project = self._file.by_type("IfcProject")[0] - alignment_within_project = self._file.createIfcRelAggregates( - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name="Aggregates alignment under the project", - RelatingObject=project, - RelatedObjects=(alignment,), - ) - - # create geometric representation - if include_geometry: - # create the footprint representation - footprint_shape_representation = self._file.create_entity( - type="IfcShapeRepresentation", - ContextOfItems=self._axis_geom_subcontext, - RepresentationIdentifier="FootPrint", - RepresentationType="Curve2D", - Items=(composite_curve,), - ) - - # create the Curve3D representation - axis3d_shape_representation = self._file.create_entity( - type="IfcShapeRepresentation", - ContextOfItems=self._axis_geom_subcontext, - RepresentationIdentifier="Axis", - RepresentationType="Curve3D", - Items=(gradient_curve,), - ) - - # create the alignment product definition - product_definition_shape = self._file.create_entity( - type="IfcProductDefinitionShape", - Name="Alignment Product Definition Shape", - Description=None, - Representations=( - footprint_shape_representation, - axis3d_shape_representation, - ), - ) - - # create representations for each segment - self._create_segment_representations(placement, horizontal_curve_segments, horizontal_segments) - self._create_segment_representations(placement, vertical_curve_segments, vertical_segments) - - # add the representation to the alignment - alignment.Representation = product_definition_shape - - return alignment - - def create_horizontal_alignment_by_pi_method( - self, - name: str, - hpoints: Sequence[Sequence[float]], - radii: Sequence[float], - include_geometry: bool = True, - description: str = None, - start_station: float = 1000.0, - ): - """ - Create a new alignment with a horizontal alignment using the PI layout method - """ - return self._add_horizontal_alignment( - alignment_name=name, - points=hpoints, - radii=radii, - include_geometry=include_geometry, - alignment_description=description, - start_station=start_station, - ) - - def save_file(self, filename) -> None: - self._file.write(filename) - - -if __name__ == "__main__": - import sys - from matplotlib import pyplot as plt - - f = ifcopenshell.file(schema="IFC4X3_ADD2") - project = f.create_entity(type="IfcProject", GlobalId=ifcopenshell.guid.new()) - context = f.create_entity(type="IfcGeometricRepresentationContext") - - points = [(0.0, 0.0), (100.0, 0.0), (200.0, 150.0)] - radii = [50.0] - - helper = IfcAlignmentHelper(f) - helper.create_horizontal_alignment_by_pi_method(name="MyAlignment", hpoints=points, radii=radii) - - # f = ifcopenshell.open(sys.argv[1]) - print_structure(f.by_type("IfcAlignment")[0]) - - al_hor_rep = f.by_type("IfcCompositeCurve")[0] - - xy = generate_vertices(rep_curve=al_hor_rep, distance_interval=10.0) - - plt.plot(xy[0], xy[1]) - plt.savefig("horizontal_alignment.png") diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py new file mode 100644 index 0000000000..1098c23fe6 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py @@ -0,0 +1,70 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 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 . + +""" +Manages alignment layout (business logical) and alignment geometry (geometric representations). + +This API does not determine alignment parameters based on rules, such as minimum curve radius as a function of design speed or sight distance. + +This API is under development and subject to code breaking changes in the future. + +Presently, this API supports: + 1. Creating alignments, both horizontal and vertical, using the PI method. Alignment definition can be read from a CSV file. + 2. Adding business logic and geometric segments to the end of an alignment + 3. Adding and removing the zero length segment at the end of alignments + 4. Creating geometric representations from a business logical definition + 5. Mapping individual business logical segments to geometric segments (complete for horizontal, missing clothoid for vertical, not implemented for cant) + 6. Using curve geometry to determine IfcCurveSegment.Transition transition code. + 7. Utility functions for printing business logical and geometric representations, as well as minimumal geometry evaluations + +Future versions of this API will support: + 1. Defining alignments using the PI method, including transition spirals + 2. Updating horizontal curve definitions by revising transition spiral parameters and circular curve radii + 3. Updating vertical curve definitions by revising horizontal length of curves + 4. Removing a segment at any location along a curve + 5. Adding a segment at any location along a curve +""" + +from .add_segment_to_curve import add_segment_to_curve +from .add_segment_to_layout import add_segment_to_layout +from .add_stationing_to_alignment import add_stationing_to_alignment +from .add_vertical_alignment_by_pi_method import add_vertical_alignment_by_pi_method +from .add_vertical_alignment import add_vertical_alignment +from .add_zero_length_segment import add_zero_length_segment +from .create_alignment_by_pi_method import create_alignment_by_pi_method +from .create_alignment_from_csv import create_alignment_from_csv +from .create_horizontal_alignment_by_pi_method import create_horizontal_alignment_by_pi_method +from .create_geometric_representation import create_geometric_representation +from .create_vertical_alignment_by_pi_method import create_vertical_alignment_by_pi_method +from .get_alignment_layouts import get_alignment_layouts +from .get_axis_subcontext import get_axis_subcontext +from .get_basis_curve import get_basis_curve +from .get_child_alignments import get_child_alignments +from .get_curve import get_curve +from .get_parent_alignment import get_parent_alignment +from .has_zero_length_segment import has_zero_length_segment +from .map_alignment_segments import map_alignment_segments +from .map_alignment_segment import map_alignment_segment +from .map_alignment_horizontal_segment import map_alignment_horizontal_segment +from .map_alignment_vertical_segment import map_alignment_vertical_segment +from .map_alignment_cant_segment import map_alignment_cant_segment +from .name_segments import name_segments +from .remove_last_segment import remove_last_segment +from .remove_zero_length_segment import remove_zero_length_segment +from .update_curve_segment_transition_code import update_curve_segment_transition_code +from .util import * diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_segment_to_curve.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_segment_to_curve.py new file mode 100644 index 0000000000..7b8370fdcc --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_segment_to_curve.py @@ -0,0 +1,76 @@ +# 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.alignment +import ifcopenshell.geom +from ifcopenshell import entity_instance + + +def add_segment_to_curve(file: ifcopenshell.file, segment: entity_instance, composite_curve: entity_instance) -> None: + """ + Adds a segment to a composite curve. The segment must not belong to another composite curve (len(segment.UsingCurves) == 0). + If the composite curve does not have any segments, the segment is simply appended to the curve. + If the composite curve has segments, the position, ref. direction, and curvature at the end of the last segment is + compared to the position, ref. direction and curvature at the start of the new segment. The IfcCurveSegment.Transition of the last curve segment is updated. + + :param segment: The segment to be added to the curve + :param composite_curve: The curve receiving the segment + :return: None + """ + expected_type = "IfcCurveSegment" + if not segment.is_a(expected_type): + raise TypeError(f"Expected to see '{expected_type}', instead received '{segment.is_a()}'.") + + if 0 < len(segment.UsingCurves): + raise TypeError("IfcCurveSegment cannot belong to other curves") + + expected_type = "IfcCompositeCurve" + if not composite_curve.is_a(expected_type): + raise TypeError(f"Expected to see '{expected_type}', instead received '{composite_curve.is_a()}'.") + + settings = ifcopenshell.geom.settings() + if composite_curve.Segments == None or 0 == len(composite_curve.Segments): + # this is the first segment so just add it + if composite_curve.Segments == None: + composite_curve.Segments = [] + + # the last segment is always discontinuous + segment.Transition = "DISCONTINUOUS" + + composite_curve.Segments += (segment,) + assert len(segment.UsingCurves) == 1 + else: + zero_length_segment = ( + ifcopenshell.api.alignment.remove_zero_length_segment(file, composite_curve) + if ifcopenshell.api.alignment.has_zero_length_segment(composite_curve) + else None + ) + + prev_segment = composite_curve.Segments[-1] + + # the last segment is always discontinuous + segment.Transition = "DISCONTINUOUS" + + # must add the new segment to the curve before updating the transition code + composite_curve.Segments += (segment,) + + ifcopenshell.api.alignment.update_curve_segment_transition_code(prev_segment, segment) + + if zero_length_segment: + ifcopenshell.api.alignment.add_segment_to_curve(zero_length_segment, composite_curve) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_segment_to_layout.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_segment_to_layout.py new file mode 100644 index 0000000000..c92e475106 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_segment_to_layout.py @@ -0,0 +1,52 @@ +# 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.alignment +import ifcopenshell.api.nest +from ifcopenshell import entity_instance +from typing import Sequence + + +def add_segment_to_layout(file: ifcopenshell.file, alignment: entity_instance, segment: entity_instance) -> None: + """ + Adds a segment to a layout alignment (horizontal, vertical, or cant) + + :param alignment: The alignment + :param segment: The segment to be appended + :return: None + """ + expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"] + if not alignment.is_a() in expected_types: + raise TypeError( + f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{alignment.is_a()}" + ) + + if not (segment.is_a("IfcAlignmentSegment")): + raise TypeError(f"Expected to see IfcAlignmentSegment, instead received '{segment.is_a()}.") + + zero_length_segment = ( + ifcopenshell.api.alignment.remove_zero_length_segment(file, alignment) + if ifcopenshell.api.alignment.has_zero_length_segment(alignment) + else None + ) + + ifcopenshell.api.nest.assign_object(file, related_objects=[segment], relating_object=alignment) + + if zero_length_segment: + ifcopenshell.api.nest.assign_object(file, related_objects=[zero_length_segment], relating_object=alignment) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_to_alignment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_to_alignment.py new file mode 100644 index 0000000000..ffc42c70d8 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_to_alignment.py @@ -0,0 +1,79 @@ +# 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.alignment +import ifcopenshell.api.nest +import ifcopenshell.guid +from ifcopenshell import entity_instance + + +def add_stationing_to_alignment(file: ifcopenshell.file, alignment: entity_instance, start_station: float) -> None: + """ + Adds stationing to an alignment by creating an IfcReferent with the Pset_Stationing property set to establish the stationing at the start of the alignment. + Note - this function assumes the stationing has not been previously defined + + :param alignment: the alignment to be stationed + :param start_station: station value at the start of the alignment + :return: None + + Example: + + .. code:: python + + alignment = model.by_type("IfcAlignment")[0] + ifcopenshell.api.alignment.add_stationing_to_alignment(model,alignment=alignment,start_station=100.0) + """ + # this commented out code is what you would do to add a geometric representation of the referent + # the example is a circle. a better way would be to pass a representation into the function + object_placement = None + representation = None + # basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + # if basis_curve: + # object_placement = file.createIfcLinearPlacement( + # RelativePlacement=file.createIfcAxis2PlacementLinear( + # Location=file.createIfcPointByDistanceExpression( + # DistanceAlong=file.createIfcLengthMeasure(0.0), + # OffsetLateral=None, + # OffsetVertical=None, + # OffsetLongitudinal=None, + # BasisCurve=basis_curve, + # ) + # ), + # CartesianPosition=None, + # ) + # representation = file.create_entity( + # name="IfcCircle", + # position=file.createIfcAxis2Placement2D(Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)), + # radius=1.0) + # ) + + # create referent for start station + start_referent = file.createIfcReferent( + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=ifcopenshell.util.stationing.station_as_string(start_station), + Description=None, + ObjectType=None, + ObjectPlacement=object_placement, + Representation=representation, + PredefinedType="STATION", + ) + pset_stationing = ifcopenshell.api.pset.add_pset(file, product=start_referent, name="Pset_Stationing") + ifcopenshell.api.pset.edit_pset(file, pset=pset_stationing, properties={"Station": start_station}) + ifcopenshell.api.nest.assign_object(file, related_objects=[start_referent], relating_object=alignment) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_vertical_alignment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_vertical_alignment.py new file mode 100644 index 0000000000..d2f7d228a8 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_vertical_alignment.py @@ -0,0 +1,198 @@ +# 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 +import ifcopenshell.util.stationing +import ifcopenshell.api +from ifcopenshell import entity_instance + + +def _move_vertical_to_child_alignment( + file: ifcopenshell.file, parent_alignment: entity_instance, vertical_alignment: 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 alignment from the parent alignment + ifcopenshell.api.nest.unassign_object(file, related_objects=[vertical_alignment]) + + # create the child alignment + child_alignment = ifcopenshell.api.root.create_entity( + file, ifc_class="IfcAlignment", name=f"Child of {parent_alignment.Name}" + ) + + # nest the vertical alignment onto the child alignment + ifcopenshell.api.nest.assign_object(file, related_objects=[vertical_alignment], relating_object=child_alignment) + + # aggreage the child alignment to the parent alignment + ifcopenshell.api.aggregate.assign_object(file, products=[child_alignment], relating_object=parent_alignment) + + # 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) + break + + +def add_vertical_alignment( + file: ifcopenshell.file, parent_alignment: entity_instance, vertical_alignment: entity_instance +) -> None: + """ + Adds a vertical alignment to a previously created alignment. + + If this is the first vertical alignment 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 alignment 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 alignment 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 alignment is unassigned from the IfcRelNests of the parent alignment and assigned to the new child alignment IfcRelNests + 3) A second child IfcAlignment is created ant is is IfcRelAggregates with the parent alignment. + 4) The vertical_alignment is assigned to the second child alignment + + For the third and subsequent vertical alignments, a new child alignment is created and aggregated to the parent alignment and an IfcAlignmentVertical is created + from vpoints and lengths and assigned to the new child alignment. + + If the parent_alignment has a geometric representation, a geometric representation will be created for the vertical alignment. + + :param parent_alignment: The parent alignment + :param vertical_alignment: The vertical alignment to be added + :return: None + """ + + # 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_alignments_nesting_alignment = [ + c for c in ifcopenshell.util.element.get_components(parent_alignment) if c.is_a("IfcAlignmentVertical") + ] + + # move the vertical alignment to a child alignment because there is going to be more than one vertical + assert len(vertical_alignments_nesting_alignment) == 0 or len(vertical_alignments_nesting_alignment) == 1 + for vertical_alignment_nesting_alignment in vertical_alignments_nesting_alignment: + _move_vertical_to_child_alignment(file, parent_alignment, vertical_alignment_nesting_alignment) + + if len(child_alignments) == 0 and len(vertical_alignments_nesting_alignment) == 0: + # this is the first vertical alignment so nest it into the parent alignment (IFC CT 4.1.4.4.1.1) + ifcopenshell.api.nest.assign_object( + file, related_objects=[vertical_alignment], relating_object=parent_alignment + ) + + base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment) + if base_curve: + # the parent alignment has a Representation so create a representation for the vertical + gradient_curve = file.create_entity( + type="IfcGradientCurve", Segments=[], SelfIntersect=False, BaseCurve=base_curve, EndPoint=None + ) + + # using the business logic definition of vertical_alignment, create the curve segments and assign to gradient_curve + ifcopenshell.api.alignment.map_alignment_segments(file, vertical_alignment, gradient_curve) + + # 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.create_entity( + type="IfcShapeRepresentation", + 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.create_entity( + type="IfcAlignment", + 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_alignment], relating_object=child_alignment) + + base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment) + if base_curve: + child_alignment.ObjectPlacement = parent_alignment.ObjectPlacement + + # the parent alignment has a Representation so create a representation for the vertical + gradient_curve = file.create_entity( + type="IfcGradientCurve", Segments=[], SelfIntersect=False, BaseCurve=base_curve, EndPoint=None + ) + + ifcopenshell.api.alignment.map_alignment_segments(file, vertical_alignment, gradient_curve) + + axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file) + + # create the Curve3D representation + axis3d_shape_representation = file.create_entity( + type="IfcShapeRepresentation", + 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) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_vertical_alignment_by_pi_method.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_vertical_alignment_by_pi_method.py new file mode 100644 index 0000000000..67d55f70dc --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_vertical_alignment_by_pi_method.py @@ -0,0 +1,59 @@ +# 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.alignment.add_vertical_alignment +from ifcopenshell import entity_instance +from typing import Sequence + + +def add_vertical_alignment_by_pi_method( + file: ifcopenshell.file, + parent_alignment: entity_instance, + vpoints: Sequence[Sequence[float]], + lengths: Sequence[float], +) -> None: + """ + Adds a vertical alignment to a previously created alignment using the PI method. + + If this is the first vertical alignment 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 alignment 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 alignment 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 alignment is unassigned from the IfcRelNests of the parent alignment and assigned to the new child alignment IfcRelNests + 3) A second child IfcAlignment is created and it is IfcRelAggregates with the parent alignment. + 4) An IfcAlignmentVertical is created from vpoints and lengths and it is assigned to the second child alignment + + For the third and subsequent vertical alignments, a new child alignment is created and aggregated to the parent alignment and an IfcAlignmentVertical is created + from vpoints and lengths and assigned to the new child alignment. + + If the parent_alignment has a geometric representation, a geometric representation will be created for the vertical alignment. + + :param parent_alignment: The parent alignment + :param vpoints: A sequence of (D,Z) points where D is distance along horizontal and Z is elevation + :param: lengths: Lengths of parabolic vertical curves occuring at each VPI + :return: None + """ + vertical_alignment = ifcopenshell.api.alignment.create_vertical_alignment_by_pi_method( + file, parent_alignment.Name, vpoints, lengths + ) + ifcopenshell.api.alignment.add_vertical_alignment(file, parent_alignment, vertical_alignment) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_zero_length_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_zero_length_segment.py new file mode 100644 index 0000000000..a161757213 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_zero_length_segment.py @@ -0,0 +1,101 @@ +# 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.alignment +import ifcopenshell.api.nest +import ifcopenshell.geom +import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper +import numpy as np +from ifcopenshell import entity_instance + + +def add_zero_length_segment(file: ifcopenshell.file, entity: entity_instance) -> None: + """ + Adds a zero length segment to the end of entity. + + :param entity: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant or IfcCompositeCurve (or subtype) + :return: None + """ + expected_types = [ + "IfcAlignmentHorizontal", + "IfcAlignmentVertical", + "IfcAlignmentCant", + "IfcCompositeCurve", + "IfcGradientCurve", + "IfcSegmentedReferenceCurve", + ] + if not entity.is_a() in expected_types: + raise TypeError( + f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{entity.is_a()}" + ) + + if entity.is_a("IfcCompositeCurve"): + last_segment = entity.Segments[-1] + settings = ifcopenshell.geom.settings() + segment_fn = ifcopenshell_wrapper.map_shape(settings, last_segment.wrapped_data) + segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn) + e = segment_evaluator.evaluate(segment_fn.end()) + end = np.array(e) + x = float(end[0, 3]) + y = float(end[1, 3]) + dx = float(end[0, 0]) + dy = float(end[1, 0]) + + 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, + ), + ) + curve_segment = file.createIfcCurveSegment( + Transition="DISCONTINUOUS", + Placement=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint(Coordinates=((x, y))), + RefDirection=file.createIfcDirection((dx, dy)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(0.0), + ParentCurve=parent_curve, + ) + ifcopenshell.api.alignment.add_segment_to_curve(file, curve_segment, entity) + else: + for rel in entity.IsNestedBy: + if 0 < len(rel.RelatedObjects): + last_segment = rel.RelatedObjects[-1] + if last_segment.is_a("IfcAlignmentSegment"): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint( + (0.0, 0.0) + ), # this is a little problematic. need to know the end point and tangent + StartDirection=0.0, # of the previous segment, which requires geometry mapping + SegmentLength=0.0, + PredefinedType="LINE", + ) + segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + ifcopenshell.api.nest.assign_object( + file, + related_objects=[ + segment, + ], + relating_object=entity, + ) + break diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment_by_pi_method.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment_by_pi_method.py new file mode 100644 index 0000000000..47dc64ef60 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment_by_pi_method.py @@ -0,0 +1,80 @@ +# 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.alignment +from ifcopenshell import entity_instance +from typing import Sequence + + +def create_alignment_by_pi_method( + file: ifcopenshell.file, + alignment_name: str, + hpoints: Sequence[Sequence[float]], + radii: Sequence[float], + vpoints: Sequence[Sequence[float]] = None, + lengths: Sequence[float] = None, + alignment_description: str = None, +) -> entity_instance: + """ + Create an alignment using the PI layout method for both horizontal and vertical alignments. + If vpoints and lengths are omitted, only a horizontal alignment is created. Only the business logic + entities are creaed. Use create_geometric_representation() to create the geometric entities. + + :param alignment_name: value for Name attribute + :param points: (X,Y) pairs denoting the location of the horizontal PIs, including start and end + :param radii: radii values to use for transition + :param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end. + :param lengths: parabolic vertical curve horizontal length values to use for transition + :param alignment_description: value for Description attribute + :return: Returns an IfcAlignment + """ + alignments = [] + + horizontal_alignment = ifcopenshell.api.alignment.create_horizontal_alignment_by_pi_method( + file, alignment_name, hpoints, radii + ) + alignments.append(horizontal_alignment) + + if vpoints and lengths: + vertical_alignment = ifcopenshell.api.alignment.create_vertical_alignment_by_pi_method( + file, alignment_name, vpoints, lengths + ) + alignments.append(vertical_alignment) + + # create the alignment + alignment = file.create_entity( + type="IfcAlignment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=alignment_name, + Description=alignment_description, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + PredefinedType=None, + ) + + # nest the horizontal and vertical under the alignment + ifcopenshell.api.nest.assign_object(file, related_objects=alignments, relating_object=alignment) + + # IFC 4.1.4.1.1 Alignment Aggregation To Project + project = file.by_type("IfcProject")[0] + ifcopenshell.api.aggregate.assign_object(file, products=[alignment], relating_object=project) + + return alignment diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment_from_csv.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment_from_csv.py new file mode 100644 index 0000000000..8643ea1405 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment_from_csv.py @@ -0,0 +1,116 @@ +# 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 +import ifcopenshell.util.stationing +import ifcopenshell.api +from ifcopenshell import entity_instance +from ifcopenshell.api.alignment import get_axis_subcontext + +import math +from typing import Sequence + +import csv + + +def create_alignment_from_csv(file: ifcopenshell.file, filepath: str) -> entity_instance: + """ + Creates an alignment from PI data stored in a CSV file. Only the business logic + entities are creaed. Use create_geometric_representation() to create the geometric entities. + + The format of the file is: + + X1,Y1,R1,X2,Y2,R2 ... Xn-1,Yn-1,Rn-1,Xn,Yn + + D1,Z1,L1,D2,Z2,L2 ... Dn-1,Zn-1,Ln-1,Dn,Zn + + D1,Z1,L1,D2,Z2,L2 ... Dn-1,Zn-1,Ln-1,Dn,Zn + + ... + + where: + X,Y are PI coordinates + + R is the horizontal circular curve radius + + D,Z are VPI coordinates as "Distance Along","Elevation" + + L is the horizontal length of a parabolic vertical transition curve + + R1 and Rn, as well as L1 and Ln are placeholders and not used. They are recommended to have values of 0.0. + + R2 and Rn-2 are the radii of the first and last horizontal curves. + + L2 and Ln-2 are the length of the first and last vertical curves. + + The CSV file contains one horizontal alignment, zero, one, or more vertical alignments + + :param filepath: path the to CSV file + :return: IfcAlignment + """ + with open(filepath, newline="") as csvfile: + reader = csv.reader(csvfile) + row_count = 0 + for row in reader: + data = list(map(float, row)) # Convert all values to float + coordinates: list[list[float]] = ( + [] + ) # horizontal coordinates for first row, vertical coordinates for subsequent rows + radii: list[float] = [] # horizontal curve radii for first row, vertical curve length for subsequent rows + + row_count += 1 + + i = 0 + while i < len(data): + if i + 1 < len(data): + x, y = float(data[i]), float(data[i + 1]) + coordinates.append((x, y)) # Store (X, Y) pair + i += 2 + if i < len(data) and (i + 1) % 3 == 0: # Every third element after an (X,Y) pair is R + radii.append(data[i]) + i += 1 + + radii = radii[1:-1] # The first radius value is a placeholder, remove it + + if row_count == 1: + # create the alignment + alignment = file.createIfcAlignment(GlobalId=ifcopenshell.guid.new()) + # create the horizontal alignment + horizontal_alignment = ifcopenshell.api.alignment.create_horizontal_alignment_by_pi_method( + file, "Alignment_from_CSV", coordinates, radii + ) + # nest them together + ifcopenshell.api.nest.assign_object( + file, related_objects=(horizontal_alignment,), relating_object=alignment + ) + else: + # add all subsequent vertical alignments + ifcopenshell.api.alignment.add_vertical_alignment_by_pi_method(file, alignment, coordinates, radii) + + # IFC 4.1.4.1.1 Alignment Aggregation To Project + project = file.by_type("IfcProject")[0] + ifcopenshell.api.aggregate.assign_object(file, products=[alignment], relating_object=project) + + return alignment diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_geometric_representation.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_geometric_representation.py new file mode 100644 index 0000000000..86353c9e82 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_geometric_representation.py @@ -0,0 +1,172 @@ +# 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.alignment +from ifcopenshell import entity_instance + +import math +from typing import Sequence + + +def create_geometric_representation(file: ifcopenshell.file, alignment: entity_instance) -> None: + """ + Create geometric representation for the alignment. + + There are 5 different cases: + + 1) Horizontal only + 2) Horizontal + Vertical + 3) Horizontal + Vertical + Cant + 4) Vertical only (this occurs when horizontal is reused from a parent alignment) + 5) Vertical + Cant (this occurs when horizontal is reused from a parent alignment) + + :param alignment: The alignment for which the representation is being created + :return: None + """ + + expected_type = "IfcAlignment" + if not alignment.is_a(expected_type): + raise TypeError("Expected '{expected_type}' but got '{alignment.is_a()}'") + + placement = file.createIfcLocalPlacement( + PlacementRelTo=None, + RelativePlacement=file.createIfcAxis2Placement2D(Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0))), + ) + + alignment.ObjectPlacement = placement + + axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file) + + layouts = ifcopenshell.api.alignment.get_alignment_layouts(alignment) + children = ifcopenshell.api.alignment.get_child_alignments(alignment) + + if len(layouts) == 1 and len(children) == 0: + assert layouts[0].is_a("IfcAlignmentHorizontal") + # Horizontal only - IFC CT 4.1.7.1.1.1 + composite_curve = file.createIfcCompositeCurve() + ifcopenshell.api.alignment.map_alignment_segments(file, layouts[0], composite_curve) + representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Curve2D", + Items=(composite_curve,), + ) + ifcopenshell.api.geometry.assign_representation(file, alignment, representation) + elif len(layouts) == 2 and len(children) == 0: + # Horizontal and Vertical - IFC CT 4.1.7.1.1.1 + assert layouts[0].is_a("IfcAlignmentHorizontal") + assert layouts[1].is_a("IfcAlignmentVertical") + composite_curve = file.createIfcCompositeCurve() + ifcopenshell.api.alignment.map_alignment_segments(file, layouts[0], composite_curve) + representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="FootPrint", + RepresentationType="Curve2D", + Items=(composite_curve,), + ) + ifcopenshell.api.geometry.assign_representation(file, alignment, representation) + + gradient_curve = file.createIfcGradientCurve(BaseCurve=composite_curve) + ifcopenshell.api.alignment.map_alignment_segments(file, layouts[1], gradient_curve) + representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Curve3D", + Items=(gradient_curve,), + ) + ifcopenshell.api.geometry.assign_representation(file, alignment, representation) + elif len(layouts) == 3 and len(children) == 0: + # Horizontal, Vertical, and Cant - IFC CT 4.1.7.1.1.3 + assert layouts[0].is_a("IfcAlignmentHorizontal") + assert layouts[1].is_a("IfcAlignmentVertical") + assert layouts[2].is_a("IfcAlignmentCant") + composite_curve = file.createIfcCompositeCurve() + ifcopenshell.api.alignment.map_alignment_segments(file, layouts[0], composite_curve) + representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="FootPrint", + RepresentationType="Curve2D", + Items=(composite_curve,), + ) + ifcopenshell.api.geometry.assign_representation(file, alignment, representation) + + gradient_curve = file.createIfcGradientCurve(BaseCurve=composite_curve) + ifcopenshell.api.alignment.map_alignment_segments(file, layouts[1], gradient_curve) + segmented_reference_curve = file.createIfcSegmentedReferenceCurve(BaseCurve=gradient_curve) + ifcopenshell.api.alignment.map_alignment_segments(file, layouts[2], segmented_reference_curve) + representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Curve3D", + Items=(segmented_reference_curve,), + ) + ifcopenshell.api.geometry.assign_representation(file, alignment, representation) + else: + # Reusing Horizontal - CT 4.1.4.4.1.2 + # Create a representation on the parent alignment + composite_curve = file.createIfcCompositeCurve() + ifcopenshell.api.alignment.map_alignment_segments(file, layouts[0], composite_curve) + representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="FootPrint", + RepresentationType="Curve2D", + Items=(composite_curve,), + ) + ifcopenshell.api.geometry.assign_representation(file, alignment, representation) + + for child_alignment in children: + child_alignment.ObjectPlacement = placement + child_layouts = ifcopenshell.api.alignment.get_alignment_layouts(child_alignment) + if len(child_layouts) == 1: + assert child_layouts[0].is_a("IfcAlignmentVertical") + base_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + gradient_curve = file.createIfcGradientCurve(BaseCurve=base_curve) + ifcopenshell.api.alignment.map_alignment_segments(file, child_layouts[0], gradient_curve) + representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Curve3D", + Items=(gradient_curve,), + ) + ifcopenshell.api.geometry.assign_representation(file, child_alignment, representation) + elif len(child_layouts) == 2: + assert child_layouts[0].is_a("IfcAlignmentVertical") + assert child_layouts[1].is_a("IfcAlignmentCant") + base_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + gradient_curve = file.createIfcGradientCurve(BaseCurve=base_curve) + ifcopenshell.api.alignment.map_alignment_segments(file, child_layouts[0], gradient_curve) + segmented_reference_curve = file.createIfcSegmentedReferenceCurve(BaseCurve=gradient_curve) + ifcopenshell.api.alignment.map_alignment_segments(file, child_layouts[1], segmented_reference_curve) + representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Curve3D", + Items=(segmented_reference_curve,), + ) + ifcopenshell.api.geometry.assign_representation(file, child_alignment, representation) + else: + assert False # should never get here - can't have more than one vertical and cant in a child alignment diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_horizontal_alignment_by_pi_method.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_horizontal_alignment_by_pi_method.py new file mode 100644 index 0000000000..e7d15c942f --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_horizontal_alignment_by_pi_method.py @@ -0,0 +1,216 @@ +# 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.alignment +from ifcopenshell import entity_instance + +import math +from typing import Sequence + + +def create_horizontal_alignment_by_pi_method( + file: ifcopenshell.file, name: str, hpoints: Sequence[Sequence[float]], radii: Sequence[float] +) -> entity_instance: + """ + Create a horizontal alignment using the PI layout method. + + :param name: value for Name attribute + :param hpoints: (X, Y) pairs denoting the location of the horizontal PIs, including start (POB) and end (POE). + :param radii: radius values to use for transition + :return: Returns a IfcAlignmentHorizontal + """ + if not (len(hpoints) - 2 == len(radii)): + raise ValueError("radii should have two fewer elements that hpoints") + + # Create the horizontal alignment (IfcAlignmentHorizontal) and nest alignment segments + horizontal_alignment = file.create_entity( + type="IfcAlignmentHorizontal", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=f"{name} - Horizontal", + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + ) + + xBT, yBT = hpoints[0] + xPI, yPI = hpoints[1] + + i = 1 + + for radius in radii: + # back tangent + dxBT = xPI - xBT + dyBT = yPI - yBT + angleBT = math.atan2(dyBT, dxBT) + lengthBT = math.sqrt(dxBT * dxBT + dyBT * dyBT) + + # forward tangent + i += 1 + xFT, yFT = hpoints[i] + dxFT = xFT - xPI + dyFT = yFT - yPI + angleFT = math.atan2(dyFT, dxFT) + + delta = angleFT - angleBT + + tangent = abs(radius * math.tan(delta / 2)) + + lc = abs(radius * delta) + + radius *= delta / abs(delta) + + xPC = xPI - tangent * math.cos(angleBT) + yPC = yPI - tangent * math.sin(angleBT) + + xPT = xPI + tangent * math.cos(angleFT) + yPT = yPI + tangent * math.sin(angleFT) + + tangent_run = lengthBT - tangent + + # create back tangent run + pt = file.create_entity( + type="IfcCartesianPoint", + Coordinates=(xBT, yBT), + ) + design_parameters = file.create_entity( + type="IfcAlignmentHorizontalSegment", + StartTag=None, + EndTag=None, + StartPoint=pt, + StartDirection=angleBT, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=tangent_run, + GravityCenterLineHeight=None, + PredefinedType="LINE", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + + ifcopenshell.api.alignment.add_segment_to_layout(file, horizontal_alignment, alignment_segment) + + # create circular curve + if radius != 0.0: + pc = file.create_entity( + type="IfcCartesianPoint", + Coordinates=(xPC, yPC), + ) + design_parameters = file.create_entity( + type="IfcAlignmentHorizontalSegment", + StartTag=None, + EndTag=None, + StartPoint=pc, + StartDirection=angleBT, + StartRadiusOfCurvature=float(radius), + EndRadiusOfCurvature=float(radius), + SegmentLength=lc, + GravityCenterLineHeight=None, + PredefinedType="CIRCULARARC", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + ifcopenshell.api.alignment.add_segment_to_layout(file, horizontal_alignment, alignment_segment) + + xBT = xPT + yBT = yPT + xPI = xFT + yPI = yFT + + # done processing radii + # create last tangent run + dx = xPI - xBT + dy = yPI - yBT + angleBT = math.atan2(dy, dx) + tangent_run = math.sqrt(dx * dx + dy * dy) + pt = file.create_entity(type="IfcCartesianPoint", Coordinates=(xBT, yBT)) + + design_parameters = file.create_entity( + type="IfcAlignmentHorizontalSegment", + StartTag=None, + EndTag=None, + StartPoint=pt, + StartDirection=angleBT, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=tangent_run, + GravityCenterLineHeight=None, + PredefinedType="LINE", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + ifcopenshell.api.alignment.add_segment_to_layout(file, horizontal_alignment, alignment_segment) + + # create zero length terminator segment + poe = file.create_entity(type="IfcCartesianPoint", Coordinates=(xPI, yPI)) + + design_parameters = file.create_entity( + type="IfcAlignmentHorizontalSegment", + StartTag="POE", + EndTag="POE", + StartPoint=poe, + StartDirection=angleBT, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=0.0, + GravityCenterLineHeight=None, + PredefinedType="LINE", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + ifcopenshell.api.alignment.add_segment_to_layout(file, horizontal_alignment, alignment_segment) + + return horizontal_alignment diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_segment_representations.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_segment_representations.py new file mode 100644 index 0000000000..a5dc42cfc3 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_segment_representations.py @@ -0,0 +1,76 @@ +# 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.alignment +from ifcopenshell import entity_instance +from ifcopenshell import ifcopenshell_wrapper +import math +from typing import Sequence + + +def create_segment_representations( + file: ifcopenshell.file, + alignment: entity_instance, +) -> None: + """ + Creates curve segment representations for the alignment for IFC CT 4.1.7.1.1.4. The alignment is expected to have representations + for "Axis/Curve2D" (horizontal only) or "FootPrint/Curve2D" and "Axis/Curve3D" (horizontal + vertical/cant). There is the additional + expectation that there is a 1-to-1 relationship between IfcAlignmentSegment and IfcCurveSegment. + That is, no Helmert curves in the alignment which have a 1-to-2 relationship + + :param alignment: The alignment to create segment representations. + """ + expected_type = "IfcAlignment" + if not alignment.is_a(expected_type): + raise TypeError(f"Expected to see type '{expected_type}', instead received '{alignment.is_a()}'.") + + axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file) + representations = ifcopenshell.util.representation.get_representations_iter(alignment) + for representation in representations: + curve = None + nested_alignment = None + if (representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve2D") or ( + representation.RepresentationIdentifier == "FootPrint" and representation.RepresentationType == "Curve2D" + ): + curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + nested_alignment = [ + c for c in ifcopenshell.util.element.get_components(alignment) if c.is_a("IfcAlignmentHorizontal") + ][0] + elif representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve3D": + curve = ifcopenshell.api.alignment.get_curve(alignment) + nested_alignment = [ + c for c in ifcopenshell.util.element.get_components(alignment) if c.is_a("IfcAlignmentVertical") + ][0] + + curve_segments = curve.Segments + segments = nested_alignment.IsNestedBy[0].RelatingObjects + + for curve_segment, alignment_segment in zip(curve_segments, segments): + axis_representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Segment", + Items=(curve_segment,), + ) + product = file.create_entity( + type="IfcProductDefinitionShape", Name=None, Description=None, Representations=(axis_representation,) + ) + alignment_segment.ObjectPlacement = alignment.ObjectPlacement + alignment_segment.Representation = product diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_vertical_alignment_by_pi_method.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_vertical_alignment_by_pi_method.py new file mode 100644 index 0000000000..42652fc9b7 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_vertical_alignment_by_pi_method.py @@ -0,0 +1,194 @@ +# 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.alignment +from ifcopenshell import entity_instance + +import math +from typing import Sequence + + +def create_vertical_alignment_by_pi_method( + file: ifcopenshell.file, name: str, vpoints: Sequence[Sequence[float]], lengths: Sequence[float] +) -> entity_instance: + """ + Create a vertical alignment using the PI layout method. + + :param name: value for Name attribute + :param base_curve: base curve representing the 2D projection of the gradient curve + :param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end. + :param lengths: horizontal length of parabolic vertical curves + :return: IfcAlignmentHorizontal + """ + if not (len(vpoints) - 2 == len(lengths)): + raise ValueError("lengths should have two fewer elements that vpoints") + + # Create the vertical alignment (IfcAlignmentVertical) and nest alignment segments + vertical_alignment = file.create_entity( + type="IfcAlignmentVertical", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=f"{name} - Vertical", + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + ) + + xPBG, yPBG = vpoints[0] + xPVI, yPVI = vpoints[1] + i = 1 + for length in lengths: + # back gradient + dxBG = xPVI - xPBG + dyBG = yPVI - yPBG + start_slope = math.tan(math.atan2(dyBG, dxBG)) + + # forward gradient + i += 1 + xPFG, yPFG = vpoints[i] + dxFG = xPFG - xPVI + dyFG = yPFG - yPVI + end_slope = math.tan(math.atan2(dyFG, dxFG)) + + xEVC = xPVI + length / 2.0 + yEVC = yPVI + end_slope * length / 2.0 + + # create gradient + gradient_length = dxBG - length / 2.0 + design_parameters = file.create_entity( + type="IfcAlignmentVerticalSegment", + StartTag=None, + EndTag=None, + StartDistAlong=xPBG, + HorizontalLength=gradient_length, + StartHeight=yPBG, + StartGradient=start_slope, + EndGradient=start_slope, + RadiusOfCurvature=None, + PredefinedType="CONSTANTGRADIENT", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + ifcopenshell.api.alignment.add_segment_to_layout(file, vertical_alignment, alignment_segment) + + # create vertical curve + if 0.0 < length: + k = (end_slope - start_slope) / length + xBVC = xPVI - length / 2.0 + yBVC = yPVI - start_slope * length / 2.0 + + design_parameters = file.create_entity( + type="IfcAlignmentVerticalSegment", + StartTag=None, + EndTag=None, + StartDistAlong=xBVC, + HorizontalLength=length, + StartHeight=yBVC, + StartGradient=start_slope, + EndGradient=end_slope, + RadiusOfCurvature=1 / k, + PredefinedType="PARABOLICARC", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + ifcopenshell.api.alignment.add_segment_to_layout(file, vertical_alignment, alignment_segment) + + # start of next curve is end of this curve + xPBG = xEVC + yPBG = yEVC + xPVI = xPFG + yPVI = yPFG + + # create last gradient run + dx = xPVI - xPBG + dy = yPVI - yPBG + slope = math.tan(math.atan2(dy, dx)) + gradient_length = dx + + design_parameters = file.create_entity( + type="IfcAlignmentVerticalSegment", + StartTag=None, + EndTag=None, + StartDistAlong=xPBG, + HorizontalLength=gradient_length, + StartHeight=yPBG, + StartGradient=slope, + EndGradient=slope, + RadiusOfCurvature=None, + PredefinedType="CONSTANTGRADIENT", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + ifcopenshell.api.alignment.add_segment_to_layout(file, vertical_alignment, alignment_segment) + + # create zero length terminator segment + design_parameters = file.create_entity( + type="IfcAlignmentVerticalSegment", + StartTag="VPOE", + EndTag="VPOE", + StartDistAlong=xPVI, + HorizontalLength=0.0, + StartHeight=yPVI, + StartGradient=slope, + EndGradient=slope, + RadiusOfCurvature=None, + PredefinedType="CONSTANTGRADIENT", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + ifcopenshell.api.alignment.add_segment_to_layout(file, vertical_alignment, alignment_segment) + + return vertical_alignment diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_alignment_layouts.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_alignment_layouts.py new file mode 100644 index 0000000000..929634f6b9 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_alignment_layouts.py @@ -0,0 +1,41 @@ +# 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.util +from ifcopenshell import entity_instance +from typing import Sequence + +import ifcopenshell.util.representation + + +def get_alignment_layouts(alignment: entity_instance) -> Sequence[entity_instance]: + """ + Returns the layout alignments nested to this alignment + """ + layouts = [] + for rel in alignment.IsNestedBy: + for layout in rel.RelatedObjects: + if ( + layout.is_a("IfcAlignmentHorizontal") + or layout.is_a("IfcAlignmentVertical") + or layout.is_a("IfcAlignmentCant") + ): + layouts.append(layout) + + return layouts diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_axis_subcontext.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_axis_subcontext.py new file mode 100644 index 0000000000..033c94d9bb --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_axis_subcontext.py @@ -0,0 +1,40 @@ +# 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.util.representation +import ifcopenshell.api.context +from ifcopenshell import entity_instance + + +def get_axis_subcontext(file: ifcopenshell.file) -> entity_instance: + """ + Returns the IfcGeometricRepresentationSubContext for Model, Axis, MODEL_VIEW. If one does not exist, it is created. + """ + axis_geom_subcontext = ifcopenshell.util.representation.get_context(file, "Model", "Axis", "MODEL_VIEW") + if axis_geom_subcontext == None: + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_geom_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + return axis_geom_subcontext diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_basis_curve.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_basis_curve.py new file mode 100644 index 0000000000..b59d623f26 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_basis_curve.py @@ -0,0 +1,51 @@ +# 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.util +from ifcopenshell import entity_instance +from typing import Sequence + +import ifcopenshell.util.representation + + +def get_basis_curve(alignment: entity_instance) -> entity_instance: + """ + Returns the basis curve for an alignment. This curve is the geometric representation that is used + as the basis curve for vertical and cant alignments. + + :param alignment: The alignment + :return: The geometric representation that is used as a basis curve, typically an IfcCompositeCurve, or None if the alignment does not have a representation + + Example: + + .. code:: python + alignment = model.by_type("IfcAlignment")[0] + composite_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + """ + axis = None + + representations = ifcopenshell.util.representation.get_representations_iter(alignment) + for representation in representations: + if (representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve2D") or ( + representation.RepresentationIdentifier == "FootPrint" and representation.RepresentationType == "Curve2D" + ): + axis = representation + break + + return None if axis == None or axis.Items == None or len(axis.Items) == 0 else axis.Items[0] diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_child_alignments.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_child_alignments.py new file mode 100644 index 0000000000..7e80477a03 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_child_alignments.py @@ -0,0 +1,44 @@ +# 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.util +from ifcopenshell import entity_instance +from typing import Sequence + +import ifcopenshell.util.element + + +def get_child_alignments(alignment: entity_instance) -> Sequence[entity_instance]: + """ + Returns the aggregated child alignments to this alignment + + Example: + + .. code:: python + + alignment = model.by_type("IfcAlignment")[0] + children = ifcopenshell.api.alignment.get_child_alignments(alignment) + """ + children = [] + for rel in alignment.IsDecomposedBy: + for child in rel.RelatedObjects: + if child.is_a("IfcAlignment"): + children.append(child) + + return children diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_curve.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_curve.py new file mode 100644 index 0000000000..c2889bdc3f --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_curve.py @@ -0,0 +1,52 @@ +# 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.util +from ifcopenshell import entity_instance +from typing import Sequence + +import ifcopenshell.util.representation + + +def get_curve(alignment: entity_instance) -> entity_instance: + """ + Returns the geometric representation curve for an alignment. + A horizontal only will have a curve of type IfcCompositeCurve + A horizontal+vertical will have a curve of type IfcGradientCurve + A horizontal+vertical+cant will have a curve of tyep IfcSegmentedReferenceCurve + + :param alignment: The alignment + :return: The geometric representation of the alignemnt or None if the alignment does not have a representation + + Example: + + .. code:: python + alignment = model.by_type("IfcAlignment")[0] + gradient_curve = ifcopenshell.api.alignment.get_curve(alignment) + """ + axis = None + representations = ifcopenshell.util.representation.get_representations_iter(alignment) + for representation in representations: + if representation.RepresentationIdentifier == "Axis" and ( + representation.RepresentationType == "Curve2D" or representation.RepresentationType == "Curve3D" + ): + axis = representation + break + + return None if axis == None or len(axis.Items) == 0 else axis.Items[0] diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_parent_alignment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_parent_alignment.py new file mode 100644 index 0000000000..dfd6fb8629 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_parent_alignment.py @@ -0,0 +1,45 @@ +# 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.util +from ifcopenshell import entity_instance +from typing import Sequence + +import ifcopenshell.util.representation + + +def get_parent_alignment(alignment: entity_instance) -> entity_instance: + """ + Returns the parent alignment. When multiple vertical alignments share a horizontal alignment + the horizontal alignment is nested to the parent alignment, a child alignment is aggregated + to the parent alignment for each vertical alignment, and the vertical alignment is nested with + its child alignment. + + Example: + + .. code:: python + alignment = model.by_type("IfcAlignment")[0] + parent = ifcopenshell.api.alignment.get_parent_alignment(alignment) + """ + + for rel in alignment.Decomposes: + if rel.RelatingObject.is_a("IfcAlignment"): + return rel.RelatingObject + + return None diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/has_zero_length_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/has_zero_length_segment.py new file mode 100644 index 0000000000..2b26cb6fe7 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/has_zero_length_segment.py @@ -0,0 +1,61 @@ +# 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.alignment +import ifcopenshell.util.element +from ifcopenshell import entity_instance + + +def has_zero_length_segment(entity: entity_instance) -> bool: + """ + Returns true if the entity ends with a zero length segment. If the entity is an IfcCompositeCurve the IfcCurveSegment.Transition must be DISCONTINUOUS + + :param entity: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant or IfcCompositeCurve + :return: True if the zero length segment is present + """ + expected_types = [ + "IfcAlignmentHorizontal", + "IfcAlignmentVertical", + "IfcAlignmentCant", + "IfcCompositeCurve", + "IfcGradientCurve", + "IfcSegmentedReferenceCurve", + ] + if not entity.is_a() in expected_types: + raise TypeError( + f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{entity.is_a()}" + ) + + if entity.is_a("IfcCompositeCurve"): + last_segment = entity.Segments[-1] + return last_segment.Transition == "DISCONTINUOUS" and last_segment.SegmentLength.wrappedValue == 0.0 + else: + segments = ifcopenshell.util.element.get_components(entity) + for rel in entity.IsNestedBy: + if 0 < len(rel.RelatedObjects): + last_segment = rel.RelatedObjects[-1] + if last_segment.is_a("IfcAlignmentSegment"): + if last_segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"): + return last_segment.DesignParameters.SegmentLength == 0.0 + elif last_segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"): + return last_segment.DesignParameters.HorizontalLength == 0.0 + elif last_segment.DesignParameters.is_a("IfcAlignmentCantSegment"): + return last_segment.DesignParameters.HorizontalLength == 0.0 + + return False diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_cant_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_cant_segment.py new file mode 100644 index 0000000000..b911fc6b45 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_cant_segment.py @@ -0,0 +1,84 @@ +# 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 +from ifcopenshell import entity_instance +from ifcopenshell.api.alignment import get_axis_subcontext +from typing import Sequence + + +def _map_constant_cant(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("CONSTANTCANT not implemented") + + +def _map_linear_transition(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("LINEARTRANSTION not implemented") + + +def _map_helmert_curve(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("HELMERTCURVE not implemented") + + +def _map_bloss_curve(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("BLOSSCURVE not implemented") + + +def _map_cosine_curve(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("COSINECURVE not implemented") + + +def _map_sine_curve(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("SINECURVE not implemented") + + +def _map_viennese_bend(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("VIENNESEBEND not implemented") + + +def map_alignment_cant_segment( + file: ifcopenshell.file, design_parameters: entity_instance +) -> Sequence[entity_instance]: + """ + Creates IfcCurveSegment entities for the represention of the supplied IfcAlignmentCantSegment business logic entity instance. + A pair of entities is returned because a single business logic segment of type HELMERTCURVE maps to two representaiton entities. + + The IfcCurveSegment.Transition transition code is set to DISCONTINUOUS. + """ + expected_type = "IfcAlignmentCantSegment" + if not design_parameters.is_a(expected_type): + raise TypeError(f"Expected to see type '{expected_type}', instead received '{design_parameters.is_a()}'.") + + match design_parameters.PredefinedType: + case "CONSTANTCANT": + result = _map_constant_cant(file, design_parameters) + case "LINEARTRANSITION": + result = _map_linear_transition(file, design_parameters) + case "HELMERTCURVE": + result = _map_helmert_curve(file, design_parameters) + case "BLOSSCURVE": + result = _map_bloss_curve(file, design_parameters) + case "COSINECURVE": + result = _map_cosine_curve(file, design_parameters) + case "SINECURVE": + result = _map_sine_curve(file, design_parameters) + case "VIENNESEBEND": + result = _map_viennese_bend(file, design_parameters) + case _: + raise TypeError("Unexpected predefined type") + + return result diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_horizontal_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_horizontal_segment.py new file mode 100644 index 0000000000..bb86ea6bbb --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_horizontal_segment.py @@ -0,0 +1,439 @@ +# 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 +from ifcopenshell import entity_instance +import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper +from typing import Sequence +import math + + +def _get_curve_factor(design_parameters: entity_instance) -> float: + start_radius = design_parameters.StartRadiusOfCurvature + end_radius = design_parameters.EndRadiusOfCurvature + length = design_parameters.SegmentLength + + f = (0.0 if end_radius == 0.0 else length / end_radius) - (0.0 if start_radius == 0.0 else length / start_radius) + return f + + +def _map_line(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_point = design_parameters.StartPoint + start_direction = design_parameters.StartDirection + length = design_parameters.SegmentLength + + transition = "DISCONTINUOUS" + + parent_curve = file.create_entity( + type="IfcLine", + Pnt=file.create_entity( + type="IfcCartesianPoint", + Coordinates=(0.0, 0.0), + ), + Dir=file.create_entity( + type="IfcVector", + Orientation=file.create_entity( + type="IfcDirection", + DirectionRatios=(1.0, 0.0), + ), + Magnitude=1.0, + ), + ) + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=file.createIfcDirection( + (math.cos(start_direction), math.sin(start_direction)), + ), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(length), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_circular_arc(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_point = design_parameters.StartPoint + start_direction = design_parameters.StartDirection + start_radius = design_parameters.StartRadiusOfCurvature + length = design_parameters.SegmentLength + + transition = "DISCONTINUOUS" + + parent_curve = file.createIfcCircle( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((0.0, 0.0)), + RefDirection=file.createIfcDirection((1.0, 0.0)), + ), + Radius=math.fabs(start_radius), + ) + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.createIfcAxis2Placement2D( + Location=start_point, + RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(length * (start_radius / math.fabs(start_radius))), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_clothoid(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_point = design_parameters.StartPoint + start_direction = design_parameters.StartDirection + start_radius = design_parameters.StartRadiusOfCurvature + end_radius = design_parameters.EndRadiusOfCurvature + length = design_parameters.SegmentLength + + transition = "DISCONTINUOUS" + + f = _get_curve_factor(design_parameters) + A = (length / math.sqrt(math.fabs(f))) * (f / math.fabs(f)) + parent_curve = file.createIfcClothoid( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((0.0, 0.0)), + RefDirection=file.createIfcDirection((1.0, 0.0)), + ), + ClothoidConstant=A, + ) + + if (math.fabs(start_radius) < math.fabs(end_radius) and start_radius != 0.0) or end_radius == 0.0: + offset = -length - (length * start_radius / (end_radius - start_radius) if end_radius != 0.0 else 0.0) + else: + offset = length * end_radius / (start_radius - end_radius) if start_radius != 0.0 else 0.0 + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + SegmentStart=file.createIfcLengthMeasure(offset), + SegmentLength=file.createIfcLengthMeasure(length), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_cubic(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_point = design_parameters.StartPoint + start_direction = design_parameters.StartDirection + start_radius = design_parameters.StartRadiusOfCurvature + end_radius = design_parameters.EndRadiusOfCurvature + length = design_parameters.SegmentLength + + transition = "DISCONTINUOUS" + + offset = 0.0 + A0 = 0.0 # constant term + A1 = 0.0 # linear term + A2 = 0.0 # quadratic term + A3 = 0.0 # cubic term + + if end_radius != 0.0 and start_radius != 0.0 and end_radius != start_radius: + f = (start_radius - end_radius) / end_radius # note, this "f" is different that _get_curve_factor computes + A3 = f / (6.0 * start_radius * length) + offset = length / f + elif end_radius != 0.0: + A3 = 1.0 / (6.0 * end_radius * length) + offset = 0.0 + elif start_radius != 0.0: + A3 = -1.0 / (6.0 * start_radius * length) + offset = -length + + parent_curve = file.createIfcPolynomialCurve( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((0.0, 0.0)), + RefDirection=file.createIfcDirection((1.0, 0.0)), + ), + CoefficientsX=(0.0, 1.0), + CoefficientsY=(A0, A1, A2, A3), + ) + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + SegmentStart=file.createIfcLengthMeasure(offset), + SegmentLength=file.createIfcLengthMeasure(length), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_helmert_curve(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_point = design_parameters.StartPoint + start_direction = design_parameters.StartDirection + start_radius = design_parameters.StartRadiusOfCurvature + end_radius = design_parameters.EndRadiusOfCurvature + length = design_parameters.SegmentLength + + transition = "DISCONTINUOUS" + f = _get_curve_factor(design_parameters) + + a0_1 = 0.0 * f + length / start_radius if start_radius != 0 else 0.0 # constant term, first half + a1_1 = 0.0 * f # linear term, first half + a2_1 = 2.0 * f # quadratic term, first half + + A0_1 = length * math.pow(math.fabs(a0_1), -1.0 / 1.0) * a0_1 / math.fabs(a0_1) if a0_1 != 0.0 else 0.0 + A1_1 = length * math.pow(math.fabs(a1_1), -1.0 / 2.0) * a1_1 / math.fabs(a1_1) if a1_1 != 0.0 else 0.0 + A2_1 = length * math.pow(math.fabs(a2_1), -1.0 / 3.0) * a2_1 / math.fabs(a2_1) if a2_1 != 0.0 else 0.0 + + x1, y1, angle1 = ifcopenshell_wrapper.helmert_curve_point(A0_1, A1_1, A2_1, length / 2) + + parent_curve1 = file.createIfcSecondOrderPolynomialSpiral( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((0.0, 0.0)), RefDirection=file.createIfcDirection((1.0, 0.0)) + ), + QuadraticTerm=A2_1, + LinearTerm=A1_1 if A1_1 != 0.0 else None, + ConstantTerm=A0_1 if A0_1 != 0.0 else None, + ) + + curve_segment1 = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(length / 2), + ParentCurve=parent_curve1, + ) + + a0_2 = -1.0 * f + (length / start_radius if start_radius != 0.0 else 0.0) # constant term, second half + a1_2 = 4.0 * f # linear term, second half + a2_2 = -2.0 * f # quadratic term, second half + + A0_2 = length * math.pow(math.fabs(a0_2), -1.0 / 1.0) * (a0_2 / math.fabs(a0_2)) if a0_2 != 0.0 else 0.0 + A1_2 = length * math.pow(math.fabs(a1_2), -1.0 / 2.0) * (a1_2 / math.fabs(a1_2)) if a1_2 != 0.0 else 0.0 + A2_2 = length * math.pow(math.fabs(a2_2), -1.0 / 3.0) * (a2_2 / math.fabs(a2_2)) if a2_2 != 0.0 else 0.0 + + x2, y2, angle2 = ifcopenshell_wrapper.helmert_curve_point(A0_2, A1_2, A2_2, length / 2) + anglep = angle1 - angle2 + xp = x1 - x2 * math.cos(anglep) + y2 * math.sin(anglep) + yp = y1 - x2 * math.sin(anglep) - y2 * math.cos(anglep) + + parent_curve2 = file.createIfcSecondOrderPolynomialSpiral( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((xp, yp)), + RefDirection=file.createIfcDirection((math.cos(anglep), math.sin(anglep))), + ), + QuadraticTerm=A2_2, + LinearTerm=A1_2 if A1_2 != 0.0 else None, + ConstantTerm=A0_2 if A0_2 != 0.0 else None, + ) + + curve_segment2 = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=file.createIfcCartesianPoint((x1, y1)), + RefDirection=file.createIfcDirection((math.cos(angle1), math.sin(angle1))), + ), + SegmentStart=file.createIfcLengthMeasure(length / 2), + SegmentLength=file.createIfcLengthMeasure(length / 2), + ParentCurve=parent_curve2, + ) + + return curve_segment1, curve_segment2 + + +def _map_bloss_curve(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_point = design_parameters.StartPoint + start_direction = design_parameters.StartDirection + start_radius = design_parameters.StartRadiusOfCurvature + length = design_parameters.SegmentLength + + transition = "DISCONTINUOUS" + f = _get_curve_factor(design_parameters) + + a0 = length / start_radius if start_radius != 0.0 else 0.0 # constant term + a1 = 0.0 # linear term + a2 = 3.0 * f # quadratic term + a3 = -2.0 * f # cubic term + + A0 = length * math.pow(math.fabs(a0), -1.0 / 1.0) * (a0 / math.fabs(a0)) if a0 != 0.0 else 0.0 + A1 = length * math.pow(math.fabs(a1), -1.0 / 2.0) * (a1 / math.fabs(a1)) if a1 != 0.0 else 0.0 + A2 = length * math.pow(math.fabs(a2), -1.0 / 3.0) * (a2 / math.fabs(a2)) if a2 != 0.0 else 0.0 + A3 = length * math.pow(math.fabs(a3), -1.0 / 4.0) * (a3 / math.fabs(a3)) if a3 != 0.0 else 0.0 + + parent_curve = file.createIfcThirdOrderPolynomialSpiral( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((0.0, 0.0)), RefDirection=file.createIfcDirection((1.0, 0.0)) + ), + CubicTerm=A3, + QuadraticTerm=A2 if A2 != 0.0 else None, + LinearTerm=A1 if A1 != 0.0 else None, + ConstantTerm=A0 if A0 != 0.0 else None, + ) + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(length), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_cosine_curve(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_point = design_parameters.StartPoint + start_direction = design_parameters.StartDirection + start_radius = design_parameters.StartRadiusOfCurvature + length = design_parameters.SegmentLength + + transition = "DISCONTINUOUS" + + f = _get_curve_factor(design_parameters) + + a0 = 0.5 * f + (length / start_radius if start_radius != 0.0 else 0.0) + a1 = -0.5 * f + + A0 = length * math.pow(math.fabs(a0), -1.0 / 1.0) * (a0 / math.fabs(a0)) if a0 != 0.0 else 0.0 + A1 = length * math.pow(math.fabs(a1), -1.0 / 1.0) * (a1 / math.fabs(a1)) if a1 != 0.0 else 0.0 + + parent_curve = file.createIfcCosineSpiral( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((0.0, 0.0)), + RefDirection=file.createIfcDirection((1.0, 0.0)), + ), + CosineTerm=A1, + ConstantTerm=(A0 if A0 != 0.0 else None), + ) + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(length), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_sine_curve(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_point = design_parameters.StartPoint + start_direction = design_parameters.StartDirection + start_radius = design_parameters.StartRadiusOfCurvature + length = design_parameters.SegmentLength + + transition = "DISCONTINUOUS" + + f = _get_curve_factor(design_parameters) + a0 = length / start_radius if start_radius != 0.0 else 0.0 + a1 = f + a2 = -f / (2.0 * math.pi) + + A0 = length * math.pow(math.fabs(a0), -1.0 / 1.0) * (a0 / math.fabs(a0)) if a0 != 0.0 else 0.0 + A1 = length * math.pow(math.fabs(a1), -1.0 / 2.0) * (a1 / math.fabs(a1)) if a1 != 0.0 else 0.0 + A2 = length * math.pow(math.fabs(a2), -1.0 / 1.0) * (a2 / math.fabs(a2)) if a2 != 0.0 else 0.0 + + parent_curve = file.createIfcSineSpiral( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((0.0, 0.0)), + RefDirection=file.createIfcDirection((1.0, 0.0)), + ), + SineTerm=A2, + LinearTerm=(A1 if A1 != 0.0 else None), + ConstantTerm=(A0 if A0 != 0.0 else None), + ) + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(length), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_viennese_bend(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("VIENNESEBEND not implemented") + + +def map_alignment_horizontal_segment( + file: ifcopenshell.file, design_parameters: entity_instance +) -> Sequence[entity_instance]: + """ + Creates IfcCurveSegment entities for the represention of the supplied IfcAlignmentHorizontalSegment business logic entity instance. + A pair of entities is returned because a single business logic segment of type HELMERTCURVE maps to two representaiton entities. + + The IfcCurveSegment.Transition transition code is set to DISCONTINUOUS + """ + expected_type = "IfcAlignmentHorizontalSegment" + if not design_parameters.is_a(expected_type): + raise TypeError(f"Expected to see type '{expected_type}', instead received '{design_parameters.is_a()}'.") + + match design_parameters.PredefinedType: + case "LINE": + result = _map_line(file, design_parameters) + case "CIRCULARARC": + result = _map_circular_arc(file, design_parameters) + case "CLOTHOID": + result = _map_clothoid(file, design_parameters) + case "CUBIC": + result = _map_cubic(file, design_parameters) + case "HELMERTCURVE": + result = _map_helmert_curve(file, design_parameters) + case "BLOSSCURVE": + result = _map_bloss_curve(file, design_parameters) + case "COSINECURVE": + result = _map_cosine_curve(file, design_parameters) + case "SINECURVE": + result = _map_sine_curve(file, design_parameters) + case "VIENNESEBEND": + result = _map_viennese_bend(file, design_parameters) + case _: + raise TypeError("Unexpected predefined type") + + return result diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_segment.py new file mode 100644 index 0000000000..d7b05d5544 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_segment.py @@ -0,0 +1,47 @@ +# 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 +from ifcopenshell import entity_instance +from typing import Sequence + + +def map_alignment_segment(file: ifcopenshell.file, segment: entity_instance) -> Sequence[entity_instance]: + """ + Creates IfcCurveSegment entities for the represention of the supplied IfcAlignmentSegment business logic entity instance. + A pair of entities is returned because a single business logic segment of type HELMERTCURVE maps to two representaiton entities. + + The IfcCurveSegment.Transition transition code is set to DISCONTINUOUS, except for the transition between helmert curve segments. + + This function will evaluate the IfcAlignmentSegment.DesignParameters type and call the correct lower level mapping function. + """ + expected_type = "IfcAlignmentSegment" + if not segment.is_a(expected_type): + raise TypeError(f"Expected to see type '{expected_type}', instead received '{segment.is_a()}'.") + + if segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"): + return ifcopenshell.api.alignment.map_alignment_horizontal_segment(file, segment.DesignParameters) + elif segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"): + return ifcopenshell.api.alignment.map_alignment_vertical_segment(file, segment.DesignParameters) + elif segment.DesignParameters.is_a("IfcAlignmentCantSegment"): + return ifcopenshell.api.alignment.map_alignment_cant_segment(file, segment.DesignParameters) + else: + raise TypeError("Unexpected type for segment.DesignParameters") + + return (None, None) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_segments.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_segments.py new file mode 100644 index 0000000000..d9012d59ef --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_segments.py @@ -0,0 +1,62 @@ +# 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.alignment +from ifcopenshell import entity_instance +from typing import Sequence + + +def map_alignment_segments( + file: ifcopenshell.file, alignment: entity_instance, composite_curve: entity_instance +) -> None: + """ + Creates IfcCurveSegment entities for the supplied alignment business logic entity instance and assigns them to the composite curve. + End-Start points of adjacent segments are evaluated and the IfcCurveSegment.Transition is set. + + This function does not create an IfcShapeRepresentation. Use create_geometric_representation to create all the representations + for an alignment. This function only populates the composite curve with IfcCurveSegment entities. + + :param alignment: The business logic alignment, expected to be IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant + :param composite_curve: The IfcCompositeCurve (or subclass) which will receive the IfcCurveSegment + :return: None + """ + expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"] + if not alignment.is_a() in expected_types: + raise TypeError( + f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{alignment.is_a()}" + ) + + if alignment.is_a("IfcAlignmentHorizontal") and not composite_curve.is_a("IfcCompositeCurve"): + raise TypeError(f"Expected to see IfcCompositeCurve, instead received '{composite_curve.is_a()}'.") + elif alignment.is_a("IfcAlignmentVertical") and not composite_curve.is_a("IfcGradientCurve"): + raise TypeError(f"Expected to see IfcGradientCurve, instead received '{composite_curve.is_a()}'.") + elif alignment.is_a("IfcAlignmentCant") and not composite_curve.is_a("IfcSegmentedReferenceCurve"): + raise TypeError(f"Expected to see IfcSegmentedReferenceCurve, instead received '{composite_curve.is_a()}'.") + + settings = ifcopenshell.geom.settings() + + composite_curve.SelfIntersect = False + + for rel_nests in alignment.IsNestedBy: + for layout in rel_nests.RelatedObjects: + if layout.is_a("IfcLinearElement"): + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, layout) + for mapped_segment in mapped_segments: + if mapped_segment: + ifcopenshell.api.alignment.add_segment_to_curve(file, mapped_segment, composite_curve) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_vertical_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_vertical_segment.py new file mode 100644 index 0000000000..3e80f3c6f3 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_vertical_segment.py @@ -0,0 +1,225 @@ +# 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 +from ifcopenshell import ifcopenshell_wrapper +from ifcopenshell import entity_instance +from typing import Sequence +import math + + +def _polynomial_length(A: float, B: float, C: float, L: float) -> float: + # closed form solultion for length of parabolic curve. + # see https://www.integral-table.com, equation #37 + # Parabolic curve equation: y = A + Bx + Cx^2 + # y' = B + 2Cx + # Length of a curve = Integral[0,L]( (y')^2 + 1) dx) + # y'^2 = 4C^2x^2 + 4BCx + B^2 + # Substituting, Length of a curve = Integral[0,L]( (4C^2)x^2 + (4BC)x + (B^2 + 1)) dx) + # for eq. #37 cited above, a = 4C^2, b = 4BC, c = B^2 + 1 + a = 4.0 * C * C + b = 4.0 * B * C + c = B * B + 1 + + v1 = lambda a, b, c, x: (b + 2.0 * a * x) / (4.0 * a) + v2 = lambda a, b, c, x: math.sqrt(a * x * x + b * x + c) + v3 = lambda a, b, c, x: (4.0 * a * c - b * b) / (8.0 * math.pow(a, 1.5)) + v4 = lambda a, b, c, x: math.log(math.fabs(2.0 * a * x + b + 2.0 * math.sqrt(a * (a * x * x + b * x + c)))) + + fn = lambda a, b, c, x: v1(a, b, c, x) * v2(a, b, c, x) + v3(a, b, c, x) * v4(a, b, c, x) + + curve_length = fn(a, b, c, L) - fn( + a, b, c, 0 + ) # remember when evaluating an integral, it must be evaluated at end points (L and 0) + return curve_length + + +def _map_constant_gradient(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_distance_along = design_parameters.StartDistAlong + horizontal_length = design_parameters.HorizontalLength + start_height = design_parameters.StartHeight + start_gradient = design_parameters.StartGradient + end_gradient = design_parameters.EndGradient + radius_of_curvature = design_parameters.RadiusOfCurvature + transition = "DISCONTINUOUS" + + parent_curve = file.create_entity( + type="IfcLine", + Pnt=file.create_entity( + type="IfcCartesianPoint", + Coordinates=(0.0, 0.0), + ), + Dir=file.create_entity( + type="IfcVector", + Orientation=file.create_entity( + type="IfcDirection", + DirectionRatios=(1.0, 0.0), + ), + Magnitude=1.0, + ), + ) + + dx = math.cos(math.atan(start_gradient)) + dy = math.sin(math.atan(start_gradient)) + curve_segment_length = horizontal_length / dx + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=file.create_entity(type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height)), + RefDirection=file.createIfcDirection((dx, dy)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(curve_segment_length), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_parabolic_arc(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_distance_along = design_parameters.StartDistAlong + horizontal_length = design_parameters.HorizontalLength + start_height = design_parameters.StartHeight + start_gradient = design_parameters.StartGradient + end_gradient = design_parameters.EndGradient + radius_of_curvature = design_parameters.RadiusOfCurvature + transition = "DISCONTINUOUS" + + A = start_height + B = start_gradient + C = (end_gradient - start_gradient) / (2.0 * horizontal_length) + + parent_curve = file.create_entity( + type="IfcPolynomialCurve", + Position=file.create_entity( + type="IfcAxis2Placement2D", + Location=file.create_entity(type="IfcCartesianPoint", Coordinates=(0.0, 0.0)), + RefDirection=file.createIfcDirection( + (1.0, 0.0), + ), + ), + CoefficientsX=(0.0, 1.0), + CoefficientsY=(A, B, C), + ) + + dx = math.cos(math.atan(start_gradient)) + dy = math.sin(math.atan(start_gradient)) + curve_segment_length = _polynomial_length(A, B, C, horizontal_length) + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=file.create_entity(type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height)), + RefDirection=file.createIfcDirection((dx, dy)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(curve_segment_length), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_circular_arc(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_distance_along = design_parameters.StartDistAlong + horizontal_length = design_parameters.HorizontalLength + start_height = design_parameters.StartHeight + start_gradient = design_parameters.StartGradient + end_gradient = design_parameters.EndGradient + radius_of_curvature = design_parameters.RadiusOfCurvature + transition = "DISCONTINUOUS" + + start_angle = math.atan(start_gradient) + end_angle = math.atan(end_gradient) + dx = math.cos(start_angle) + dy = math.sin(start_angle) + if start_angle < end_angle: + radius = horizontal_length / (math.sin(end_angle) - math.sin(start_angle)) + x = -radius * math.sin(start_angle) + y = radius * math.cos(start_angle) + start_angle += 3.0 * math.pi / 2.0 + end_angle += 3.0 * math.pi / 2.0 + else: + radius = horizontal_length / (math.sin(start_angle) - math.sin(end_angle)) + x = radius * math.sin(start_angle) + y = -radius * math.cos(start_angle) + start_angle += math.pi / 2.0 + end_angle += math.pi / 2.0 + + parent_curve = file.createIfcCircle( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((x, y)), + RefDirection=file.createIfcDirection((1.0, 0.0)), + ), + Radius=radius, + ) + + segment_curve_length = radius * math.fabs(end_angle - start_angle) + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((start_distance_along, start_height)), + RefDirection=file.createIfcDirection( + (dx, dy), + ), + ), + SegmentStart=file.createIfcLengthMeasure(radius * start_angle), + SegmentLength=file.createIfcLengthMeasure(radius * (end_angle - start_angle)), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_clothoid(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("mapping for IfcVerticalSegment.CLOTHOID not implemented") + + +def map_alignment_vertical_segment( + file: ifcopenshell.file, design_parameters: entity_instance +) -> Sequence[entity_instance]: + """ + Creates IfcCurveSegment entities for the represention of the supplied IfcAlignmentVerticalSegment business logic entity instance. + A pair of entities is returned for consistency with map_alignment_horizontal_segment and map_alignment_cant_segment. + + """ + expected_type = "IfcAlignmentVerticalSegment" + if not design_parameters.is_a(expected_type): + raise TypeError(f"Expected to see type '{expected_type}', instead received '{design_parameters.is_a()}'.") + + match design_parameters.PredefinedType: + case "CONSTANTGRADIENT": + result = _map_constant_gradient(file, design_parameters) + + case "PARABOLICARC": + result = _map_parabolic_arc(file, design_parameters) + + case "CIRCULARARC": + result = _map_circular_arc(file, design_parameters) + + case "CLOTHOID": + result = _map_clothoid(file, design_parameters) + + case _: + raise TypeError("Unexpected predefined type") + + return result diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/name_segments.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/name_segments.py new file mode 100644 index 0000000000..41d6ff968f --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/name_segments.py @@ -0,0 +1,42 @@ +# 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 +from ifcopenshell import entity_instance +from typing import Sequence + + +def name_segments(prefix: str, alignment: entity_instance) -> None: + """ + Sets the segment name like ("H1" for horizontal, "V1" for vertical, "C1" for cant) + + :param prefix: The naming prefix + :param alignment: The alignment whose segments are to be named. This should be a IfcAlignmentHorizontal, IfcAlignmentVertical or IfcAlignmentCant + """ + expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"] + if not alignment.is_a() in expected_types: + raise TypeError( + f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{v.is_a()}" + ) + + i = 1 + for rel in alignment.IsNestedBy: + for segment in rel.RelatedObjects: + if segment.is_a("IfcAlignmentSegment"): + segment.Name = f"{prefix}{i}" + i += 1 diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/remove_last_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/remove_last_segment.py new file mode 100644 index 0000000000..3f6d3d8c87 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/remove_last_segment.py @@ -0,0 +1,59 @@ +# 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.alignment +import ifcopenshell.api.nest +import ifcopenshell.geom +import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper +import numpy as np +from ifcopenshell import entity_instance +import ifcopenshell.util +import ifcopenshell.util.element + + +def remove_last_segment(file: ifcopenshell.file, entity: entity_instance) -> entity_instance: + """ + Removes the last segment from the end of entity. + + :param entity: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant or IfcCompositeCurve + :return: The segment + """ + expected_types = [ + "IfcAlignmentHorizontal", + "IfcAlignmentVertical", + "IfcAlignmentCant", + "IfcCompositeCurve", + "IfcGradientCurve", + "IfcSegmentedReferenceCurve", + ] + if not entity.is_a() in expected_types: + raise TypeError( + f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{entity.is_a()}" + ) + + if entity.is_a("IfcCompositeCurve"): + last_segment = entity.Segments[-1] + entity.Segments = tuple(set(entity.Segments) - {last_segment}) + entity.Segments[-1].Transition = "DISCONTINUOUS" + return last_segment + else: + components = ifcopenshell.util.element.get_components(entity) + last_segment = components[-1] + ifcopenshell.api.nest.unassign_object(file, (last_segment,)) + return last_segment diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/remove_zero_length_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/remove_zero_length_segment.py new file mode 100644 index 0000000000..00fa307e49 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/remove_zero_length_segment.py @@ -0,0 +1,35 @@ +# 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.alignment +from ifcopenshell import entity_instance +import ifcopenshell.api.alignment.remove_last_segment + + +def remove_zero_length_segment(file: ifcopenshell.file, entity: entity_instance) -> entity_instance: + """ + Removes the zero length segment from the end of entity. + + :param entity: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant or IfcCompositeCurve + :return: The zero length segment + """ + if not ifcopenshell.api.alignment.has_zero_length_segment(entity): + return None + + return ifcopenshell.api.alignment.remove_last_segment(file, entity) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/update_curve_segment_transition_code.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/update_curve_segment_transition_code.py new file mode 100644 index 0000000000..2eae483eb2 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/update_curve_segment_transition_code.py @@ -0,0 +1,77 @@ +# 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 +from ifcopenshell import ifcopenshell_wrapper +import ifcopenshell.geom +from ifcopenshell import entity_instance +from typing import Sequence +import numpy as np +import math + + +def update_curve_segment_transition_code(prev_segment: entity_instance, segment: entity_instance) -> None: + """ + Updates IfcCurveSegment.Transition of prev_segment based on a comparison of + the position, ref. direction, and curvature at the end of the prev_segment and the start of segment. + """ + expected_type = "IfcCurveSegment" + if not prev_segment.is_a(expected_type): + raise TypeError(f"Expected to see '{expected_type}', instead received '{prev_segment.is_a()}'.") + + if not segment.is_a(expected_type): + raise TypeError(f"Expected to see '{expected_type}', instead received '{segment.is_a()}'.") + + if len(prev_segment.UsingCurves) != 1: + raise TypeError("prev_segment must belong to exactly one curve") + + if len(segment.UsingCurves) != 1: + raise TypeError("segment must belong to exactly one curve") + + if prev_segment.UsingCurves[0] != segment.UsingCurves[0]: + raise TypeError("Both segments must belong to the same curve") + + settings = ifcopenshell.geom.settings() + settings.set("COMPUTE_CURVATURE", True) + + prev_segment_fn = ifcopenshell_wrapper.map_shape(settings, prev_segment.wrapped_data) + prev_segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, prev_segment_fn) + e = prev_segment_evaluator.evaluate(prev_segment_fn.end()) + end = np.array(e) + + # must add the new segment to the container before mapping it, otherwise the segment doesn't + # have enough context to know if it is for horizontal, vertical, cant + + segment_fn = ifcopenshell_wrapper.map_shape(settings, segment.wrapped_data) + segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn) + s = segment_evaluator.evaluate(segment_fn.start()) + start = np.array(s) + + same_position = True if np.allclose(end[:3], start[:3]) else False + same_gradient = True if np.allclose(end[:0], start[:0]) else False + same_curvature = True if np.allclose(end[3:], start[3:]) else False + + if same_position: + prev_segment.Transition = "CONTINUOUS" + if same_gradient: + prev_segment.Transition = "CONTSAMEGRADIENT" + if same_curvature: + prev_segment.Transition = "CONTSAMEGRADIENTSAMECURVATURE" + else: + prev_segment.Transition = "DISCONTINUOUS" diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/util.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/util.py new file mode 100644 index 0000000000..0a07c8a328 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/util.py @@ -0,0 +1,134 @@ +# 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.alignment +import math +from typing import Sequence + +import numpy as np + +import ifcopenshell +import ifcopenshell.geom +import ifcopenshell.guid +import ifcopenshell.template +from ifcopenshell import entity_instance +from ifcopenshell import ifcopenshell_wrapper +import ifcopenshell.util +import ifcopenshell.util.stationing + + +def evaluate_representation(shape_rep: entity_instance, dist_along: float) -> np.ndarray: + """ + Calculate the 4x4 geometric transform at a point on an alignment segment + + :param shape_rep: The representation shape (composite curve, gradient curve, or segmented reference curve) to evaluate + :param dist_along: The distance along this representation at the point of interest (point to be calculated) + """ + supported_rep_types = ["IFCCOMPOSITECURVE", "IFCGRADIENTCURVE", "IFCSEGMENTEDREFERENCECURVE"] + shape_rep_type = shape_rep.is_a().upper() + if not shape_rep_type in supported_rep_types: + raise NotImplementedError( + f"Expected entity type to be one of {[_ for _ in supported_rep_types]}, got '{shape_rep_type}" + ) + + # TODO: confirm point is not beyond limits of alignment + + s = ifcopenshell.geom.settings() + function_item = ifcopenshell_wrapper.map_shape(s, shape_rep.wrapped_data) + evaluator = ifcopenshell_wrapper.function_item_evaluator(s, function_item) + + trans_matrix = evaluator.evaluate(dist_along) + + return np.array(trans_matrix, dtype=np.float64).T + + +def evaluate_segment(segment: entity_instance, dist_along: float) -> np.ndarray: + """ + Calculate the 4x4 geometric transform at a point on an alignment segment + + :param segment: The segment containing the point that we would like to + :param dist_along: The distance along this segment at the point of interest (point to be calculated) + """ + supported_segment_types = ["IFCCURVESEGMENT"] + segment_type = segment.is_a().upper() + if not segment_type in supported_segment_types: + raise NotImplementedError(f"Expected entity type 'IFCCURVESEGMENT', got '{segment_type}") + if dist_along > segment.SegmentLength: + raise ValueError(f"Provided value {dist_along=} is beyond the end of the segment ({segment.SegmentLength}).") + + s = ifcopenshell.geom.settings() + function_item = ifcopenshell_wrapper.map_shape(s, segment.wrapped_data) + evaluator = ifcopenshell_wrapper.function_item_evaluator(s, function_item) + + trans_matrix = evaluator.evaluate(dist_along) + + return np.array(trans_matrix, dtype=np.float64).T + + +def generate_vertices(rep_curve: entity_instance, distance_interval: float = 5.0) -> np.ndarray: + """ + Generate vertices along an alignment + + :param rep_curve: The alignment's representation curve to use to generate vertices. + :param distance_interval: The distance between points along the alignment at which to generate the points + """ + if rep_curve is None: + raise ValueError("Alignment representation not found.") + + supported_rep_types = ["IFCCOMPOSITECURVE", "IFCGRADIENTCURVE", "IFCSEGMENTEDREFERENCECURVE"] + shape_rep_type = rep_curve.is_a().upper() + if not shape_rep_type in supported_rep_types: + raise NotImplementedError( + f"Expected entity type to be one of {[_ for _ in supported_rep_types]}, got '{shape_rep_type}" + ) + + s = ifcopenshell.geom.settings() + s.set("piecewise-step-type", 0) # 0 = step-size is maximum step size, 1 = step-size is mininimum number of steps + s.set("piecewise-step-size", distance_interval) + shape = ifcopenshell.geom.create_shape(s, rep_curve) + vertices = shape.verts + if len(vertices) == 0: + msg = f"[ERROR] No vertices generated by ifcopenshell.geom.create_shape()." + raise ValueError(msg) + return np.array(vertices).reshape((-1, 3)) + + +def print_alignment(alignment, indent=0): + """ + Debugging function to print alignment decomposition + """ + print(" " * indent, alignment) + + for rel in alignment.IsNestedBy: + for child in rel.RelatedObjects: + print_alignment(child, indent + 2) + + for agg in alignment.IsDecomposedBy: + for child in agg.RelatedObjects: + print_alignment(child, indent + 2) + + +def print_composite_curve(curve): + """ + Debugging function to print composite curve segments + """ + print(str(curve)[0:100]) + + for segment in curve.Segments: + print(" " * 2, segment) diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index d56c9873a8..508aa449e4 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -68,6 +68,7 @@ SETTING = Literal[ "building-local-placement", "cgal-original-edges", "circle-segments", + "compute-curvature", "context-identifiers", "context-ids", "context-types", diff --git a/src/ifcopenshell-python/test/api/alignment/test_add_segment_to_curve.py b/src/ifcopenshell-python/test/api/alignment/test_add_segment_to_curve.py new file mode 100644 index 0000000000..8098f7b51b --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_add_segment_to_curve.py @@ -0,0 +1,72 @@ +# 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 pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def test_add_segment_to_curve(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + circular_arc = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((4084.115884, 3889.462938)), + file.createIfcDirection((0.224530986099614, 0.974466949814685)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(-1848.115835), + ParentCurve=file.createIfcCircle( + Position=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((0.0, 0.0)), file.createIfcDirection((1.0, 0.0)) + ), + Radius=1250.0, + ), + ) + + line = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((5469.395067, 4847.56631)), + file.createIfcDirection((0.991014275066766, -0.133756146078947)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(1564.635765), + ParentCurve=file.createIfcLine( + Pnt=file.createIfcCartesianPoint((0.0, 0.0)), + Dir=file.createIfcVector(Orientation=file.createIfcDirection((1.0, 0.0)), Magnitude=1.0), + ), + ) + + composite_curve = file.createIfcCompositeCurve(SelfIntersect=False) + + ifcopenshell.api.alignment.add_segment_to_curve(file, circular_arc, composite_curve) + assert circular_arc.UsingCurves[0] == composite_curve + assert composite_curve.Segments[-1] == circular_arc + + ifcopenshell.api.alignment.add_segment_to_curve(file, line, composite_curve) + assert line.UsingCurves[0] == composite_curve + assert composite_curve.Segments[-1] == line diff --git a/src/ifcopenshell-python/test/api/alignment/test_add_segment_to_layout.py b/src/ifcopenshell-python/test/api/alignment/test_add_segment_to_layout.py new file mode 100644 index 0000000000..96ad157f60 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_add_segment_to_layout.py @@ -0,0 +1,75 @@ +# 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 pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def test_add_segment_to_layout(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + horizontal_alignment = file.create_entity( + type="IfcAlignmentHorizontal", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + ) + + design_parameters = file.create_entity( + type="IfcAlignmentHorizontalSegment", + StartTag=None, + EndTag=None, + StartPoint=file.createIfcCartesianPoint(Coordinates=((0.0, 0.0))), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + GravityCenterLineHeight=None, + PredefinedType="LINE", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + + ifcopenshell.api.alignment.add_segment_to_layout(file, horizontal_alignment, alignment_segment) + + assert len(horizontal_alignment.IsNestedBy) == 1 + assert len(horizontal_alignment.IsNestedBy[0].RelatedObjects) == 1 + assert horizontal_alignment.IsNestedBy[0].RelatedObjects[0] == alignment_segment diff --git a/src/ifcopenshell-python/test/api/alignment/test_add_stationing_to_alignment.py b/src/ifcopenshell-python/test/api/alignment/test_add_stationing_to_alignment.py new file mode 100644 index 0000000000..e559653618 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_add_stationing_to_alignment.py @@ -0,0 +1,56 @@ +# 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 pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def test_add_stationing_to_alignment(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + vpoints = [(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (7400.0, 153.0), (9800.0, 105.0), (12800.0, 90.0)] + lengths = [(1600.0), (1200.0), (2000.0), (800.0)] + + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method( + file, "TestAlignment", coordinates, radii, vpoints, lengths + ) + + ifcopenshell.api.alignment.add_stationing_to_alignment(file, alignment, 2000.0) + + for rel in alignment.IsNestedBy: + for referent in rel.RelatedObjects: + if referent.is_a("IfcReferent"): + assert referent.PredefinedType == "STATION" + assert referent.Name == "2+000.000" + assert ifcopenshell.util.element.get_pset(element=referent, name="Pset_Stationing") + assert ( + ifcopenshell.util.element.get_pset(element=referent, name="Pset_Stationing", prop="Station") + == 2000.0 + ) diff --git a/src/ifcopenshell-python/test/api/alignment/test_add_vertical_by_pi_method.py b/src/ifcopenshell-python/test/api/alignment/test_add_vertical_by_pi_method.py new file mode 100644 index 0000000000..53a8f7c0df --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_add_vertical_by_pi_method.py @@ -0,0 +1,74 @@ +# 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 pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def test_add_vertical_by_pi_method(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + vpoints = [(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (7400.0, 153.0), (9800.0, 105.0), (12800.0, 90.0)] + lengths = [(1600.0), (1200.0), (2000.0), (800.0)] + + # single horizontal alignment + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method(file, "TestAlignment", coordinates, radii) + + assert len(alignment.IsDecomposedBy) == 0 # no child alignments + assert len(alignment.IsNestedBy) == 1 # nesting IfcAlignemtHorizontal + assert len(alignment.IsNestedBy[0].RelatedObjects) == 1 # nesting one IfcAlignmentHorizontal + assert alignment.IsNestedBy[0].RelatedObjects[0].is_a("IfcAlignmentHorizontal") + assert ( + len(alignment.IsNestedBy[0].RelatedObjects[0].IsNestedBy) == 1 + ) # nesting of segments beneath IfcAlignmentHorizontal + assert len(alignment.IsNestedBy[0].RelatedObjects[0].IsNestedBy[0].RelatedObjects) == 8 # segments + + # add first vertical + ifcopenshell.api.alignment.add_vertical_alignment_by_pi_method(file, alignment, vpoints, lengths) + assert len(alignment.IsDecomposedBy) == 0 # no child alignments + assert len(alignment.IsNestedBy) == 1 # 1 nesting relationsip for the alignments + assert len(alignment.IsNestedBy[0].RelatedObjects) == 2 # nesting IfcAlignmentHorizontal and IfcAlignmentVertical + assert alignment.IsNestedBy[0].RelatedObjects[0].is_a("IfcAlignmentHorizontal") + assert alignment.IsNestedBy[0].RelatedObjects[1].is_a("IfcAlignmentVertical") + + # add second vertical + ifcopenshell.api.alignment.add_vertical_alignment_by_pi_method(file, alignment, vpoints, lengths) + assert len(alignment.IsDecomposedBy) == 1 # 1 IfcRelAggreates relationship for the child algiments + assert ( + len(alignment.IsDecomposedBy[0].RelatedObjects) == 2 + ) # two child alignments, one for the first vertical and one for the vertical just added + for child_alignment in alignment.IsDecomposedBy[0].RelatedObjects: + assert child_alignment.is_a("IfcAlignment") + assert len(child_alignment.IsNestedBy) == 1 # one nesting relationship for the IfcAlignmentVertical + assert len(child_alignment.IsNestedBy[0].RelatedObjects) == 1 # The IfcAlignmentVertical + assert child_alignment.IsNestedBy[0].RelatedObjects[0].is_a("IfcAlignmentVertical") + assert len(alignment.IsNestedBy) == 1 # 1 nesting relationsip for the alignments + assert len(alignment.IsNestedBy[0].RelatedObjects) == 1 # nesting one IfcAlignmentHorizontal + assert alignment.IsNestedBy[0].RelatedObjects[0].is_a("IfcAlignmentHorizontal") diff --git a/src/ifcopenshell-python/test/api/alignment/test_get_basis_curve.py b/src/ifcopenshell-python/test/api/alignment/test_get_basis_curve.py new file mode 100644 index 0000000000..ee1a65af19 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_get_basis_curve.py @@ -0,0 +1,70 @@ +# 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 pytest + +# import test.bootstrap +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +# class TestGetBasisCurve(test.bootstrap.IFC4X3): +def test_horizontal(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method(file, "TestAlignment", coordinates, radii) + ifcopenshell.api.alignment.create_geometric_representation(file, alignment) + basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + assert basis_curve.is_a("IfcCompositeCurve") + + +def test_horizontal_and_vertical(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + vpoints = [(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (7400.0, 153.0), (9800.0, 105.0), (12800.0, 90.0)] + lengths = [(1600.0), (1200.0), (2000.0), (800.0)] + + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method( + file, "TestAlignment", coordinates, radii, vpoints, lengths + ) + ifcopenshell.api.alignment.create_geometric_representation(file, alignment) + basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + assert basis_curve.is_a("IfcCompositeCurve") diff --git a/src/ifcopenshell-python/test/api/alignment/test_get_curve.py b/src/ifcopenshell-python/test/api/alignment/test_get_curve.py new file mode 100644 index 0000000000..a00104d1b6 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_get_curve.py @@ -0,0 +1,70 @@ +# 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 pytest + +# import test.bootstrap +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +# class TestGetCurve(test.bootstrap.IFC4X3): +def test_horizontal(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method(file, "TestAlignment", coordinates, radii) + ifcopenshell.api.alignment.create_geometric_representation(file, alignment) + curve = ifcopenshell.api.alignment.get_curve(alignment) + assert curve.is_a("IfcCompositeCurve") + + +def test_horizontal_and_vertical(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + vpoints = [(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (7400.0, 153.0), (9800.0, 105.0), (12800.0, 90.0)] + lengths = [(1600.0), (1200.0), (2000.0), (800.0)] + + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method( + file, "TestAlignment", coordinates, radii, vpoints, lengths + ) + ifcopenshell.api.alignment.create_geometric_representation(file, alignment) + curve = ifcopenshell.api.alignment.get_curve(alignment) + assert curve.is_a("IfcGradientCurve") diff --git a/src/ifcopenshell-python/test/api/alignment/test_has_zero_length_segment.py b/src/ifcopenshell-python/test/api/alignment/test_has_zero_length_segment.py new file mode 100644 index 0000000000..b70002baa5 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_has_zero_length_segment.py @@ -0,0 +1,123 @@ +# 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 pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.alignment.has_zero_length_segment +import ifcopenshell.api.alignment.remove_zero_length_segment +import ifcopenshell.api.context +import ifcopenshell.guid +import ifcopenshell.api.nest + + +def _test_business_definition(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + horizontal = file.createIfcAlignmentHorizontal("Horizontal Alignment") + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + segment = file.createIfcAlignmentSegment(GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters) + ifcopenshell.api.nest.assign_object( + file, + related_objects=[ + segment, + ], + relating_object=horizontal, + ) + + assert False == ifcopenshell.api.alignment.has_zero_length_segment(horizontal) + + ifcopenshell.api.alignment.add_zero_length_segment(file, horizontal) + assert len(horizontal.IsNestedBy[0].RelatedObjects) == 2 + + assert True == ifcopenshell.api.alignment.has_zero_length_segment(horizontal) + + zero_length_segment = ifcopenshell.api.alignment.remove_zero_length_segment(file, horizontal) + assert len(horizontal.IsNestedBy[0].RelatedObjects) == 1 + assert False == ifcopenshell.api.alignment.has_zero_length_segment(horizontal) + + ifcopenshell.api.alignment.add_segment_to_layout(file, horizontal, zero_length_segment) + assert len(horizontal.IsNestedBy[0].RelatedObjects) == 2 + assert True == ifcopenshell.api.alignment.has_zero_length_segment(horizontal) + + +def _test_geometric_definition(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + circular_arc = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((4084.115884, 3889.462938)), + file.createIfcDirection((0.224530986099614, 0.974466949814685)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(-1848.115835), + ParentCurve=file.createIfcCircle( + Position=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((0.0, 0.0)), file.createIfcDirection((1.0, 0.0)) + ), + Radius=1250.0, + ), + ) + + composite_curve = file.createIfcCompositeCurve(Segments=(circular_arc,), SelfIntersect=False) + + assert False == ifcopenshell.api.alignment.has_zero_length_segment(composite_curve) + + ifcopenshell.api.alignment.add_zero_length_segment(file, composite_curve) + + assert True == ifcopenshell.api.alignment.has_zero_length_segment(composite_curve) + + assert len(composite_curve.Segments) == 2 + + zero_length_segment = ifcopenshell.api.alignment.remove_zero_length_segment(file, composite_curve) + assert len(composite_curve.Segments) == 1 + assert False == ifcopenshell.api.alignment.has_zero_length_segment(composite_curve) + + ifcopenshell.api.alignment.add_segment_to_curve(file, zero_length_segment, composite_curve) + assert len(composite_curve.Segments) == 2 + assert True == ifcopenshell.api.alignment.has_zero_length_segment(composite_curve) + + segment = composite_curve.Segments[-1] + assert segment.Placement.Location.Coordinates == (5469.394535876198, 4847.567078630914) + assert segment.Placement.RefDirection.DirectionRatios == (0.9910142986043448, -0.13375597168627318) + + +def test_has_zero_length_segment(): + _test_business_definition() + _test_geometric_definition() diff --git a/src/ifcopenshell-python/test/api/alignment/test_map_alignment_cant_segment.py b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_cant_segment.py new file mode 100644 index 0000000000..755fe3e13c --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_cant_segment.py @@ -0,0 +1,26 @@ +# 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 pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def test_map_alignment_cant_segment(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + # create tests as the mapping functions are implemented diff --git a/src/ifcopenshell-python/test/api/alignment/test_map_alignment_horizontal_segment.py b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_horizontal_segment.py new file mode 100644 index 0000000000..0d8e55293d --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_horizontal_segment.py @@ -0,0 +1,2101 @@ +# 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 . + +# These are test cases generated from https://github.com/bSI-RailwayRoom/IFC-Rail-Unit-Test-Reference-Code/tree/master/alignment_testset/IFC-WithGeneratedGeometry +# for horizontal alignment. + +import pytest +import ifcopenshell.api.alignment + + +def _BlossCurve_100_0_300_1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=1000.0, + SegmentLength=100.0, + PredefinedType="BLOSSCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcThirdOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CubicTerm == pytest.approx(120.989673502444) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-112.624788044361) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(300.0) + + +def _BlossCurve_100_0__300__1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-1000.0, + SegmentLength=100.0, + PredefinedType="BLOSSCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcThirdOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CubicTerm == pytest.approx(-120.989673502444) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(112.624788044361) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-300.0) + + +def _BlossCurve_100_0_300_inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="BLOSSCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcThirdOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CubicTerm == pytest.approx(110.668191970032) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-100.0) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(300.0) + + +def _BlossCurve_100_0__300__inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="BLOSSCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcThirdOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CubicTerm == pytest.approx(-110.668191970032) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(100.0) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-300.0) + + +def _BlossCurve_100_0_1000_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=1000.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="BLOSSCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcThirdOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CubicTerm == pytest.approx(-120.989673502444) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(112.624788044361) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(1000.0) + + +def _BlossCurve_100_0__1000__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-1000.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="BLOSSCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcThirdOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CubicTerm == pytest.approx(120.989673502444) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-112.624788044361) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-1000.0) + + +def _BlossCurve_100_0_inf_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="BLOSSCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcThirdOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CubicTerm == pytest.approx(-110.668191970032) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(100.0) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(None) + + +def _BlossCurve_100_0__inf__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="BLOSSCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcThirdOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CubicTerm == pytest.approx(110.668191970032) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-100.0) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(None) + + +def _CircularArc_100_0_300_1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(300.0) + + +def _CircularArc_100_0__300__1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(-100.0) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(300.0) + + +def _CircularArc_100_0_300_inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(300.0) + + +def _CircularArc_100_0__300__inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(-100.0) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(300.0) + + +def _CircularArc_100_0_1000_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=1000.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(1000.0) + + +def _CircularArc_100_0__1000__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(-100.0) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(300.0) + + +def _CircularArc_100_0_inf_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(300.0) + + +def _CircularArc_100_0__inf__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(-100.0) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(300.0) + + +def _Clothoid_100_0_300_1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=1000.0, + SegmentLength=100.0, + PredefinedType="CLOTHOID", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(-142.857142857143) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcClothoid") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.ClothoidConstant == pytest.approx(-207.019667802706) + + +def _Clothoid_100_0__300__1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-1000.0, + SegmentLength=100.0, + PredefinedType="CLOTHOID", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(-142.857142857143) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcClothoid") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.ClothoidConstant == pytest.approx(207.019667802706) + + +def _Clothoid_100_0_300_inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="CLOTHOID", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(-100.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcClothoid") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.ClothoidConstant == pytest.approx(-173.205080756888) + + +def _Clothoid_100_0__300__inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="CLOTHOID", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(-100.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcClothoid") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.ClothoidConstant == pytest.approx(173.205080756888) + + +def _Clothoid_100_0_1000_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=1000.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="CLOTHOID", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(42.8571428571429) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcClothoid") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.ClothoidConstant == pytest.approx(207.019667802706) + + +def _Clothoid_100_0__1000__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-1000.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="CLOTHOID", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(42.8571428571429) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcClothoid") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.ClothoidConstant == pytest.approx(-207.019667802706) + + +def _Clothoid_100_0_inf_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="CLOTHOID", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcClothoid") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.ClothoidConstant == pytest.approx(173.205080756888) + + +def _Clothoid_100_0__inf__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="CLOTHOID", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcClothoid") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.ClothoidConstant == pytest.approx(-173.205080756888) + + +def _CosineCurve_100_0_300_1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=1000.0, + SegmentLength=100.0, + PredefinedType="COSINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCosineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CosineTerm == pytest.approx(857.142857142857) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(461.538461538462) + + +def _CosineCurve_100_0__300__1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-1000.0, + SegmentLength=100.0, + PredefinedType="COSINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCosineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CosineTerm == pytest.approx(-857.142857142857) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-461.538461538462) + + +def _CosineCurve_100_0_300_inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="COSINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCosineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CosineTerm == pytest.approx(600.0) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(600.0) + + +def _CosineCurve_100_0__300__inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="COSINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCosineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CosineTerm == pytest.approx(-600.0) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-600.0) + + +def _CosineCurve_100_0_1000_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=1000.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="COSINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCosineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CosineTerm == pytest.approx(-857.142857142857) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(461.538461538462) + + +def _CosineCurve_100_0__1000__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-1000.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="COSINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCosineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CosineTerm == pytest.approx(857.142857142857) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-461.538461538462) + + +def _CosineCurve_100_0_inf_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="COSINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCosineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CosineTerm == pytest.approx(-600.0) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(600.0) + + +def _CosineCurve_100_0__inf__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="COSINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCosineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CosineTerm == pytest.approx(600.0) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-600.0) + + +def _Cubic_100_0_300_1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=1000.0, + SegmentLength=100.0, + PredefinedType="CUBIC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(-142.857142857143) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((0.0, 0.0, 0.0, -3.88888888888889e-06)) + + +def _Cubic_100_0__300__1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-1000.0, + SegmentLength=100.0, + PredefinedType="CUBIC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(-142.857142857143) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((0.0, 0.0, 0.0, 3.88888888888889e-06)) + + +def _Cubic_100_0_300_inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="CUBIC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(-100.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((0.0, 0.0, 0.0, -5.55555555555556e-06)) + + +def _Cubic_100_0__300__inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="CUBIC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(-100.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((0.0, 0.0, 0.0, 5.55555555555556e-06)) + + +def _Cubic_100_0_1000_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=1000.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="CUBIC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(42.8571428571429) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((0.0, 0.0, 0.0, 3.88888888888889e-06)) + + +def _Cubic_100_0__1000__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-1000.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="CUBIC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(42.8571428571429) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((0.0, 0.0, 0.0, -3.88888888888889e-06)) + + +def _Cubic_100_0_inf_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="CUBIC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((0.0, 0.0, 0.0, 5.55555555555556e-06)) + + +def _Cubic_100_0__inf__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="CUBIC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((0.0, 0.0, 0.0, -5.55555555555556e-06)) + + +def _HelmertCurve_100_0_300_1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=1000.0, + SegmentLength=100.0, + PredefinedType="HELMERTCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-128.92319893893) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(300.0) + mapped_segment = mapped_segments[1] + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((49.7998035122387, 3.91603145329256)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.9892460407218963, 0.146260968532457) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(50.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (-0.009321141429516372, 0.46831933573745577) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx( + (0.9992574637140321, -0.03852948496670688) + ) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(128.92319893893) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(-103.509833901353) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(176.470588235294) + + +def _HelmertCurve_100_0__300__1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-1000.0, + SegmentLength=100.0, + PredefinedType="HELMERTCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(128.92319893893) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-300.0) + mapped_segment = mapped_segments[1] + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((49.7998035122387, -3.91603145329256)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.9892460407218963, -0.146260968532457) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(50.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (-0.009321141429516372, -0.46831933573745577) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx( + (0.9992574637140321, 0.03852948496670688) + ) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-128.92319893893) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(103.509833901353) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-176.470588235294) + + +def _HelmertCurve_100_0_300_inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="HELMERTCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-114.471424255333) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(300.0) + mapped_segment = mapped_segments[1] + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((49.8122545525202, 3.81263503030693)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.9904138664989948, 0.1381317235341378) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(50.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (-0.010305467756443198, 0.6738837916692928) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx( + (0.9984794480380026, -0.05512523782919828) + ) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(114.471424255333) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(-86.6025403784439) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(150.0) + + +def _HelmertCurve_100_0__300__inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="HELMERTCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(114.471424255333) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-300.0) + mapped_segment = mapped_segments[1] + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((49.8122545525202, -3.81263503030693)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.9904138664989948, -0.1381317235341378) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(50.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (-0.010305467756443198, -0.6738837916692928) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx( + (0.9984794480380026, 0.05512523782919828) + ) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-114.471424255333) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(86.6025403784439) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-150.0) + + +def _HelmertCurve_100_0_1000_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=1000.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="HELMERTCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(128.92319893893) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(1000.0) + mapped_segment = mapped_segments[1] + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((49.9681012468824, 1.49252747074135)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.997594495159641, 0.0693197174487962) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(50.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (0.010408767953926904, -0.4828832446956578) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx( + (0.9992463304688143, 0.03881714884698913) + ) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-128.92319893893) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(103.509833901353) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-750.0) + + +def _HelmertCurve_100_0__1000__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-1000.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="HELMERTCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-128.92319893893) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-1000.0) + mapped_segment = mapped_segments[1] + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((49.9681012468824, -1.49252747074135)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.997594495159641, -0.0693197174487962) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(50.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (0.010408767953926904, 0.4828832446956578) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx( + (0.9992463304688143, -0.03881714884698913) + ) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(128.92319893893) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(-103.509833901353) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(750.0) + + +def _HelmertCurve_100_0_inf_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="HELMERTCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(114.471424255333) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(None) + mapped_segment = mapped_segments[1] + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((49.9972443634885, 0.347204361427475)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.999614222337484, 0.027769614722351524) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(50.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (0.011625841243773832, -0.6968669147609581) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx( + (0.9984543318840984, 0.05557829739996359) + ) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-114.471424255333) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(86.6025403784439) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-300.0) + + +def _HelmertCurve_100_0__inf__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="HELMERTCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-114.471424255333) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(None) + mapped_segment = mapped_segments[1] + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((49.9972443634885, -0.347204361427475)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.999614222337484, -0.027769614722351524) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(50.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (0.011625841243773832, 0.6968669147609581) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx( + (0.9984543318840984, -0.05557829739996359) + ) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(114.471424255333) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(-86.6025403784439) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(300.0) + + +def _Line_100_0_300_1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _Line_100_0__300__1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _Line_100_0_300_inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _Line_100_0__300__inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _Line_100_0_1000_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _Line_100_0__1000__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _Line_100_0_inf_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _Line_100_0__inf__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _SineCurve_100_0_300_1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=1000.0, + SegmentLength=100.0, + PredefinedType="SINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcSineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.SineTerm == pytest.approx(2692.79370307697) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(-207.019667802706) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(300.0) + + +def _SineCurve_100_0__300__1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-1000.0, + SegmentLength=100.0, + PredefinedType="SINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcSineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.SineTerm == pytest.approx(-2692.79370307697) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(207.019667802706) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-300.0) + + +def _SineCurve_100_0_300_inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="SINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcSineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.SineTerm == pytest.approx(1884.95559215388) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(-173.205080756888) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(300.0) + + +def _SineCurve_100_0__300__inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="SINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcSineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.SineTerm == pytest.approx(-1884.95559215388) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(173.205080756888) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-300.0) + + +def _SineCurve_100_0_1000_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=1000.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="SINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcSineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.SineTerm == pytest.approx(-2692.79370307697) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(207.019667802706) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(1000.0) + + +def _SineCurve_100_0__1000__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-1000.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="SINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcSineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.SineTerm == pytest.approx(2692.79370307697) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(-207.019667802706) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-1000.0) + + +def _SineCurve_100_0_inf_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="SINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcSineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.SineTerm == pytest.approx(-1884.95559215388) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(173.205080756888) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(None) + + +def _SineCurve_100_0__inf__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="SINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcSineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.SineTerm == pytest.approx(1884.95559215388) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(-173.205080756888) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(None) + + +def test_map_alignment_horizontal_segment(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + _BlossCurve_100_0_300_1000_1_Meter(file) + _BlossCurve_100_0__300__1000_1_Meter(file) + _BlossCurve_100_0_300_inf_1_Meter(file) + _BlossCurve_100_0__300__inf_1_Meter(file) + _BlossCurve_100_0_1000_300_1_Meter(file) + _BlossCurve_100_0__1000__300_1_Meter(file) + _BlossCurve_100_0_inf_300_1_Meter(file) + _BlossCurve_100_0__inf__300_1_Meter(file) + _CircularArc_100_0_300_1000_1_Meter(file) + _CircularArc_100_0__300__1000_1_Meter(file) + _CircularArc_100_0_300_inf_1_Meter(file) + _CircularArc_100_0__300__inf_1_Meter(file) + _CircularArc_100_0_1000_300_1_Meter(file) + _CircularArc_100_0__1000__300_1_Meter(file) + _CircularArc_100_0_inf_300_1_Meter(file) + _CircularArc_100_0__inf__300_1_Meter(file) + _Clothoid_100_0_300_1000_1_Meter(file) + _Clothoid_100_0__300__1000_1_Meter(file) + _Clothoid_100_0_300_inf_1_Meter(file) + _Clothoid_100_0__300__inf_1_Meter(file) + _Clothoid_100_0_1000_300_1_Meter(file) + _Clothoid_100_0__1000__300_1_Meter(file) + _Clothoid_100_0_inf_300_1_Meter(file) + _Clothoid_100_0__inf__300_1_Meter(file) + _CosineCurve_100_0_300_1000_1_Meter(file) + _CosineCurve_100_0__300__1000_1_Meter(file) + _CosineCurve_100_0_300_inf_1_Meter(file) + _CosineCurve_100_0__300__inf_1_Meter(file) + _CosineCurve_100_0_1000_300_1_Meter(file) + _CosineCurve_100_0__1000__300_1_Meter(file) + _CosineCurve_100_0_inf_300_1_Meter(file) + _CosineCurve_100_0__inf__300_1_Meter(file) + _Cubic_100_0_300_1000_1_Meter(file) + _Cubic_100_0__300__1000_1_Meter(file) + _Cubic_100_0_300_inf_1_Meter(file) + _Cubic_100_0__300__inf_1_Meter(file) + _Cubic_100_0_1000_300_1_Meter(file) + _Cubic_100_0__1000__300_1_Meter(file) + _Cubic_100_0_inf_300_1_Meter(file) + _Cubic_100_0__inf__300_1_Meter(file) + _HelmertCurve_100_0_300_1000_1_Meter(file) + _HelmertCurve_100_0__300__1000_1_Meter(file) + _HelmertCurve_100_0_300_inf_1_Meter(file) + _HelmertCurve_100_0__300__inf_1_Meter(file) + _HelmertCurve_100_0_1000_300_1_Meter(file) + _HelmertCurve_100_0__1000__300_1_Meter(file) + _HelmertCurve_100_0_inf_300_1_Meter(file) + _HelmertCurve_100_0__inf__300_1_Meter(file) + _Line_100_0_300_1000_1_Meter(file) + _Line_100_0__300__1000_1_Meter(file) + _Line_100_0_300_inf_1_Meter(file) + _Line_100_0__300__inf_1_Meter(file) + _Line_100_0_1000_300_1_Meter(file) + _Line_100_0__1000__300_1_Meter(file) + _Line_100_0_inf_300_1_Meter(file) + _Line_100_0__inf__300_1_Meter(file) + _SineCurve_100_0_300_1000_1_Meter(file) + _SineCurve_100_0__300__1000_1_Meter(file) + _SineCurve_100_0_300_inf_1_Meter(file) + _SineCurve_100_0__300__inf_1_Meter(file) + _SineCurve_100_0_1000_300_1_Meter(file) + _SineCurve_100_0__1000__300_1_Meter(file) + _SineCurve_100_0_inf_300_1_Meter(file) + _SineCurve_100_0__inf__300_1_Meter(file) + + # VIENESSE BEND NOT IMPLEMENTED diff --git a/src/ifcopenshell-python/test/api/alignment/test_map_alignment_segments.py b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_segments.py new file mode 100644 index 0000000000..1c9d04bc24 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_segments.py @@ -0,0 +1,102 @@ +# 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 pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def test_map_alignment_horizontal_segment(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + vpoints = [(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (7400.0, 153.0), (9800.0, 105.0), (12800.0, 90.0)] + lengths = [(1600.0), (1200.0), (2000.0), (800.0)] + + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method( + file, "TestAlignment", coordinates, radii, vpoints, lengths + ) + + horizontal_alignment = alignment.IsNestedBy[0].RelatedObjects[0] + assert horizontal_alignment.is_a("IfcAlignmentHorizontal") + + composite_curve = file.create_entity( + type="IfcCompositeCurve", + Segments=[], + SelfIntersect=False, + ) + + ifcopenshell.api.alignment.map_alignment_segments(file, horizontal_alignment, composite_curve) + assert len(composite_curve.Segments) == 8 + assert composite_curve.Segments[0].ParentCurve.is_a("IfcLine") + assert composite_curve.Segments[0].Transition == "CONTSAMEGRADIENT" + assert composite_curve.Segments[1].ParentCurve.is_a("IfcCircle") + assert composite_curve.Segments[1].Transition == "CONTSAMEGRADIENT" + assert composite_curve.Segments[2].ParentCurve.is_a("IfcLine") + assert composite_curve.Segments[2].Transition == "CONTSAMEGRADIENT" + assert composite_curve.Segments[3].ParentCurve.is_a("IfcCircle") + assert composite_curve.Segments[3].Transition == "CONTSAMEGRADIENT" + assert composite_curve.Segments[4].ParentCurve.is_a("IfcLine") + assert composite_curve.Segments[4].Transition == "CONTSAMEGRADIENT" + assert composite_curve.Segments[5].ParentCurve.is_a("IfcCircle") + assert composite_curve.Segments[5].Transition == "CONTSAMEGRADIENT" + assert composite_curve.Segments[6].ParentCurve.is_a("IfcLine") + assert composite_curve.Segments[6].Transition == "CONTSAMEGRADIENTSAMECURVATURE" + assert composite_curve.Segments[7].ParentCurve.is_a("IfcLine") + assert composite_curve.Segments[7].Transition == "DISCONTINUOUS" + + vertical_alignment = alignment.IsNestedBy[0].RelatedObjects[1] + assert vertical_alignment.is_a("IfcAlignmentVertical") + + gradient_curve = file.create_entity( + type="IfcGradientCurve", Segments=[], SelfIntersect=False, BaseCurve=composite_curve, EndPoint=None + ) + + ifcopenshell.api.alignment.map_alignment_segments(file, vertical_alignment, gradient_curve) + assert len(gradient_curve.Segments) == 10 + + assert gradient_curve.Segments[0].ParentCurve.is_a("IfcLine") + assert gradient_curve.Segments[0].Transition == "CONTSAMEGRADIENT" + assert gradient_curve.Segments[1].ParentCurve.is_a("IfcPolynomialCurve") + assert gradient_curve.Segments[1].Transition == "CONTSAMEGRADIENT" + assert gradient_curve.Segments[2].ParentCurve.is_a("IfcLine") + assert gradient_curve.Segments[2].Transition == "CONTSAMEGRADIENT" + assert gradient_curve.Segments[3].ParentCurve.is_a("IfcPolynomialCurve") + assert gradient_curve.Segments[3].Transition == "CONTSAMEGRADIENT" + assert gradient_curve.Segments[4].ParentCurve.is_a("IfcLine") + assert gradient_curve.Segments[4].Transition == "CONTSAMEGRADIENT" + assert gradient_curve.Segments[5].ParentCurve.is_a("IfcPolynomialCurve") + assert gradient_curve.Segments[5].Transition == "CONTSAMEGRADIENT" + assert gradient_curve.Segments[6].ParentCurve.is_a("IfcLine") + assert gradient_curve.Segments[6].Transition == "CONTSAMEGRADIENT" + assert gradient_curve.Segments[7].ParentCurve.is_a("IfcPolynomialCurve") + assert gradient_curve.Segments[7].Transition == "CONTSAMEGRADIENT" + assert gradient_curve.Segments[8].ParentCurve.is_a("IfcLine") + assert gradient_curve.Segments[8].Transition == "CONTSAMEGRADIENTSAMECURVATURE" + assert gradient_curve.Segments[9].ParentCurve.is_a("IfcLine") + assert gradient_curve.Segments[9].Transition == "DISCONTINUOUS" diff --git a/src/ifcopenshell-python/test/api/alignment/test_map_alignment_vertical_segment.py b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_vertical_segment.py new file mode 100644 index 0000000000..867183d369 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_vertical_segment.py @@ -0,0 +1,795 @@ +# 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 . + +# These are test cases generated from https://github.com/bSI-RailwayRoom/IFC-Rail-Unit-Test-Reference-Code/tree/master/alignment_testset/IFC-WithGeneratedGeometry +# for vertical alignment. + +import pytest +import ifcopenshell.api.alignment + + +def _CircularArc_100_0_10_0_0_0_0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.0, + EndGradient=0.5, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(1053.72220965611) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(103.674757133105) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((-0.0, 223.606797749979)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(223.606797749979) + + +def _CircularArc_100_0_10_0_0_0__0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.0, + EndGradient=-0.5, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(351.240736552036) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(-103.674757133105) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (-1.36919674566051e-14, -223.606797749979) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(223.606797749979) + + +def _CircularArc_100_0_10_0_0_5_0_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.5, + EndGradient=0.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, 0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(454.915493685141) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(-103.674757133105) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((100.0, -200.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(223.606797749979) + + +def _CircularArc_100_0_10_0__0_5_0_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-0.5, + EndGradient=0.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, -0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(950.047452523004) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(103.674757133105) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((100.0, 200.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(223.606797749979) + + +def _CircularArc_100_0_10_0_0_5_1_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.5, + EndGradient=1.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, 0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(1991.60150186753) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(123.801073716741) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (-172.075922005613, 344.151844011225) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(384.773458895502) + + +def _CircularArc_100_0_10_0__0_5__1_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-0.5, + EndGradient=-1.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, -0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(426.001441657352) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(-123.801073716741) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (-172.075922005613, -344.151844011225) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(384.773458895502) + + +def _CircularArc_100_0_10_0_1_0_0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=1.0, + EndGradient=0.5, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.707106781186547, 0.707106781186547) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(906.601103821832) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(-123.801073716741) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (272.075922005613, -272.075922005613) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(384.773458895502) + + +def _CircularArc_100_0_10_0__1_0__0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-1.0, + EndGradient=-0.5, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.707106781186547, -0.707106781186547) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(1511.00183970305) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(123.801073716741) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (272.075922005613, 272.075922005613) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(384.773458895502) + + +def _ConstantGradient_100_0_10_0_0_0_0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.0, + EndGradient=0.5, + PredefinedType="CONSTANTGRADIENT", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _ConstantGradient_100_0_10_0_0_0__0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.0, + EndGradient=-0.5, + PredefinedType="CONSTANTGRADIENT", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _ConstantGradient_100_0_10_0_0_5_0_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.5, + EndGradient=0.0, + PredefinedType="CONSTANTGRADIENT", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, 0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(111.803398874989) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _ConstantGradient_100_0_10_0__0_5_0_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-0.5, + EndGradient=0.0, + PredefinedType="CONSTANTGRADIENT", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, -0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(111.803398874989) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _ConstantGradient_100_0_10_0_0_5_1_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.5, + EndGradient=1.0, + PredefinedType="CONSTANTGRADIENT", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, 0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(111.803398874989) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _ConstantGradient_100_0_10_0__0_5__1_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-0.5, + EndGradient=-1.0, + PredefinedType="CONSTANTGRADIENT", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, -0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(111.803398874989) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _ConstantGradient_100_0_10_0_1_0_0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=1.0, + EndGradient=0.5, + PredefinedType="CONSTANTGRADIENT", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.707106781186547, 0.707106781186547) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(141.42135623731) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _ConstantGradient_100_0_10_0__1_0__0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-1.0, + EndGradient=-0.5, + PredefinedType="CONSTANTGRADIENT", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.707106781186547, -0.707106781186547) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(141.42135623731) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _ParabolicArc_100_0_10_0_0_0_0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.0, + EndGradient=0.5, + PredefinedType="PARABOLICARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(104.02288238772185) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((10.0, 0.0, 0.0025)) + + +def _ParabolicArc_100_0_10_0_0_0__0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.0, + EndGradient=-0.5, + PredefinedType="PARABOLICARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(104.02288238772185) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((10.0, 0.0, -0.0025)) + + +def _ParabolicArc_100_0_10_0_0_5_0_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.5, + EndGradient=0.0, + PredefinedType="PARABOLICARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, 0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(104.02288238772185) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((10.0, 0.5, -0.0025)) + + +def _ParabolicArc_100_0_10_0__0_5_0_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-0.5, + EndGradient=0.0, + PredefinedType="PARABOLICARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, -0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(104.02288238772185) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((10.0, -0.5, 0.0025)) + + +def _ParabolicArc_100_0_10_0_0_5_1_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.5, + EndGradient=1.0, + PredefinedType="PARABOLICARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, 0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(125.53583325398947) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((10.0, 0.5, 0.0025)) + + +def _ParabolicArc_100_0_10_0__0_5__1_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-0.5, + EndGradient=-1.0, + PredefinedType="PARABOLICARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, -0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(125.53583325398947) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((10.0, -0.5, -0.0025)) + + +def _ParabolicArc_100_0_10_0_1_0_0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=1.0, + EndGradient=0.5, + PredefinedType="PARABOLICARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.707106781186547, 0.707106781186547) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(125.53583325398947) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((10.0, 1.0, -0.0025)) + + +def _ParabolicArc_100_0_10_0__1_0__0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-1.0, + EndGradient=-0.5, + PredefinedType="PARABOLICARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.707106781186547, -0.707106781186547) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(125.53583325398947) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((10.0, -1.0, 0.0025)) + + +def test_map_alignment_vertical_segment(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + _CircularArc_100_0_10_0_0_0_0_5_1_Meter(file) + _CircularArc_100_0_10_0_0_0__0_5_1_Meter(file) + _CircularArc_100_0_10_0_0_5_0_0_1_Meter(file) + _CircularArc_100_0_10_0__0_5_0_0_1_Meter(file) + _CircularArc_100_0_10_0_0_5_1_0_1_Meter(file) + _CircularArc_100_0_10_0__0_5__1_0_1_Meter(file) + _CircularArc_100_0_10_0_1_0_0_5_1_Meter(file) + _CircularArc_100_0_10_0__1_0__0_5_1_Meter(file) + _ConstantGradient_100_0_10_0_0_0_0_5_1_Meter(file) + _ConstantGradient_100_0_10_0_0_0__0_5_1_Meter(file) + _ConstantGradient_100_0_10_0_0_5_0_0_1_Meter(file) + _ConstantGradient_100_0_10_0__0_5_0_0_1_Meter(file) + _ConstantGradient_100_0_10_0_0_5_1_0_1_Meter(file) + _ConstantGradient_100_0_10_0__0_5__1_0_1_Meter(file) + _ConstantGradient_100_0_10_0_1_0_0_5_1_Meter(file) + _ConstantGradient_100_0_10_0__1_0__0_5_1_Meter(file) + _ParabolicArc_100_0_10_0_0_0_0_5_1_Meter(file) + _ParabolicArc_100_0_10_0_0_0__0_5_1_Meter(file) + _ParabolicArc_100_0_10_0_0_5_0_0_1_Meter(file) + _ParabolicArc_100_0_10_0__0_5_0_0_1_Meter(file) + _ParabolicArc_100_0_10_0_0_5_1_0_1_Meter(file) + _ParabolicArc_100_0_10_0__0_5__1_0_1_Meter(file) + _ParabolicArc_100_0_10_0_1_0_0_5_1_Meter(file) + _ParabolicArc_100_0_10_0__1_0__0_5_1_Meter(file) + + # VERTICAL CLOTHOID NOT IMPLEMENTED diff --git a/src/ifcopenshell-python/test/api/alignment/test_name_segments.py b/src/ifcopenshell-python/test/api/alignment/test_name_segments.py new file mode 100644 index 0000000000..4ad01063d5 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_name_segments.py @@ -0,0 +1,53 @@ +# 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 pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def test_name_segments(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + vpoints = [(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (7400.0, 153.0), (9800.0, 105.0), (12800.0, 90.0)] + lengths = [(1600.0), (1200.0), (2000.0), (800.0)] + + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method( + file, "TestAlignment", coordinates, radii, vpoints, lengths + ) + + for rel in alignment.IsNestedBy: + for a in rel.RelatedObjects: + if a.is_a("IfcLinearElement"): + ifcopenshell.api.alignment.name_segments("Q", a) + i = 1 + for sr in a.IsNestedBy: + for s in sr.RelatedObjects: + assert f"Q{i}" == s.Name + i += 1 diff --git a/src/ifcopenshell-python/test/api/alignment/test_update_curve_segment_transition_code.py b/src/ifcopenshell-python/test/api/alignment/test_update_curve_segment_transition_code.py new file mode 100644 index 0000000000..fcfe6cb2af --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_update_curve_segment_transition_code.py @@ -0,0 +1,248 @@ +# 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 pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def _test1(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + # 26=IFCCARTESIANPOINT((4084.115884,3889.462938)); + # 70=IFCDIRECTION((0.224530986099614,0.974466949814685)); + # 71=IFCAXIS2PLACEMENT2D(#26,#70); + # 72=IFCCARTESIANPOINT((0.,0.)); + # 73=IFCDIRECTION((1.,0.)); + # 74=IFCAXIS2PLACEMENT2D(#72,#73); + # 75=IFCCIRCLE(#74,1250.); + # 76=IFCCURVESEGMENT(.CONTSAMEGRADIENT.,#71,IFCLENGTHMEASURE(0.),IFCLENGTHMEASURE(-1848.115835),#75); + circular_arc = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((4084.115884, 3889.462938)), + file.createIfcDirection((0.224530986099614, 0.974466949814685)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(-1848.115835), + ParentCurve=file.createIfcCircle( + Position=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((0.0, 0.0)), file.createIfcDirection((1.0, 0.0)) + ), + Radius=1250.0, + ), + ) + + # 27=IFCCARTESIANPOINT((5469.395067,4847.56631)); + # 77=IFCDIRECTION((0.991014275066766,-0.133756146078947)); + # 78=IFCAXIS2PLACEMENT2D(#27,#77); + # 79=IFCCARTESIANPOINT((0.,0.)); + # 80=IFCDIRECTION((1.,0.)); + # 81=IFCVECTOR(#80,1.); + # 82=IFCLINE(#79,#81); + # 83=IFCCURVESEGMENT(.CONTSAMEGRADIENT.,#78,IFCLENGTHMEASURE(0.),IFCLENGTHMEASURE(1564.635765),#82); + line = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((5469.395067, 4847.56631)), + file.createIfcDirection((0.991014275066766, -0.133756146078947)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(1564.635765), + ParentCurve=file.createIfcLine( + Pnt=file.createIfcCartesianPoint((0.0, 0.0)), + Dir=file.createIfcVector(Orientation=file.createIfcDirection((1.0, 0.0)), Magnitude=1.0), + ), + ) + + composite_curve = file.createIfcCompositeCurve(Segments=(circular_arc, line), SelfIntersect=False) + + ifcopenshell.api.alignment.update_curve_segment_transition_code(circular_arc, line) + assert circular_arc.Transition == "CONTSAMEGRADIENT" + + +def _test2(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + # 30=IFCCARTESIANPOINT((0.,0.)); + # 31=IFCALIGNMENTHORIZONTALSEGMENT($,$,#30,0.523598775598299,0.,0.,27.8843513637174,$,.LINE.); + # 32=IFCALIGNMENTSEGMENT('3$jiMaOgfAoujgvRyMLw0X',$,'H1',$,$,#111,#113,#31); + # 33=IFCDIRECTION((0.866025403784439,0.5)); + # 34=IFCAXIS2PLACEMENT2D(#30,#33); + # 35=IFCCARTESIANPOINT((0.,0.)); + # 36=IFCDIRECTION((1.,0.)); + # 37=IFCVECTOR(#36,1.); + # 38=IFCLINE(#35,#37); + # 39=IFCCURVESEGMENT(.CONTSAMEGRADIENT.,#34,IFCLENGTHMEASURE(0.),IFCLENGTHMEASURE(27.8843513637174),#38); + + line1 = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((0.0, 0.0)), + file.createIfcDirection((0.866025403784439, 0.5)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(27.8843513637174), + ParentCurve=file.createIfcLine( + Pnt=file.createIfcCartesianPoint((0.0, 0.0)), + Dir=file.createIfcVector(Orientation=file.createIfcDirection((1.0, 0.0)), Magnitude=1.0), + ), + ) + + # 40=IFCCARTESIANPOINT((24.1485566490305,13.9421756818587)); + # 41=IFCALIGNMENTHORIZONTALSEGMENT($,$,#40,0.523598775598299,0.,1524.,152.4,$,.CLOTHOID.); + # 42=IFCALIGNMENTSEGMENT('0Rd38fCkHF1Q11ppiqdMP6',$,'H2',$,$,#111,#115,#41); + # 43=IFCDIRECTION((0.866025403784439,0.5)); + # 44=IFCAXIS2PLACEMENT2D(#40,#43); + # 45=IFCCARTESIANPOINT((0.,0.)); + # 46=IFCDIRECTION((1.,0.)); + # 47=IFCAXIS2PLACEMENT2D(#45,#46); + # 48=IFCCLOTHOID(#47,481.931115409661); + # 49=IFCCURVESEGMENT(.CONTSAMEGRADIENT.,#44,IFCLENGTHMEASURE(0.),IFCLENGTHMEASURE(152.4),#48); + + clothoid1 = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((24.1485566490305, 13.9421756818587)), + file.createIfcDirection((0.866025403784439, 0.5)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(152.4), + ParentCurve=file.createIfcClothoid( + Position=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((0.0, 0.0)), RefDirection=file.createIfcDirection((1.0, 0.0)) + ), + ClothoidConstant=481.931115409661, + ), + ) + + # 50=IFCCARTESIANPOINT((154.828063204281,92.32243963907)); + # 51=IFCALIGNMENTHORIZONTALSEGMENT($,$,#50,0.573598775598299,1524.,1524.,246.582267005904,$,.CIRCULARARC.); + # 52=IFCALIGNMENTSEGMENT('2OGYY2lQjCnRlzxKU9vtdu',$,'H3',$,$,#111,#117,#51); + # 53=IFCDIRECTION((0.839953512903025,0.542658360445933)); + # 54=IFCAXIS2PLACEMENT2D(#50,#53); + # 55=IFCCARTESIANPOINT((0.,0.)); + # 56=IFCDIRECTION((1.,0.)); + # 57=IFCAXIS2PLACEMENT2D(#55,#56); + # 58=IFCCIRCLE(#57,1524.); + # 59=IFCCURVESEGMENT(.CONTSAMEGRADIENT.,#54,IFCLENGTHMEASURE(0.),IFCLENGTHMEASURE(246.582267005904),#58); + + circular_arc = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((154.828063204281, 92.32243963907)), + file.createIfcDirection((0.839953512903025, 0.542658360445933)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(246.582267005904), + ParentCurve=file.createIfcCircle( + Position=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((0.0, 0.0)), file.createIfcDirection((1.0, 0.0)) + ), + Radius=1524.0, + ), + ) + + # 60=IFCCARTESIANPOINT((350.24160971216,242.268527691248)); + # 61=IFCALIGNMENTHORIZONTALSEGMENT($,$,#60,0.735398163397447,1524.,0.,152.4,$,.CLOTHOID.); + # 62=IFCALIGNMENTSEGMENT('13CGRzUN9CAfNVKnf0Pxza',$,'H4',$,$,#111,#119,#61); + # 63=IFCDIRECTION((0.741563691346478,0.670882472327743)); + # 64=IFCAXIS2PLACEMENT2D(#60,#63); + # 65=IFCCARTESIANPOINT((0.,0.)); + # 66=IFCDIRECTION((1.,0.)); + # 67=IFCAXIS2PLACEMENT2D(#65,#66); + # 68=IFCCLOTHOID(#67,-481.931115409661); + # 69=IFCCURVESEGMENT(.CONTSAMEGRADIENT.,#64,IFCLENGTHMEASURE(-152.4),IFCLENGTHMEASURE(152.4),#68); + + clothoid2 = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((350.24160971216, 242.268527691248)), + file.createIfcDirection((0.741563691346478, 0.670882472327743)), + ), + SegmentStart=file.createIfcLengthMeasure(-152.4), + SegmentLength=file.createIfcLengthMeasure(152.4), + ParentCurve=file.createIfcClothoid( + Position=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((0.0, 0.0)), RefDirection=file.createIfcDirection((1.0, 0.0)) + ), + ClothoidConstant=-481.931115409661, + ), + ) + + # 70=IFCCARTESIANPOINT((459.773476040884,348.208932967387)); + # 71=IFCALIGNMENTHORIZONTALSEGMENT($,$,#70,0.785398163397448,0.,0.,0.,$,.LINE.); + # 72=IFCALIGNMENTSEGMENT('0FlcTrOfT5YBi0fqVhTMyc',$,'H5',$,$,#111,#121,#71); + # 73=IFCDIRECTION((0.707106781186548,0.707106781186548)); + # 74=IFCAXIS2PLACEMENT2D(#70,#73); + # 75=IFCCARTESIANPOINT((0.,0.)); + # 76=IFCDIRECTION((1.,0.)); + # 77=IFCVECTOR(#76,1.); + # 78=IFCLINE(#75,#77); + # 79=IFCCURVESEGMENT(.DISCONTINUOUS.,#74,IFCLENGTHMEASURE(0.),IFCLENGTHMEASURE(0.),#78); + + line2 = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((459.773476040884, 348.208932967387)), + file.createIfcDirection((0.707106781186548, 0.707106781186548)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(0.0), + ParentCurve=file.createIfcLine( + Pnt=file.createIfcCartesianPoint((0.0, 0.0)), + Dir=file.createIfcVector(Orientation=file.createIfcDirection((1.0, 0.0)), Magnitude=1.0), + ), + ) + + composite_curve = file.createIfcCompositeCurve(Segments=[], SelfIntersect=False) + + # add_segment_to_curve calls update_curve_segment_transition_code + ifcopenshell.api.alignment.add_segment_to_curve(file, line1, composite_curve) + assert line1.Transition == "DISCONTINUOUS" + + ifcopenshell.api.alignment.add_segment_to_curve(file, clothoid1, composite_curve) + assert line1.Transition == "CONTSAMEGRADIENTSAMECURVATURE" + assert clothoid1.Transition == "DISCONTINUOUS" + + ifcopenshell.api.alignment.add_segment_to_curve(file, circular_arc, composite_curve) + assert clothoid1.Transition == "CONTSAMEGRADIENTSAMECURVATURE" + assert circular_arc.Transition == "DISCONTINUOUS" + + ifcopenshell.api.alignment.add_segment_to_curve(file, clothoid2, composite_curve) + assert circular_arc.Transition == "CONTSAMEGRADIENTSAMECURVATURE" + assert clothoid2.Transition == "DISCONTINUOUS" + + ifcopenshell.api.alignment.add_segment_to_curve(file, line2, composite_curve) + assert clothoid2.Transition == "CONTSAMEGRADIENTSAMECURVATURE" + assert line2.Transition == "DISCONTINUOUS" + + +def test_update_curve_segment_transition_code(): + _test1() + _test2() diff --git a/src/ifcparse/IfcAlignmentHelper.cpp b/src/ifcparse/IfcAlignmentHelper.cpp index a2953c16c2..b5936561a5 100644 --- a/src/ifcparse/IfcAlignmentHelper.cpp +++ b/src/ifcparse/IfcAlignmentHelper.cpp @@ -718,7 +718,7 @@ std::pair mapAlign // dy/dx = B + 2Cx auto dx = cos(atan(start_gradient)); auto dy = sin(atan(start_gradient)); - auto curve_length_fn = [B, C](double x) { return sqrt(1 + pow(B + C * x, 2)); }; + auto curve_length_fn = [B, C](double x) { return sqrt(1 + pow(B + 2*C * x, 2)); }; auto segment_curve_length = boost::math::quadrature::trapezoidal(curve_length_fn, 0.0, horizontal_length); auto curve_segment = new Ifc4x3_add2::IfcCurveSegment(