Import alignment from csv (#6234)

* Start of the official alignment API

* Import alignment into bonsai model using CSV file
This commit is contained in:
Richard Brice
2025-03-14 07:31:50 -07:00
committed by GitHub
parent eed47c8c87
commit 04e07db0a3
59 changed files with 7369 additions and 1311 deletions
+1
View File
@@ -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,
@@ -0,0 +1,36 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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)
@@ -0,0 +1,113 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>, 2022 Yassine Oualid <yassine@sigmadimensions.com>
#
# 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 <http://www.gnu.org/licenses/>.
# 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))
@@ -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"
+37
View File
@@ -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, <https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/concepts/Object_Composition/Nesting/Alignment_Layouts/Alignment_Layout_-_Horizontal,_Vertical_and_Cant/content.html>`_. Alignments with multiple vertical layouts are modeled per `IFC Concept Template 4.1.4.4.1.2, Alignment Layout - Reusing Horizontal Layout, <https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/concepts/Object_Composition/Nesting/Alignment_Layouts/Alignment_Layout_-_Reusing_Horizontal_Layout/content.html>`_.
Example based on the `FHWA Bridge Geometry Manual <https://www.fhwa.dot.gov/bridge/pubs/hif22034.pdf>`_:
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
+1
View File
@@ -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
+1
View File
@@ -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.
+7 -1
View File
@@ -365,6 +365,12 @@ namespace ifcopenshell {
static constexpr bool defaultvalue = false;
};
struct ComputeCurvature : public SettingBase<ComputeCurvature, bool> {
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<MesherLinearDeflection, MesherAngularDeflection, ReorientShells, LengthUnit, PlaneUnit, Precision, OutputDimensionality, LayersetFirst, DisableBooleanResult, NoWireIntersectionCheck, NoWireIntersectionTolerance, PrecisionFactor, DebugBooleanOperations, BooleanAttempt2d, SurfaceColour, WeldVertices, UseWorldCoords, UnifyShapes, UseMaterialNames, ConvertBackUnits, ContextIds, ContextTypes, ContextIdentifiers, IteratorOutput, DisableOpeningSubtractions, ApplyDefaultMaterials, DontEmitNormals, GenerateUvs, ApplyLayerSets, UseElementHierarchy, ValidateQuantities, EdgeArrows, BuildingLocalPlacement, SiteLocalPlacement, ForceSpaceTransparency, CircleSegments, KeepBoundingBoxes, FunctionStepType, FunctionStepParam, NoParallelMapping, ModelOffset, ModelRotation, TriangulationType, CgalEmitOriginalEdges>
std::tuple<MesherLinearDeflection, MesherAngularDeflection, ReorientShells, LengthUnit, PlaneUnit, Precision, OutputDimensionality, LayersetFirst, DisableBooleanResult, NoWireIntersectionCheck, NoWireIntersectionTolerance, PrecisionFactor, DebugBooleanOperations, BooleanAttempt2d, SurfaceColour, WeldVertices, UseWorldCoords, UnifyShapes, UseMaterialNames, ConvertBackUnits, ContextIds, ContextTypes, ContextIdentifiers, IteratorOutput, DisableOpeningSubtractions, ApplyDefaultMaterials, DontEmitNormals, GenerateUvs, ApplyLayerSets, UseElementHierarchy, ValidateQuantities, EdgeArrows, BuildingLocalPlacement, SiteLocalPlacement, ForceSpaceTransparency, CircleSegments, KeepBoundingBoxes, ComputeCurvature, FunctionStepType, FunctionStepParam, NoParallelMapping, ModelOffset, ModelRotation, TriangulationType, CgalEmitOriginalEdges>
>
{};
}
+49 -7
View File
@@ -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<double> 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<double>&
}
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<ifcopenshell::geometry::settings::ComputeCurvature>().get()) {
m.row(3) = Eigen::Vector4d(0, 0, 0, 1);
}
return m;
}
+4 -11
View File
@@ -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<double> 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:
+218 -130
View File
@@ -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<Eigen::Matrix4d(double)> fn) : fn_(fn) {
}
parent_curve_function& operator=(std::function<Eigen::Matrix4d(double)> fn) {
fn_ = fn;
return *this;
parent_curve_function(std::function<Eigen::Matrix4d(double)> fn, std::function<Eigen::Matrix4d(double)> 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<Eigen::Matrix4d(double)> fn_;
std::function<Eigen::Matrix4d(double)> 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<double(double)> fnX, std::function<double(double)> fnY) {
void set_spiral_function(double s, std::function<double(double)> fnX, std::function<double(double)> fnY, std::function<double(double)> 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<spiral_parent_curve>([start=start_, s, convert_u, fnX, fnY](double u) {
u = convert_u(u+start);
parent_curve_fn_ = std::make_shared<spiral_parent_curve>(
[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<parent_curve_function>([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); });
parent_curve_fn_ = std::make_shared<parent_curve_function>(
[](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<parent_curve_function>([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); });
parent_curve_fn_ = std::make_shared<parent_curve_function>(
[](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<spiral_parent_curve>([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<spiral_parent_curve>(
[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<std::function<double(double)>> 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<parent_curve_function>([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); });
parent_curve_fn_ = std::make_shared<parent_curve_function>(
[](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<parent_curve_function>([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); });
parent_curve_fn_ = std::make_shared<parent_curve_function>(
[](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<std::function<double(double)>> 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<parent_curve_function>([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); });
parent_curve_fn_ = std::make_shared<parent_curve_function>(
[](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<parent_curve_function>([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); });
parent_curve_fn_ = std::make_shared<parent_curve_function>(
[](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); },
[](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); });
}
}
#endif
void polynomial_spiral(boost::optional<double> A0, boost::optional<double> A1, boost::optional<double> A2, boost::optional<double> A3, boost::optional<double> A4, boost::optional<double> A5, boost::optional<double> A6, boost::optional<double> 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<double> A0, boost::optional<double> A1, boost::optional<double> A2, boost::optional<double> A3, boost::optional<double> A4, boost::optional<double> A5, boost::optional<double> A6, boost::optional<double> A7) {
boost::optional<std::function<double(double)>> 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<circle_parent_curve>([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<circle_parent_curve>(
[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<parent_curve_function>([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); });
parent_curve_fn_ = std::make_shared<parent_curve_function>(
[](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<parent_curve_function>([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); });
parent_curve_fn_ = std::make_shared<parent_curve_function>(
[](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<line_parent_curve>([pcX, pcY, pcDx, pcDy, convert_u](double u) {
u = convert_u(u);
parent_curve_fn_ = std::make_shared<line_parent_curve>(
[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<parent_curve_function>([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); });
Logger::Warning(std::runtime_error("Unexpected segment type encountered"));
parent_curve_fn_ = std::make_shared<parent_curve_function>(
[](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<polynomial_parent_curve>([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<const std::vector<double>*, 2> coefficients{&coeffX, &coeffY};
std::array<double, 2> position{0.0, 0.0}; // = SUM(coeff*u^pos)
std::array<double, 2> 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<polynomial_parent_curve>(
[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<const std::vector<double>*, 2> coefficients{&coeffX, &coeffY};
std::array<double, 2> position{0.0, 0.0}; // = SUM(coeff*u^pos)
std::array<double, 2> 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<parent_curve_function>([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); });
parent_curve_fn_ = std::make_shared<parent_curve_function>(
[](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<parent_curve_function>([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); });
parent_curve_fn_ = std::make_shared<parent_curve_function>(
[](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); },
[](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); });
}
}
};
@@ -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<taxonomy::piecewise_function>(map(basis_curve));
auto curve = taxonomy::dcast<taxonomy::function_item>(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();
@@ -43,7 +43,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in
auto csps = inst->CrossSectionPositions();
std::vector<taxonomy::face::ptr> 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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,70 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
"""
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 *
@@ -0,0 +1,76 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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)
@@ -0,0 +1,52 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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)
@@ -0,0 +1,79 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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)
@@ -0,0 +1,198 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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)
@@ -0,0 +1,59 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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)
@@ -0,0 +1,101 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,80 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,116 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,172 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,216 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,76 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,194 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,41 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,40 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,51 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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]
@@ -0,0 +1,44 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,52 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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]
@@ -0,0 +1,45 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,61 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,84 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,439 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,47 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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)
@@ -0,0 +1,62 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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)
@@ -0,0 +1,225 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,42 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,59 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,35 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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)
@@ -0,0 +1,77 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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"
@@ -0,0 +1,134 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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)
@@ -68,6 +68,7 @@ SETTING = Literal[
"building-local-placement",
"cgal-original-edges",
"circle-segments",
"compute-curvature",
"context-identifiers",
"context-ids",
"context-types",
@@ -0,0 +1,72 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,75 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,56 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
)
@@ -0,0 +1,74 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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")
@@ -0,0 +1,70 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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")
@@ -0,0 +1,70 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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")
@@ -0,0 +1,123 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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()
@@ -0,0 +1,26 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,102 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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"
@@ -0,0 +1,795 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
# 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
@@ -0,0 +1,53 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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
@@ -0,0 +1,248 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
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()
+1 -1
View File
@@ -718,7 +718,7 @@ std::pair<Ifc4x3_add2::IfcCurveSegment*, Ifc4x3_add2::IfcCurveSegment*> 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(