First Commit

This commit is contained in:
2026-05-31 10:17:09 +07:00
commit 17a9c69379
4547 changed files with 1170384 additions and 0 deletions
@@ -0,0 +1 @@
pip
@@ -0,0 +1,108 @@
Metadata-Version: 2.4
Name: ifcopenshell
Version: 0.8.5
Summary: Python bindings, utility functions, and high-level API for IfcOpenShell
Author-email: Dion Moult <dion@thinkmoult.com>
Project-URL: Homepage, http://ifcopenshell.org
Project-URL: Bug Tracker, https://github.com/ifcopenshell/ifcopenshell/issues
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
Requires-Python: <3.15,>=3.10
Description-Content-Type: text/markdown
Requires-Dist: shapely
Requires-Dist: numpy
Requires-Dist: isodate
Requires-Dist: python-dateutil
Requires-Dist: lark
Requires-Dist: typing-extensions
Provides-Extra: advanced
Requires-Dist: networkx; extra == "advanced"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: tabulate; extra == "dev"
IfcOpenShell
============
<p align="center">
<img src="https://github.com/IfcOpenShell/IfcOpenShell/assets/88302/34901387-e2dd-4a0c-8e38-9ffc32a66cde">
</p>
IfcOpenShell is an open source ([LGPL]) software library for working with Industry Foundation Classes ([IFC]). Complete
parsing support is provided for [IFC2x3 TC1], [IFC4 Add2 TC1], IFC4x1, IFC4x2, and [IFC4x3 Add2]. Extensive geometric support
is implemented for the IFC releases [IFC2x3 TC1] and [IFC4 Add2 TC1]. Extending with support for arbitrary IFC schemas
is possible at compile-time when using C++ and at run-time when using Python.
In addition to a C++ and Python API, IfcOpenShell comes with an ecosystem of tools, notably including IfcConvert (an application
to convert IFC models to other formats), Bonsai (an add-on to Blender providing a graphical IFC authoring platform),
and many other libraries, CLI apps, and more. Support is also provided for auxiliary standards such as BCF and IDS.
For more information, see:
* [IfcOpenShell Website](http://ifcopenshell.org)
* [IfcOpenShell Documentation](https://docs.ifcopenshell.org)
* [IfcOpenShell C++ Installation](https://docs.ifcopenshell.org/ifcopenshell/installation.html)
* [IfcOpenShell Python Installation](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html)
* [IfcOpenShell Python Hello World Tutorial](https://docs.ifcopenshell.org/ifcopenshell-python/hello_world.html)
* [Bonsai Website](https://bonsaibim.org)
* [Bonsai Documentation](https://docs.bonsaibim.org/index.html)
* [Add-on Installation](https://docs.bonsaibim.org/quickstart/installation.html)
* [Exploring an IFC model](https://docs.bonsaibim.org/quickstart/explore_model.html)
Development is sponsored through your generous donations!
[![Open Collective Contributors](https://img.shields.io/opencollective/all/opensourcebim?label=Sponsors&color=22ce5f)](https://opencollective.com/opensourcebim/)
Contents
--------
| Name | Description | License | Service |
| ------------------------- | --------------------------------------------------------------------- | ------------------- | ------- |
| [bcf](https://docs.ifcopenshell.org/bcf.html) | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bcf-client?label=PyPI&color=006dad)](https://pypi.org/project/bcf-client/) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/bcf-client/badges/version.svg)](https://anaconda.org/conda-forge/bcf-client) |
| [bonsai](https://docs.ifcopenshell.org/bonsai.html) | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [![Official](https://img.shields.io/badge/BonsaiBIM.org-Download-70ba35)](https://bonsaibim.org/download.html) [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=bonsai-*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [![Chocolatey](https://img.shields.io/chocolatey/v/blenderbim-nightly?label=Chocolatey&color=5c9fd8)](https://community.chocolatey.org/packages/blenderbim-nightly/) |
| [bsdd](https://docs.ifcopenshell.org/bsdd.html) | Library to query the bSDD API | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bsdd?label=PyPI&color=006dad)](https://pypi.org/project/bsdd/) |
| [ifc2ca](https://docs.ifcopenshell.org/ifc2ca.html) | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
| [ifc4d](https://docs.ifcopenshell.org/ifc4d.html) | Convert to and from IFC and project management software | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc4d?label=PyPI&color=006dad)](https://pypi.org/project/ifc4d/) |
| [ifc5d](https://docs.ifcopenshell.org/ifc5d.html) | Report and optimise cost information from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc5d?label=PyPI&color=006dad)](https://pypi.org/project/ifc5d/) |
| [ifcbimtester](https://docs.ifcopenshell.org/bimtester.html) | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later |
| ifcblender | Historic Blender IFC import add-on | LGPL-3.0-or-later\* |
| [ifccityjson](https://docs.ifcopenshell.org/ifccityjson.html) | Convert CityJSON to IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccityjson?label=PyPI&color=006dad)](https://pypi.org/project/ifccityjson/) |
| [ifcclash](https://docs.ifcopenshell.org/ifcclash.html) | Clash detection library and CLI app | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcclash?label=PyPI&color=006dad)](https://pypi.org/project/ifcclash/) |
| [ifcconvert](https://docs.ifcopenshell.org/ifcconvert.html) | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcconvert/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcconvert-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
| [ifccsv](https://docs.ifcopenshell.org/ifccsv.html) | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccsv?label=PyPI&color=006dad)](https://pypi.org/project/ifccsv/) |
| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcdiff?label=PyPI&color=006dad)](https://pypi.org/project/ifcdiff/) |
| [ifcedit](https://docs.ifcopenshell.org/ifcedit.html) | CLI wrapper for ifcopenshell.api IFC model mutation functions | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcedit?label=PyPI&color=006dad)](https://pypi.org/project/ifcedit/) |
| [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcfm?label=PyPI&color=006dad)](https://pypi.org/project/ifcfm/) |
| [ifcmax](https://docs.ifcopenshell.org/ifcmax.html) | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcmax.html)
| [ifcmcp](https://docs.ifcopenshell.org/ifcmcp.html) | MCP server for querying and editing IFC building models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcmcp?label=PyPI&color=006dad)](https://pypi.org/project/ifcmcp/) |
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) [![Pyodide WASM Wheels tag](https://img.shields.io/github/v/tag/ifcopenshell/wasm-wheels?sort=semver&label=pyodide-wasm-wheels)](https://github.com/IfcOpenShell/wasm-wheels) |
| [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcpatch?label=PyPI&color=006dad)](https://pypi.org/project/ifcpatch/) |
| [ifcquery](https://docs.ifcopenshell.org/ifcquery.html) | CLI tool for querying and inspecting IFC building models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcquery?label=PyPI&color=006dad)](https://pypi.org/project/ifcquery/) |
| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcsverchok-*.*.*.*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
| [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifctester?label=PyPI&color=006dad)](https://pypi.org/project/ifctester/) |
The IfcOpenShell C++ codebase is split into multiple interal libraries:
| Name | Description | License |
| ------------------------- | --------------------------------------------------------------------- | ------------------- |
| ifcgeom | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| ifcgeom\_schema\_agnostic | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| ifcgeomserver | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| ifcjni | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| ifcparse | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| ifcwrap | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| qtviewer | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
| serializers | Internal library for IfcOpenShell | LGPL-3.0-or-later\* |
[LGPL]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/COPYING.LESSER "LGPL-3.0-or-later"
[IFC]: https://technical.buildingsmart.org/standards/ifc/ "IFC"
[IFC2x3 TC1]: https://standards.buildingsmart.org/IFC/RELEASE/IFC2x3/TC1/HTML/ "IFC2x3 TC1"
[IFC4 Add2 TC1]: https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/ "IFC4 Add2 TC1"
[IFC4x3 Add2]: https://standards.buildingsmart.org/IFC/RELEASE/IFC4_3/ "IFC4x3 Add2"
[Visual Studio]: https://www.visualstudio.com/ "Visual Studio"
[Visual C++ Build Tools]: http://landinghub.visualstudio.com/visual-cpp-build-tools "Visual C++ Build Tools"
[MSYS2]: https://msys2.github.io/ "MSYS2"
[win/readme.md]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/win/readme.md "win/readme.md"
[nix/build-all.py]: https://github.com/IfcOpenShell/IfcOpenShell/tree/master/nix/build-all.py "nix/build-all.py"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: setuptools (82.0.1)
Root-Is-Purelib: true
Tag: py3-none-any
@@ -0,0 +1 @@
ifcopenshell
@@ -0,0 +1,392 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 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/>.
"""Welcome to IfcOpenShell! IfcOpenShell provides a way to read and write IFCs.
IfcOpenShell can open IFC files, read entities (such as walls, buildings,
properties, systems, etc), edit attributes, write out ``.ifc`` files and more.
This module provides primitive functions to interact with IFC, including:
- For most users, you can open and read IFC models, see docs for :func:`open`.
This returns an :class:`file` object representing the IFC model. You can then
query the model to filter elements.
- For developers, you can query the schema itself, see docs for
:func:`schema_by_name`. This returns a schema object which you can use to
analyse the definitions of IFC classes and data types.
You may also be interested in:
- For model authoring and editing operations, see :mod:`ifcopenshell.api`.
- For extracting information from models, see :mod:`ifcopenshell.util`.
- For processing geometry, see :mod:`ifcopenshell.geom`.
For more details, consult https://docs.ifcopenshell.org/
Example:
.. code:: python
import ifcopenshell
print(ifcopenshell.version)
model = ifcopenshell.open("/path/to/model.ifc")
walls = model.by_type("IfcWall")
for wall in walls:
print(wall.Name)
"""
from __future__ import annotations
import os
import sys
import tempfile
import zipfile
from collections.abc import Generator, Sequence
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, Optional, Union, overload
if TYPE_CHECKING:
import ifcopenshell.express.schema_class
if sys.platform != "win32":
platform_system = os.uname()[0].lower()
else:
platform_system = "windows"
if sys.maxsize == (1 << 31) - 1:
platform_architecture = "32bit"
else:
platform_architecture = "64bit"
python_version_tuple = tuple(sys.version.split(" ")[0].split("."))
python_distribution = os.path.join(platform_system, platform_architecture, "python%s.%s" % python_version_tuple[:2])
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "lib", python_distribution)))
try:
from . import ifcopenshell_wrapper
except Exception:
raise ImportError("IfcOpenShell not built for '%s'" % python_distribution)
# `_file`, `_stream` is used only for annotations inside this file,
# see https://github.com/microsoft/pyright/discussions/9065.
from . import guid
from .entity_instance import entity_instance, register_schema_attributes
from .file import file, rocksdb_lazy_instance
from .file import file as _file
from .sql import sqlite, sqlite_entity
# explicitly specify available imported symbols
# (it's a requirement for a typed library)
__all__ = [
"entity_instance",
"file",
"guid",
"ifcopenshell_wrapper",
"rocksdb_lazy_instance",
"sqlite",
"sqlite_entity",
"stream",
"stream_entity",
]
try:
from .stream import stream, stream_entity # ty: ignore[possibly-missing-import]
from .stream import stream as _stream # ty: ignore[possibly-missing-import]
except:
pass
class Error(Exception):
"""Error used when a generic problem occurs"""
pass
class SchemaError(Error):
"""Error used when an IFC schema related problem occurs"""
pass
@overload
def open(
path: Union[os.PathLike, str], format: SupportedFormat = None, *, should_stream: Literal[False] = False
) -> Union[_file, sqlite]: ...
@overload
def open(path: Union[os.PathLike, str], format: SupportedFormat = None, *, should_stream: Literal[True]) -> _stream: ...
@overload
def open(
path: Union[os.PathLike, str],
format: SupportedFormat = None,
*,
should_stream: bool = False,
readonly: bool = False,
) -> Union[_file, sqlite, _stream]: ...
def open(
path: Union[os.PathLike, str],
format: SupportedFormat = None,
should_stream: bool = False,
readonly: bool = False,
mmap: bool = False,
bypass_types: Optional[Sequence[str]] = None,
) -> Union[_file, sqlite, _stream]:
"""Loads an IFC dataset from a filepath
:param should_stream: Whether to open the file in streaming mode. Could be useful
for reading large files.
You can specify a file format. If no format is given, it is guessed from
its extension.
You can then filter by element ID, class, etc, and subscript by id or guid.
Example:
.. code:: python
model = ifcopenshell.open("/path/to/model.ifc")
model = ifcopenshell.open("/path/to/model.ifcXML")
model = ifcopenshell.open("/path/to/model.any_extension", ".ifc")
products = model.by_type("IfcProduct")
print(products[0].id(), products[0].GlobalId) # 122 2XQ$n5SLP5MBLyL442paFx
print(products[0] == model[122] == model["2XQ$n5SLP5MBLyL442paFx"]) # True
"""
path = Path(path)
if not path.exists():
raise FileNotFoundError(f"Path does not exist: '{path}'.")
if format is None:
format = guess_format(path)
if format == ".ifcXML":
f = ifcopenshell_wrapper.parse_ifcxml(str(path.absolute()))
if f:
return file(f)
raise OSError(f"Failed to parse .ifcXML file from {path}")
if format == ".ifcZIP":
with tempfile.TemporaryDirectory() as unzipped_path:
with zipfile.ZipFile(path) as zf:
for name in zf.namelist():
if Path(name).suffix.lower() in (".ifc", ".ifcxml"):
return open(zf.extract(name, unzipped_path))
else:
raise LookupError(f"No .ifc or .ifcXML file found in {path}")
if format == ".ifcSQLite":
return sqlite(path)
if should_stream:
return stream(path)
if readonly: # Temporary conditional see #7131. Remove once newer builds don't segfault on Linux.
f = ifcopenshell_wrapper.open(str(path.absolute()), readonly=readonly)
elif bypass_types:
f = ifcopenshell_wrapper.file(ifcopenshell_wrapper.uninitialized_tag())
for ty in bypass_types:
f.bypass_type(ty)
if mmap:
# mmap parameter is only available for builds with USE_MMAP, not used in our main builds
f.initialize(str(path.absolute()), mmap=mmap) # ty: ignore[unknown-argument]
else:
f.initialize(str(path.absolute()))
elif mmap:
# mmap parameter is only available for builds with USE_MMAP, not used in our main builds
f = ifcopenshell_wrapper.open(str(path.absolute()), mmap=mmap) # ty: ignore[unknown-argument]
else:
f = ifcopenshell_wrapper.open(str(path.absolute()))
return file(f)
def create_entity(type: str, schema: str = "IFC4", *args: Any, **kwargs: Any) -> entity_instance:
"""Creates a new IFC entity that does not belong to an IFC file object
Note that it is more common to create entities within a existing file
object. See :meth:`ifcopenshell.file.create_entity`.
:param type: Case insensitive name of the IFC class
:param schema: The IFC schema identifier
:param args: The positional arguments of the IFC class
:param kwargs: The keyword arguments of the IFC class
:returns: An entity instance
Example:
.. code:: python
person = ifcopenshell.create_entity("IfcPerson") # #0=IfcPerson($,$,$,$,$,$,$,$)
model = ifcopenshell.file()
model.add(person) # #1=IfcPerson($,$,$,$,$,$,$,$)
"""
e = entity_instance((schema, type))
attrs = list(enumerate(args)) + [(e.wrapped_data.get_argument_index(name), arg) for name, arg in kwargs.items()]
for idx, arg in attrs:
e[idx] = arg
return e
def register_schema(schema: ifcopenshell.express.schema_class.SchemaClass) -> None:
"""Registers a custom IFC schema
:param schema: A schema object
Example:
.. code:: python
schema = ifcopenshell.express.parse("/path/to/ifc-custom.exp")
ifcopenshell.register_schema(schema)
ifcopenshell.file(schema="IFC_CUSTOM")
"""
schema.schema.this.disown()
schema.disown()
ifcopenshell_wrapper.register_schema(schema.schema)
register_schema_attributes(schema.schema)
def schema_by_name(
schema: Optional[str] = None,
schema_version: Optional[tuple[int, ...]] = None,
) -> ifcopenshell_wrapper.schema_definition:
"""Returns an object allowing you to query the IFC schema itself
:param schema: Which IFC schema to use, chosen from "IFC2X3", "IFC4",
or "IFC4X3". These refer to the ISO approved versions of IFC.
E.g. from ``ifcopenshell.file.schema_identifier``.
Passing ``ifcopenshell.file.schema`` also will work but may result
in not precisely matching schema but it's only conern if you're
using not one of the main schemas.
:param schema_version: If you want to specify an exact version of IFC
that may not be an ISO approved version, use this argument instead
of ``schema``. IFC versions on technical.buildingsmart.org are
described using 4 integers representing the major, minor, addendum,
and corrigendum number. For example, (4, 0, 2, 1) refers to IFC4
ADD2 TC1, which is the official version approved by ISO when people
refer to "IFC4". Generally you should not use this argument unless
you are testing non-ISO IFC releases.
:return: Schema definition object.
"""
assert schema_version or schema, "Either schema or schema_version must be specified."
if schema_version:
prefixes = ("IFC", "X", "_ADD", "_TC")
schema = "".join("".join(map(str, t)) if t[1] else "" for t in zip(prefixes, schema_version))
else:
schema = {"IFC4X3": "IFC4X3_ADD2"}.get(schema, schema)
return ifcopenshell_wrapper.schema_by_name(schema)
SupportedFormat = Literal[".ifc", ".ifcZIP", ".ifcXML", ".ifcJSON", ".ifcSQLite", "rocksdb", None]
def guess_format(path: Path) -> SupportedFormat:
"""Guesses the IFC format using file extension
IFCs may be serialised as different formats. The most common is a ``.ifc``
file, which is plaintext and stores data using the STEP Physical File
format. IFC can also be stored as a Zipfile, XML, JSON, or SQL.
This will return the canonical form of the format. For example, if a path
has the extension of .xml or .ifcxml (case insensitive), it will return
.ifcXML.
Users generally won't call this function. The :func:`open` function uses
this internally to guess the file format.
"""
suffix = path.suffix.lower()
if path.is_dir():
return "rocksdb"
elif suffix == ".ifc":
return ".ifc"
elif suffix in (".ifczip", ".zip"):
return ".ifcZIP"
elif suffix in (".ifcxml", ".xml"):
return ".ifcXML"
elif suffix in (".ifcjson", ".json"):
return ".ifcJSON"
elif suffix in (".ifcsqlite", ".sqlite", ".db"):
return ".ifcSQLite"
return None
def stream2(path: Union[Path, str], mmap: bool = False, page_size: int = 0):
"""Streams the content of a file path from disk, yielding each instance
as a dictionary.
:param path: Input file path
:param mmap: Open the file contents using memory mapping
:param page_size: Open file in python and feed chunks to the parser.
:yield: Entity instance dictionaries
"""
if page_size:
import builtins
f = builtins.open(path, encoding="ascii")
strm = ifcopenshell_wrapper.InstanceStreamer()
strm.pushPage(f.read(page_size))
finished = False
while True:
while strm.hasSemicolon():
if inst := strm.readInstancePy():
yield inst
else:
finished = True
break
if finished:
break
else:
if data := f.read(page_size):
strm.pushPage(data)
else:
break
else:
streamer = ifcopenshell_wrapper.InstanceStreamer(str(path), mmap)
while streamer:
if inst := streamer.readInstancePy():
yield inst
def stream2_from_string(data: str) -> Generator[dict]:
"""Streams the content of a file path from string, yielding each instance
as a dictionary.
:param data: Input data string
:yield: entity instance dictionaries
"""
streamer = ifcopenshell_wrapper.stream_from_string(data)
while streamer:
if inst := streamer.read_instance_py():
yield inst
def convert_path_to_rocksdb(ifcspf_path: Union[Path, str], rocksdb_path: Union[Path, str]) -> None:
"""Converts an IFC-SPF file on disk to the IfcOpenShell-specific
RocksDB encoding. RocksDB is an embedded key-value store that allows
partial reads and is therefore more memory efficient with larger files.
:param ifcspf_path: Input file path - needs to exist
:param rocksdb_path: RocksDB file path (directory) - may exist, but result may then be invalid
"""
ser = ifcopenshell_wrapper.RocksDbSerializer(str(ifcspf_path), str(rocksdb_path), True)
ser.finalize()
version_core = ifcopenshell_wrapper.version()
__version__ = version = "0.8.5"
get_log = ifcopenshell_wrapper.get_log
@@ -0,0 +1,314 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 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/>.
"""High level IFC authoring and editing functions
Authoring, editing, and deleting IFC data requires a detailed understanding of
the rules of the IFC schema. This API module provides simple to use authoring
functions that hide this complexity from you. Things like managing differences
between IFC versions, tracking owernship changes, or cleaning up after orphaned
relationships are all handled automatically.
If you're new to IFC authoring, start by looking at the following APIs:
- See :func:`ifcopenshell.api.project.create_file` to create a new IFC.
- See :func:`ifcopenshell.api.root.create_entity` to create new entities, like
the mandatory IfcProject, and then an IfcSite, IfcWall, etc.
- See :func:`ifcopenshell.api.aggregate.assign_object` to create a spatial
hierarchy.
- See :func:`ifcopenshell.api.spatial.assign_container` to place physical
elements (e.g. walls) inside spatial elements (e.g. building storeys).
Also see how to `create a simple model from scratch
<https://docs.ifcopenshell.org/ifcopenshell-python/code_examples.html#create-a-simple-model-from-scratch>`_.
"""
import importlib
import inspect
import json
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, Optional
import numpy
import ifcopenshell
if TYPE_CHECKING:
import ifcopenshell.api
pre_listeners: dict[str, dict] = {}
post_listeners: dict[str, dict] = {}
def batching_argument_deprecation(
usecase_path: str, settings: dict, prev_argument: str, new_argument: str, replace_usecase: Optional[str] = None
) -> tuple[str, dict]:
if replace_usecase is not None:
print(f"WARNING. `{usecase_path}` api method is deprecated and should be replaced with `{replace_usecase}`.")
if prev_argument in settings:
print(
f"WARNING. `{prev_argument}` argument is deprecated for API method "
f'"{usecase_path}" and should be replaced with `{new_argument}`.'
)
settings = settings | {new_argument: [settings[prev_argument]]}
settings.pop(prev_argument)
return (replace_usecase or usecase_path, settings)
def renamed_arguments_deprecation(
usecase_path: str, settings: dict, arguments_remapped: dict[str, str]
) -> tuple[str, dict]:
for prev_argument, new_argument in arguments_remapped.items():
if prev_argument in settings:
print(
f"WARNING. `{prev_argument}` argument is deprecated for API method "
f'"{usecase_path}" and should be replaced with `{new_argument}`.'
)
settings = settings | {new_argument: settings[prev_argument]}
settings.pop(prev_argument)
return (usecase_path, settings)
# Example item:
# "group.add_group": partial(
# renamed_arguments_deprecation, arguments_remapped={"Name": "name", "Description": "description"}
# ),
ARGUMENTS_DEPRECATION: dict[str, Callable[[str, dict[str, Any]], tuple[str, dict[str, Any]]]] = {}
CACHED_USECASE_CLASSES: dict[str, Callable] = {}
CACHED_USECASES: dict[str, Callable] = {}
def run(
usecase_path: str,
ifc_file: Optional[ifcopenshell.file] = None,
should_run_listeners: bool = True,
**settings: Any,
) -> Any:
"""This is deprecated and will be removed in a future version. Do not use this function."""
usecase_function = CACHED_USECASES.get(usecase_path)
if not usecase_function:
importlib.import_module(f"ifcopenshell.api.{usecase_path}")
module, usecase = usecase_path.split(".")
usecase_function = getattr(getattr(ifcopenshell.api, module), usecase)
CACHED_USECASES[usecase_path] = usecase_function
if ifc_file:
return usecase_function(ifc_file, should_run_listeners=should_run_listeners, **settings)
return usecase_function(should_run_listeners=should_run_listeners, **settings)
if should_run_listeners:
for listener in pre_listeners.get(usecase_path, {}).values():
listener(usecase_path, ifc_file, settings)
usecase_class = CACHED_USECASE_CLASSES.get(usecase_path)
if usecase_class is None:
importlib.import_module(f"ifcopenshell.api.{usecase_path}")
module, usecase = usecase_path.split(".")
usecase_class = getattr(getattr(getattr(ifcopenshell.api, module), usecase), "Usecase")
CACHED_USECASE_CLASSES[usecase_path] = usecase_class
if ifc_file:
result = usecase_class(ifc_file, **settings).execute()
else:
result = usecase_class(**settings).execute()
if should_run_listeners:
for listener in post_listeners.get(usecase_path, {}).values():
listener(usecase_path, ifc_file, settings)
return result
def add_pre_listener(usecase_path: str, name: str, callback: Callable[[str, ifcopenshell.file, dict], None]) -> None:
"""Add a pre listener
:param usecase_path: string, ifcopenshell api use case path
:param name: string, name of listener
:param callback: callback function with 3 arguments: `usecase_path`, `ifc_file`, `settings`
"""
pre_listeners.setdefault(usecase_path, {})[name] = callback
def add_post_listener(usecase_path: str, name: str, callback: Callable[[str, ifcopenshell.file, dict], None]) -> None:
"""Add a post listener
:param usecase_path: string, ifcopenshell api use case path
:param name: string, name of listener
:param callback: callback function with 3 arguments: `usecase_path`, `ifc_file`, `settings`
"""
post_listeners.setdefault(usecase_path, {})[name] = callback
def remove_pre_listener(usecase_path: str, name: str, callback: Callable[[str, ifcopenshell.file, dict], None]) -> None:
"""Remove a pre listener
:param usecase_path: string, ifcopenshell api use case path
:param name: string, name of listener
:param callback: callback function with 3 arguments: `usecase_path`, `ifc_file`, `settings`
"""
pre_listeners.get(usecase_path, {}).pop(name, None)
def remove_post_listener(
usecase_path: str, name: str, callback: Callable[[str, ifcopenshell.file, dict], None]
) -> None:
"""Remove a post listener
:param usecase_path: string, ifcopenshell api use case path
:param name: string, name of listener
:param callback: callback function with 3 arguments: `usecase_path`, `ifc_file`, `settings`
"""
post_listeners.get(usecase_path, {}).pop(name, None)
def remove_all_listeners():
pre_listeners.clear()
post_listeners.clear()
def extract_docs(module: str, usecase: str) -> dict[str, Any]:
import collections
import typing
inputs = collections.OrderedDict()
function_init = getattr(getattr(ifcopenshell.api, module), usecase).Usecase.__init__
function_execute = getattr(getattr(ifcopenshell.api, module), usecase).Usecase.execute
node_data = {"module": module, "usecase": usecase}
signature = inspect.signature(function_init)
for name, parameter in signature.parameters.items():
if name == "self":
continue
inputs[name] = {"name": name}
if isinstance(parameter.default, (str, float, int, bool)):
inputs[name]["default"] = parameter.default
type_hints = typing.get_type_hints(function_init)
for name, socket_data in inputs.items():
type_hint = type_hints[name]
if isinstance(type_hint, typing._UnionGenericAlias): # pyright: ignore[reportAttributeAccessIssue]
inputs[name]["type"] = [t.__name__ for t in typing.get_args(type_hint)]
else:
inputs[name]["type"] = type_hint.__name__
description = ""
for i, line in enumerate(function_init.__doc__.split("\n")):
line = line.strip()
if i == 0:
node_data["name"] = line
elif line.startswith(":return:"):
node_data["output"] = {"name": line.split(":")[2].strip(), "description": line.split(":")[3].strip()}
elif line.startswith(":param"):
param_name = line.split(":")[1].strip().replace("param ", "")
inputs[param_name]["description"] = line.split(":")[2].strip()
elif i >= 2:
description += line
if "output" in node_data:
node_data["output"]["type"] = typing.get_type_hints(function_execute)["return"].__name__
node_data["description"] = description.strip()
node_data["inputs"] = inputs
return node_data
def serialise_settings(settings):
def serialise_entity_instance(entity):
return {"cast_type": "entity_instance", "value": entity.id(), "Name": getattr(entity, "Name", None)}
vcs_settings = settings.copy()
for key, value in settings.items():
if isinstance(value, ifcopenshell.entity_instance):
vcs_settings[key] = serialise_entity_instance(value)
elif isinstance(value, numpy.ndarray):
vcs_settings[key] = {"cast_type": "ndarray", "value": value.tolist()}
elif isinstance(value, list) and value and isinstance(value[0], ifcopenshell.entity_instance):
vcs_settings[key] = [serialise_entity_instance(i) for i in value]
else:
try:
vcs_settings[key] = str(value)
except:
vcs_settings[key] = "n/a"
try:
return json.dumps(vcs_settings)
except:
return str(vcs_settings)
def wrap_usecase(usecase_path, usecase):
"""Wraps an API function in pre/post listeners."""
def wrapper(*args, should_run_listeners: bool = True, **settings):
ifc_file = args[0] if args else None
nonlocal usecase_path
if should_run_listeners:
listeners = list(pre_listeners.get(usecase_path, {}).values())
listeners += pre_listeners.get("*", {}).values()
for listener in listeners:
listener(usecase_path, ifc_file, settings)
# see #4531
if usecase_path in ARGUMENTS_DEPRECATION:
usecase_path, settings = ARGUMENTS_DEPRECATION[usecase_path](usecase_path, settings)
try:
result = usecase(*args, **settings)
except TypeError as e:
if not e.args[0].startswith(f"{usecase.__name__}()"):
# signature errors typically start with function name
# e.g. "TypeError: edit_library() got an unexpected keyword argument 'test'"
# otherwise it's an error inside api call and we shouldn't get in the way
raise e
msg = (
f"Incorrect function arguments provided for {usecase_path}\n{str(e)}. "
f"You specified args {args} and settings {settings}\n\n"
f"Correct signature is {inspect.signature(usecase)}\n"
f"See help(ifcopenshell.api.{usecase_path}) for documentation."
)
raise TypeError(msg) from e
if should_run_listeners:
listeners = list(post_listeners.get(usecase_path, {}).values())
listeners += post_listeners.get("*", {}).values()
for listener in listeners:
listener(usecase_path, ifc_file, settings)
return result
wrapper.__signature__ = inspect.signature(usecase)
wrapper.__doc__ = usecase.__doc__
wrapper.__name__ = usecase_path
return wrapper
def wrap_usecases(path, name):
"""This developer feature wraps an API module's usecases with listeners."""
import pkgutil
import sys
module_name = name.split(".")[-1]
module = sys.modules[name]
for loader, usecase_name, is_pkg in pkgutil.iter_modules(path):
# We may not be able to get the usecase if we are missing a dependency.
usecase = getattr(module, usecase_name, None)
if callable(usecase):
usecase_path = f"{module_name}.{usecase_name}"
setattr(module, usecase_name, wrap_usecase(usecase_path, usecase))
@@ -0,0 +1,35 @@
# 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/>.
"""Aggregates is the concept of breaking down larger wholes into smaller parts.
For example, spatial elements such as sites are broken down into one or more
buildings, and a building is broken down into storeys. Another example is for
physical elements, such as how a wall is made out of members and coverings.
"""
from .. import wrap_usecases
from .assign_object import assign_object
from .unassign_object import unassign_object
wrap_usecases(__path__, __name__)
__all__ = [
"assign_object",
"unassign_object",
]
@@ -0,0 +1,160 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
from typing import Union
import ifcopenshell
import ifcopenshell.api.geometry
import ifcopenshell.api.owner
import ifcopenshell.api.spatial
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.placement
def assign_object(
file: ifcopenshell.file,
products: list[ifcopenshell.entity_instance],
relating_object: ifcopenshell.entity_instance,
) -> Union[ifcopenshell.entity_instance, None]:
"""Assigns object as an aggregate to the products
All physical IFC model elements must be part of a hierarchical tree
called the "spatial decomposition", where large things are made up of
smaller things. This tree always begins at an "IfcProject" and is then
broken down using "decomposition" relationships, of which aggregation is
the first relationship you will use.
Typically used when you want to describe how large spaces are made up of
smaller spaces. For example large spatial elements (e.g. sites,
buidings) can be made out of smaller spatial elements (e.g. storeys,
spaces).
The largest space (typically the IfcSite) can then be aggregated in a
project. It is requirement for all spatial structures to be directly or
indirectly aggregated back to the IfcProject to create a hierarchy of
spaces.
The other common usecase is when larger physical products are made up of
smaller physical products. For example, a stair might be made out of a
flight, a landing, a railing and so on. Or a wall might be made out of
stud members, and coverings.
As a product may only have a single location in the "spatial
decomposition" tree, assigning an aggregate relationship will remove any
previous aggregation, containment, or nesting relationships it may have.
IFC placements follow a convention where the placement is relative to
its parent in the spatial hierarchy. If your product has a placement,
its placement will be recalculated to follow this convention.
:param products: The list of parts of the aggregate, typically of IfcElement or
IfcSpatialStructureElement subclass
:param relating_object: The whole of the aggregate, typically an
IfcElement or IfcSpatialStructureElement subclass
:return: The IfcRelAggregate relationship instance
or `None` if `products` was empty list.
Example:
.. code:: python
project = ifcopenshell.api.root.create_entity(model, ifc_class="IfcProject")
element = ifcopenshell.api.root.create_entity(model, ifc_class="IfcSite")
subelement = ifcopenshell.api.root.create_entity(model, ifc_class="IfcBuilding")
# The project contains a site (note that project aggregation is a special case in IFC)
ifcopenshell.api.aggregate.assign_object(model, products=[element], relating_object=project)
# The site has a building
ifcopenshell.api.aggregate.assign_object(model, products=[subelement], relating_object=element)
"""
if not products:
return
products_set = set(products)
is_decomposed_by = next((i for i in relating_object.IsDecomposedBy if i.is_a("IfcRelAggregates")), None)
previous_aggregates_rels: set[ifcopenshell.entity_instance] = set()
products_without_aggregates: list[ifcopenshell.entity_instance] = []
products_with_aggregates: list[ifcopenshell.entity_instance] = []
# check if there is anything to change
for product in products_set:
product_rel = next(iter(product.Decomposes), None)
if product_rel is None:
products_without_aggregates.append(product)
continue
# either is_decomposed_by is None or product is part of different rel
if product_rel != is_decomposed_by:
previous_aggregates_rels.add(product_rel)
products_with_aggregates.append(product)
# products with already assigned aggregates will be skipped
products_to_change = products_without_aggregates + products_with_aggregates
# nothing to change
if not products_to_change:
return is_decomposed_by
# can be either only aggregated or only contained at the same time
# some product might not be able to have a container
possibly_contained_products = [p for p in products_without_aggregates if hasattr(p, "ContainedInStructure")]
ifcopenshell.api.spatial.unassign_container(file, products=possibly_contained_products)
# unassign elements from previous aggregates
for decomposes in previous_aggregates_rels:
related_objects = set(decomposes.RelatedObjects) - products_set
if related_objects:
decomposes.RelatedObjects = list(related_objects)
ifcopenshell.api.owner.update_owner_history(file, element=decomposes)
else:
history = decomposes.OwnerHistory
file.remove(decomposes)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
# assign elements to a new aggregate
if is_decomposed_by:
is_decomposed_by.RelatedObjects = list(set(is_decomposed_by.RelatedObjects) | products_set)
ifcopenshell.api.owner.update_owner_history(file, element=is_decomposed_by)
else:
is_decomposed_by = file.create_entity(
"IfcRelAggregates",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.owner.create_owner_history(file),
"RelatedObjects": list(products_set),
"RelatingObject": relating_object,
}
)
# localize placement relative to a new aggregate for affected products
for product in products_to_change:
placement = getattr(product, "ObjectPlacement", None)
if placement and placement.is_a("IfcLocalPlacement"):
ifcopenshell.api.geometry.edit_object_placement(
file,
product=product,
matrix=ifcopenshell.util.placement.get_local_placement(product.ObjectPlacement),
is_si=False,
)
return is_decomposed_by
@@ -0,0 +1,75 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.api.owner
import ifcopenshell.util.element
def unassign_object(file: ifcopenshell.file, products: list[ifcopenshell.entity_instance]) -> None:
"""Unassigns products from their aggregate
A product (i.e. a smaller part of a whole) may be aggregated into zero
or one larger space or element. This function will remove that
aggregation relationship.
As all physical IFC model elements must be part of a hierarchical tree
called the "spatial decomposition", using this function will remove the
product from that tree. This is a dangerous operation and may result in
the product no longer being visible in IFC applications.
If the product is not part of an aggregation relationship, nothing will
happen.
:param products: The list of parts of the aggregate, typically of IfcElements or
IfcSpatialStructureElement subclass
:return: None
Example:
.. code:: python
element = ifcopenshell.api.root.create_entity(model, ifc_class="IfcSite")
subelement1 = ifcopenshell.api.root.create_entity(model, ifc_class="IfcBuilding")
subelement2 = ifcopenshell.api.root.create_entity(model, ifc_class="IfcBuilding")
ifcopenshell.api.aggregate.assign_object(model, products=[subelement1], relating_object=element)
ifcopenshell.api.aggregate.assign_object(model, products=[subelement2], relating_object=element)
# nothing is returned
ifcopenshell.api.aggregate.unassign_object(model, products=[subelement1])
# nothing is returned, relationship is removed
ifcopenshell.api.aggregate.unassign_object(model, products=[subelement2])
"""
settings = {"products": products}
products = set(settings["products"])
rels = set(
rel
for product in products
if (rel := next((rel for rel in product.Decomposes if rel.is_a("IfcRelAggregates")), None))
)
for rel in rels:
related_objects = set(rel.RelatedObjects) - products
if related_objects:
rel.RelatedObjects = list(related_objects)
ifcopenshell.api.owner.update_owner_history(file, element=rel)
else:
history = rel.OwnerHistory
file.remove(rel)
if history:
ifcopenshell.util.element.remove_deep2(file, history)
@@ -0,0 +1,129 @@
# 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 (semantic definition) and alignment geometry (geometric definition).
This API is defined in terms of the semantic definition of an alignment. The corresponding geometric definition
is created and maintained automatically. The manditory zero length segment for the semantic and geometric definitions
are automatically created and maintained.
Alignments are created with stationing referents. Each layout segment is assigned a position referent that informs about
the start point of the segment. An example is the point of curvature of a horizontal circular curve. The referent is
nested to the segment representing the circular arc and is named with a indicator of the position and the station, e.g. "P.C. (145+98.32)"
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. Creating alignments segment by segment.
3. Automatic creation of geometric definitions (IfcCompositeCurve, IfcGradientCurve, IfcSegmentedReferenceCurve)
4. Automatic definition of stationing
5. Automatic definition of alignment transition point referents
6. Utility functions for printing business logical and geometric representations, as well as minimal geometry evaluations
Future versions of this API may 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 ._get_segment_start_point_label import register_referent_name_callback
from .add_stationing_referent import add_stationing_referent
from .add_vertical_layout import add_vertical_layout
from .add_zero_length_segment import add_zero_length_segment
from .create import create
from .create_as_offset_curve import create_as_offset_curve
from .create_as_polyline import create_as_polyline
from .create_by_pi_method import create_by_pi_method
from .create_from_csv import create_from_csv
from .create_layout_segment import create_layout_segment
from .create_representation import create_representation
from .create_segment_representations import create_segment_representations
from .distance_along_from_station import distance_along_from_station
from .get_alignment import get_alignment
from .get_alignment_layout_nest import get_alignment_layout_nest
from .get_alignment_layouts import get_alignment_layouts
from .get_alignment_segment_nest import get_alignment_segment_nest
from .get_alignment_start_station import get_alignment_start_station
from .get_axis_subcontext import get_axis_subcontext
from .get_basis_curve import get_basis_curve
from .get_cant_layout import get_cant_layout
from .get_child_alignments import get_child_alignments
from .get_curve import get_curve
from .get_curve_segment_transition_code import get_curve_segment_transition_code
from .get_horizontal_layout import get_horizontal_layout
from .get_layout_curve import get_layout_curve
from .get_layout_segments import get_layout_segments
from .get_mapped_segments import get_mapped_segments
from .get_parent_alignment import get_parent_alignment
from .get_referent_nest import get_referent_nest
from .get_vertical_layout import get_vertical_layout
from .has_zero_length_segment import has_zero_length_segment
from .layout_horizontal_alignment_by_pi_method import (
layout_horizontal_alignment_by_pi_method,
)
from .layout_vertical_alignment_by_pi_method import (
layout_vertical_alignment_by_pi_method,
)
from .name_segments import name_segments
from .update_fallback_position import update_fallback_position
from .util import *
__all__ = [
"add_stationing_referent",
"add_vertical_layout",
"add_zero_length_segment",
"create",
"create_as_offset_curve",
"create_as_polyline",
"create_by_pi_method",
"create_from_csv",
"create_layout_segment",
"create_representation",
"create_segment_representations",
"distance_along_from_station",
"get_alignment",
"get_alignment_layout_nest",
"get_alignment_layouts",
"get_alignment_segment_nest",
"get_alignment_start_station",
"get_axis_subcontext",
"get_basis_curve",
"get_cant_layout",
"get_child_alignments",
"get_curve",
"get_curve_segment_transition_code",
"get_horizontal_layout",
"get_layout_curve",
"get_layout_segments",
"get_parent_alignment",
"get_referent_nest",
"get_vertical_layout",
"has_zero_length_segment",
"layout_horizontal_alignment_by_pi_method",
"layout_vertical_alignment_by_pi_method",
"name_segments",
"register_referent_name_callback",
"update_fallback_position",
"get_mapped_segments",
]
@@ -0,0 +1,146 @@
# 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 numpy as np
import ifcopenshell
import ifcopenshell.api.alignment
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
import ifcopenshell.util.unit
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._map_alignment_cant_segment import (
_map_alignment_cant_segment,
)
from ifcopenshell.api.alignment._map_alignment_horizontal_segment import (
_map_alignment_horizontal_segment,
)
from ifcopenshell.api.alignment._map_alignment_vertical_segment import (
_map_alignment_vertical_segment,
)
from ifcopenshell.api.alignment._update_curve_segment_transition_code import (
_update_curve_segment_transition_code,
)
def _add_curve_segment_to_composite_curve(
file: ifcopenshell.file, curve_segment: entity_instance, composite_curve: entity_instance
):
if 0 < len(curve_segment.UsingCurves):
raise TypeError("IfcCurveSegment cannot belong to other curves")
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
curve_segment.Transition = "DISCONTINUOUS"
composite_curve.Segments += (curve_segment,)
assert len(curve_segment.UsingCurves) == 1
else:
zero_length_segment = (
composite_curve.Segments[-1]
if ifcopenshell.api.alignment.has_zero_length_segment(composite_curve)
else None
)
prev_segment = None
if zero_length_segment and 1 < len(composite_curve.Segments):
prev_segment = composite_curve.Segments[-2]
elif zero_length_segment == None:
prev_segment = composite_curve.Segments[-1]
curve_segment.Transition = "CONTINUOUS"
segments = composite_curve.Segments[0:-1]
if zero_length_segment:
segments += (
curve_segment,
zero_length_segment,
)
composite_curve.Segments = []
composite_curve.Segments += segments
else:
composite_curve.Segments += (curve_segment,)
if prev_segment:
_update_curve_segment_transition_code(prev_segment, curve_segment)
if zero_length_segment:
settings = ifcopenshell.geom.settings()
segment_fn = ifcopenshell_wrapper.map_shape(settings, curve_segment.wrapped_data)
segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn)
e = segment_evaluator.evaluate(segment_fn.end())
end = np.array(e)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
# assume IfcAxis2Placement2D
zero_length_segment.Placement.Location.Coordinates = (x, y)
zero_length_segment.Placement.RefDirection.DirectionRatios = (dx, dy)
_update_curve_segment_transition_code(curve_segment, zero_length_segment)
def _add_segment_to_curve(file: ifcopenshell.file, segment: entity_instance, curve: entity_instance) -> None:
"""
Creates an IfcCurveSegment from the IfcAlignmentSegment and adds it to the representation curve. The IfcCurveSegment is added
at the end of the curve, but before the manditory zero length segment. The IfcCurveSegment.Transition for the segment
that preceeds the new segment is updated.
:param segment: The segment to be added to the curve
:param curve: The representation curve receiving the segment
:return: None
"""
expected_types = ["IfcAlignmentSegment"]
if not segment.is_a() in expected_types:
raise TypeError(
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{segment.is_a()}"
)
if segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment") and not curve.is_a("IfcCompositeCurve"):
raise TypeError(f"Expected to see IfcCompositeCurve, instead received '{curve.is_a()}'.")
elif segment.DesignParameters.is_a("IfcAlignmentVerticalSegment") and not curve.is_a("IfcGradientCurve"):
raise TypeError(f"Expected to see IfcGradientCurve, instead received '{curve.is_a()}'.")
elif segment.DesignParameters.is_a("IfcAlignmentCantSegment") and not curve.is_a("IfcSegmentedReferenceCurve"):
raise TypeError(f"Expected to see IfcSegmentedReferenceCurve, instead received '{curve.is_a()}'.")
expected_type = "IfcCompositeCurve"
if not curve.is_a(expected_type):
raise TypeError(f"Expected to see {expected_type}, instead received {curve.is_a()}.")
# map the IfcAlignmentSegment to an IfcCurveSegment (or two in the case of helmert curves)
if segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
mapped_segments = _map_alignment_horizontal_segment(file, segment)
elif segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"):
mapped_segments = _map_alignment_vertical_segment(file, segment)
elif segment.DesignParameters.is_a("IfcAlignmentCantSegment"):
cant_layout = segment.Nests[0].RelatingObject
mapped_segments = _map_alignment_cant_segment(file, segment, cant_layout.RailHeadDistance)
else:
assert False
for mapped_segment in mapped_segments:
if mapped_segment:
_add_curve_segment_to_composite_curve(file, mapped_segment, curve)
@@ -0,0 +1,209 @@
# 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 math
import numpy as np
import ifcopenshell
import ifcopenshell.api.alignment
import ifcopenshell.api.nest
import ifcopenshell.api.pset
import ifcopenshell.geom
import ifcopenshell.util.alignment
import ifcopenshell.util.unit
from ifcopenshell import entity_instance, ifcopenshell_wrapper
from ifcopenshell.api.alignment._add_segment_to_curve import _add_segment_to_curve
from ifcopenshell.api.alignment._get_segment_start_point_label import (
_get_segment_start_point_label,
)
def _add_segment_to_layout(file: ifcopenshell.file, layout: entity_instance, segment: entity_instance) -> None:
"""
Adds an IfcAlignmentSegment to a layout alignment (IfcAlignmentHorizontal/Vertical/Cant). This segment is added at the end
of the layout, before the manditory zero length segment. An IfcCurveSegment is created for the corresponding geometric representation.
:param layout: The layout alignment
:param segment: The segment to be appended
:return: None
"""
expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"]
if not layout.is_a() in expected_types:
raise TypeError(
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}"
)
if not (segment.is_a("IfcAlignmentSegment")):
raise TypeError(f"Expected to see IfcAlignmentSegment, instead received {segment.is_a()}.")
curve = ifcopenshell.api.alignment.get_layout_curve(layout)
# add the new segment to the layout
ifcopenshell.api.nest.assign_object(file, related_objects=[segment], relating_object=layout)
# segment is attached at the end, but this is after the zero length segment
# swap the last two segments
ifcopenshell.api.nest.reorder_nesting(file, segment, -1, -1)
if curve:
# add the new segment to the geometric representation curve
_add_segment_to_curve(file, segment, curve)
# gather information to:
# (1) add a referent at the start of this segment
# (2) update the name of the zero length segment's referent
# get the distance along the alignment to the start of the new segment
dist_along = 0.0
if layout.is_a("IfcAlignmentHorizontal"):
for nest in layout.IsNestedBy:
for seg in nest.RelatedObjects:
if seg.is_a("IfcAlignmentSegment"):
dist_along += seg.DesignParameters.SegmentLength
# the length of the current segment is in dist_along, so subtract it out
dist_along -= segment.DesignParameters.SegmentLength
else:
dist_along = segment.DesignParameters.StartDistAlong
# get the station of the start of the segment
alignment = ifcopenshell.api.alignment.get_alignment(layout)
start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
station = start_station + dist_along
# update the zero length layout segment
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout)
zero_length_segment = segment_nest.RelatedObjects[-1]
mapped_segments = ifcopenshell.api.alignment.get_mapped_segments(segment)
mapped_segment = mapped_segments[0] if mapped_segments[1] == None else mapped_segments[1]
# compute the end point matrix
settings = ifcopenshell.geom.settings()
segment_fn = ifcopenshell_wrapper.map_shape(settings, mapped_segment.wrapped_data)
segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn)
e = segment_evaluator.evaluate(segment_fn.end())
end = np.array(e)
# update the zero length segment semantic representation parameters
if zero_length_segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
x = float(end[0, 3]) / unit_scale
y = float(end[1, 3]) / unit_scale
dx = float(end[0, 0])
dy = float(end[1, 0])
zero_length_segment.DesignParameters.StartPoint.Coordinates = (x, y)
zero_length_segment.DesignParameters.StartDirection = dy / dx
elif zero_length_segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"):
y = float(end[1, 3]) / unit_scale
zero_length_segment.DesignParameters.StartHeight = y
dx = float(end[0, 0])
dy = float(end[1, 0])
zero_length_segment.DesignParameters.StartGradient = dy / dx
zero_length_segment.DesignParameters.EndGradient = zero_length_segment.DesignParameters.StartGradient
else:
z = float(end[2, 3]) / unit_scale
dx = float(end[0, 1])
dy = float(end[1, 1])
dz = float(end[2, 1])
ds = math.sqrt(dx * dx + dy * dy)
slope = dz / ds
railhead = layout.RailHeadDistance
zero_length_segment.DesignParameters.StartCantLeft = z + slope * railhead / 2.0
zero_length_segment.DesignParameters.StartCantRight = z - slope * railhead / 2.0
# updated the referent's name because the referent is now at a new station
start_dist_along = 0.0
if segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
start_dist_along = dist_along + segment.DesignParameters.SegmentLength
else:
start_dist_along = segment.DesignParameters.StartDistAlong + segment.DesignParameters.HorizontalLength
zero_length_segment.DesignParameters.StartDistAlong = start_dist_along
end_referent = zero_length_segment.PositionedRelativeTo[0].RelatingPositioningElement
end_referent.Name = f"{_get_segment_start_point_label(zero_length_segment,None)} ({ifcopenshell.util.alignment.station_as_string(file,start_station+start_dist_along)})"
# update the referent's geometric representation's location
end_referent.ObjectPlacement.RelativePlacement.Location.DistanceAlong.wrappedValue = start_dist_along
settings = ifcopenshell.geom.settings()
basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
curve_fn = ifcopenshell_wrapper.map_shape(settings, basis_curve.wrapped_data)
curve_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, curve_fn)
p = curve_evaluator.evaluate(start_dist_along * unit_scale)
p = np.array(p)
x = float(p[0, 3]) / unit_scale
y = float(p[1, 3]) / unit_scale
z = float(p[2, 3]) / unit_scale
rx = float(p[0, 0])
ry = float(p[1, 0])
rz = float(p[2, 0])
ax = float(p[0, 2])
ay = float(p[1, 2])
az = float(p[2, 2])
end_referent.ObjectPlacement.CartesianPosition.Location.Coordinates = (x, y, z)
end_referent.ObjectPlacement.CartesianPosition.Axis.DirectionRatios = (ax, ay, az)
end_referent.ObjectPlacement.CartesianPosition.RefDirection.DirectionRatios = (rx, ry, rz)
start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
end_referent_station = start_station + start_dist_along
pset_stationing = ifcopenshell.api.pset.add_pset(file, product=end_referent, name="Pset_Stationing")
ifcopenshell.api.pset.edit_pset(file, pset=pset_stationing, properties={"Station": end_referent_station})
# create the start of segment referent
# get the previous segment. Working from the end of the basis curve, -1 is zero length segment
# -2 is the newly added segment, so -3 is the segment occuring just before the newly added segment
prev_segment = segment_nest.RelatedObjects[-3] if 2 < len(segment_nest.RelatedObjects) else None
name = f"{_get_segment_start_point_label(prev_segment,segment)} ({ifcopenshell.util.alignment.station_as_string(file,station)})"
referent = ifcopenshell.api.alignment.add_stationing_referent(
file, alignment, distance_along=dist_along, station=station, name=name, positioned_product=segment
)
if len(curve.Segments) == 2 and layout.is_a("IfcAlignmentHorizontal"):
# this is the first real segment in the horizontal alignment
# update the location of the alignment's stationing referent
alignment = ifcopenshell.api.alignment.get_alignment(layout)
ref_nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
stationing_referent = ref_nest.RelatedObjects[0]
p = curve_evaluator.evaluate(
stationing_referent.ObjectPlacement.RelativePlacement.Location.DistanceAlong.wrappedValue
)
p = np.array(p)
x = float(p[0, 3]) / unit_scale
y = float(p[1, 3]) / unit_scale
z = float(p[2, 3]) / unit_scale
rx = float(p[0, 0])
ry = float(p[1, 0])
rz = float(p[2, 0])
ax = float(p[0, 2])
ay = float(p[1, 2])
az = float(p[2, 2])
stationing_referent.ObjectPlacement.CartesianPosition.Location.Coordinates = (x, y, z)
stationing_referent.ObjectPlacement.CartesianPosition.Axis.DirectionRatios = (ax, ay, az)
stationing_referent.ObjectPlacement.CartesianPosition.RefDirection.DirectionRatios = (rx, ry, rz)
@@ -0,0 +1,58 @@
# 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.alignment
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._get_segment_start_point_label import (
_get_segment_start_point_label,
)
def _add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance) -> None:
"""
Adds a zero length segment to the end of a layout. Also adds a zero length segment to the end of the corresponding geometric curve.
This function depends on the assumptions made in ifcopenshell.api.alignment.create and is called by that function.
This is not a general purpose function.
:param layout: An IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant
:return: None
"""
expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"]
if not layout.is_a() in expected_types:
raise TypeError(
f"Expected layout type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}"
)
if not ifcopenshell.api.alignment.add_zero_length_segment(file, layout, include_referent=False):
return # zero length segment not added, probably because it already exists
curve = ifcopenshell.api.alignment.get_layout_curve(layout)
if curve:
ifcopenshell.api.alignment.add_zero_length_segment(file, curve)
segment_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout)
segment = segment_nest.RelatedObjects[-1]
alignment = ifcopenshell.api.alignment.get_alignment(layout)
station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
name = f"{_get_segment_start_point_label(segment,None)} ({ifcopenshell.util.alignment.station_as_string(file,station)})"
referent = ifcopenshell.api.alignment.add_stationing_referent(file, alignment, 0.0, station, name, segment)
@@ -0,0 +1,158 @@
# 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.geometry
from ifcopenshell import entity_instance
def _create_geometric_representation(file: ifcopenshell.file, alignment: entity_instance) -> None:
"""
Create geometric representation for the alignment and its nested layouts.
There are 5 different cases (the IfcCurve created is indicated):
1) Horizontal only -> IfcCompositeCurve
2) Horizontal + Vertical -> IfcCompositeCurve and IfcGradientCurve
3) Horizontal + Vertical + Cant -> IfcCompositeCurve and IfcSegmentedReferentCurve
4) Vertical only (this occurs when horizontal is reused from a parent alignment) -> IfcGradientCurve
5) Vertical + Cant (this occurs when horizontal is reused from a parent alignment) -> IfcSegmentedReferenceCurve
: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(f"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(Segments=[], SelfIntersect=False)
representation = file.createIfcShapeRepresentation(
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(Segments=[], SelfIntersect=False)
representation = file.createIfcShapeRepresentation(
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="FootPrint",
RepresentationType="Curve2D",
Items=(composite_curve,),
)
ifcopenshell.api.geometry.assign_representation(file, alignment, representation)
gradient_curve = file.createIfcGradientCurve(Segments=[], BaseCurve=composite_curve, SelfIntersect=False)
representation = file.createIfcShapeRepresentation(
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(Segments=[], SelfIntersect=False)
representation = file.createIfcShapeRepresentation(
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="FootPrint",
RepresentationType="Curve2D",
Items=(composite_curve,),
)
ifcopenshell.api.geometry.assign_representation(file, alignment, representation)
gradient_curve = file.createIfcGradientCurve(Segments=[], BaseCurve=composite_curve, SelfIntersect=False)
segmented_reference_curve = file.createIfcSegmentedReferenceCurve(
Segments=[], BaseCurve=gradient_curve, SelfIntersect=False
)
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(Segments=[], SelfIntersect=False)
representation = file.createIfcShapeRepresentation(
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(Segments=[], BaseCurve=base_curve, SelfIntersect=False)
representation = file.createIfcShapeRepresentation(
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(Segments=[], BaseCurve=base_curve, SelfIntersect=False)
segmented_reference_curve = file.createIfcSegmentedReferenceCurve(
Segments=[], BaseCurve=gradient_curve, SelfIntersect=False
)
representation = file.creatIfcShapeRepresentation(
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,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/>.
from collections.abc import Sequence
import ifcopenshell
import ifcopenshell.api.alignment
import ifcopenshell.api.geometry
from ifcopenshell import entity_instance
def _create_offset_curve_representation(
file: ifcopenshell.file, alignment: entity_instance, offsets: Sequence[entity_instance]
) -> None:
"""
Create geometric representation for the alignment based on an IfcOffsetByDistances curve
: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(f"Expected {expected_type} but got {alignment.is_a()}")
expected_type = "IfcPointByDistanceExpression"
for offset in offsets:
if not offset.is_a(expected_type):
raise TypeError(f"Expected {expected_type} but got {offset.is_a()}")
axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file)
basis_curve = offsets[0].BasisCurve # IfcPointByDistanceExpression.BasisCurve
if basis_curve.Dim == 3:
placement = file.createIfcLocalPlacement(
PlacementRelTo=None,
RelativePlacement=file.createIfcAxis2Placement3D(
Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0, 0.0))
),
)
representation_type = "Curve3D"
else:
placement = file.createIfcLocalPlacement(
PlacementRelTo=None,
RelativePlacement=file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0))
),
)
representation_type = "Curve2D"
curve = file.createIfcOffsetCurveByDistances(BasisCurve=basis_curve, OffsetValues=offsets)
representation = file.createIfcShapeRepresentation(
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="Axis",
RepresentationType=representation_type,
Items=(curve,),
)
alignment.ObjectPlacement = placement
ifcopenshell.api.geometry.assign_representation(file, alignment, representation)
@@ -0,0 +1,69 @@
# 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/>.
from collections.abc import Sequence
import ifcopenshell
import ifcopenshell.api.alignment
import ifcopenshell.api.geometry
from ifcopenshell import entity_instance
def _create_polyline_representation(
file: ifcopenshell.file, alignment: entity_instance, points: Sequence[entity_instance]
) -> None:
"""
Create geometric representation for the alignment based on an IfcPolyline
: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(f"Expected {expected_type} but got {alignment.is_a()}")
axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file)
if points[0].Dim == 3:
placement = file.createIfcLocalPlacement(
PlacementRelTo=None,
RelativePlacement=file.createIfcAxis2Placement3D(
Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0, 0.0))
),
)
representation_type = "Curve3D"
else:
placement = file.createIfcLocalPlacement(
PlacementRelTo=None,
RelativePlacement=file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0))
),
)
representation_type = "Curve2D"
curve = file.createIfcPolyLine(Points=points)
representation = file.createIfcShapeRepresentation(
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="Axis",
RepresentationType=representation_type,
Items=(curve,),
)
alignment.ObjectPlacement = placement
ifcopenshell.api.geometry.assign_representation(file, alignment, representation)
@@ -0,0 +1,73 @@
# 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.api.alignment
from ifcopenshell import entity_instance
def _get_cant_segment(horizontal_segment: entity_instance) -> entity_instance:
"""
Returns the IfcAlignmentSegment from the cant layout that corresponds to horizontal_segment.
Returns None if the cant segment cannot be found
"""
expected_type = "IfcAlignmentSegment"
if not horizontal_segment.is_a(expected_type):
raise TypeError(f"Expected {expected_type} but got {horizontal_segment.is_a()}")
if not horizontal_segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
raise TypeError(
f"Expect DesignParameter to be IfcAlignmentHorizontal but got {horizontal_segment.DesignParameters.is_a()}"
)
# get the index of horizontal_segment in the horizontal_layout
horizontal_layout = horizontal_segment.Nests[0].RelatingObject
index = 0
for segment in horizontal_layout.IsNestedBy[0].RelatedObjects:
if segment == horizontal_segment:
break
else:
index += 1
cant_segment = None
# first check CT 4.1.4.4.1.1 Alignment Layout - Horizontal, Vertical and Cant
nests_layouts = horizontal_layout.Nests[0]
for layout in nests_layouts.RelatedObjects:
if layout.is_a("IfcAlignmentCant"):
cant_segment = layout.IsNestedBy[0].RelatedObjects[index]
break
# if a cant_segment wasn't found, check CT 4.1.4.4.1.2 Alignment Layout - Reusing Horizontal Layout
# Note that nothing forbids multiple child alignments to have cant layouts. However, this would not make
# sense for Viennese Bend because the Viennese Bend cant segment influences the geometry of the horizontal
# Viennese Bend transition curve segment. The horizontal geometry would not be unique if there are
# multiple child alignments with cant layouts.
# For this reason, use the first cant layout found
if cant_segment == None:
alignment = ifcopenshell.api.alignment.get_alignment(horizontal_layout)
for child_alignment in alignment.IsDecomposedBy[0].RelatedObjects:
for layout in child_alignment.Nests[0].RelatedObjects:
if layout.is_a("IfcAlignmentCant"):
cant_segment = layout.IsNestedBy[0].RelatedObjects[index]
break
if cant_segment:
break
return cant_segment
@@ -0,0 +1,312 @@
# 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/>.
from ifcopenshell import entity_instance
_horizontal_callback = None
_vertical_callback = None
_cant_callback = None
def register_referent_name_callback(horizontal=None, vertical=None, cant=None):
"""
Referents are automatically created at the start of each horizontal, vertical, and cant segment.
The referents represent key points in the alignment layout such as Point of Curvature, Point of Tangent, and others.
Different juristicions use different naming systems for these key points.
The referent name callback functions provide a customizable method for naming these referents. If a callback is registered,
it is called when creating the referent name, otherwise the default naming is used.
The callback function signature is
def mycallback(prev_segment : entity_instance, segment : entity_instance) -> str:
The callback function returns a string that is used in the referent name for the referent at the start of `segment`.
The callback must accomodate the following cases:
* prev_segment = None and segment != None - this indicates the last segment so the "End of Alignment" name is returned
* prev_segment != None and segment == None - this indicates the first segment so the "Beginning of Alignment" name is returned
* prev_segment != None and segment != None - this indicates an intermediate segment so a name representitive of the transition is returned
Setting any or all of the callbacks to None causes the default naming to be used.
"""
global _horizontal_callback
_horizontal_callback = horizontal
global _vertical_callback
_vertical_callback = vertical
global _cant_callback
_cant_callback = cant
def _horizontal_label(prev_segment: entity_instance, segment: entity_instance) -> str:
if prev_segment == None and segment != None:
label = "P.O.B."
elif prev_segment != None and segment == None:
label = "P.O.E."
else:
lookup_table = {
"BLOSSCURVE": {
"BLOSSCURVE": "xx",
"CIRCULARARC": "S.C.",
"CLOTHOID": "xx",
"COSINECURVE": "xx",
"CUBIC": "xx",
"HELMERTCURVE": "xx",
"LINE": "S.T.",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"CIRCULARARC": {
"BLOSSCURVE": "C.S.",
"CIRCULARARC": "P.C.C.",
"CLOTHOID": "C.S.",
"COSINECURVE": "C.S.",
"CUBIC": "C.S.",
"HELMERTCURVE": "C.S.",
"LINE": "P.T.",
"SINECURVE": "C.S.",
"VIENNESEBEND": "C.S.",
},
"CLOTHOID": {
"BLOSSCURVE": "xx",
"CIRCULARARC": "S.C.",
"CLOTHOID": "xx",
"COSINECURVE": "xx",
"CUBIC": "xx",
"HELMERTCURVE": "xx",
"LINE": "S.T.",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"COSINECURVE": {
"BLOSSCURVE": "xx",
"CIRCULARARC": "S.C.",
"CLOTHOID": "xx",
"COSINECURVE": "xx",
"CUBIC": "xx",
"HELMERTCURVE": "xx",
"LINE": "S.T.",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"CUBIC": {
"BLOSSCURVE": "xx",
"CIRCULARARC": "S.C.",
"CLOTHOID": "xx",
"COSINECURVE": "xx",
"CUBIC": "xx",
"HELMERTCURVE": "xx",
"LINE": "S.T.",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"HELMERTCURVE": {
"BLOSSCURVE": "xx",
"CIRCULARARC": "S.C.",
"CLOTHOID": "xx",
"COSINECURVE": "xx",
"CUBIC": "xx",
"HELMERTCURVE": "xx",
"LINE": "S.T.",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"LINE": {
"BLOSSCURVE": "T.S.",
"CIRCULARARC": "P.C.",
"CLOTHOID": "T.S.",
"COSINECURVE": "T.S.",
"CUBIC": "T.S.",
"HELMERTCURVE": "T.S.",
"LINE": "P.I.",
"SINECURVE": "T.S.",
"VIENNESEBEND": "T.S.",
},
"SINECURVE": {
"BLOSSCURVE": "xx",
"CIRCULARARC": "S.C.",
"CLOTHOID": "xx",
"COSINECURVE": "xx",
"CUBIC": "xx",
"HELMERTCURVE": "xx",
"LINE": "S.T.",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"VIENNESEBEND": {
"BLOSSCURVE": "xx",
"CIRCULARARC": "S.C.",
"CLOTHOID": "xx",
"COSINECURVE": "xx",
"CUBIC": "xx",
"HELMERTCURVE": "xx",
"LINE": "S.T.",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
}
label = lookup_table[prev_segment.DesignParameters.PredefinedType][segment.DesignParameters.PredefinedType]
return label
def _vertical_label(prev_segment: entity_instance, segment: entity_instance) -> str:
if prev_segment == None and segment != None:
label = "V.P.O.B."
elif prev_segment != None and segment == None:
label = "V.P.O.E."
else:
lookup_table = {
"CIRCULARARC": {"CIRCULARARC": "xx", "CLOTHOID": "xx", "CONSTANTGRADIENT": "xx", "PARABOLICARC": "xx"},
"CLOTHOID": {"CIRCULARARC": "xx", "CLOTHOID": "xx", "CONSTANTGRADIENT": "xx", "PARABOLICARC": "xx"},
"CONSTANTGRADIENT": {
"CIRCULARARC": "xx",
"CLOTHOID": "xx",
"CONSTANTGRADIENT": "P.V.I",
"PARABOLICARC": "P.V.C.",
},
"PARABOLICARC": {
"CIRCULARARC": "xx",
"CLOTHOID": "xx",
"CONSTANTGRADIENT": "P.V.T.",
"PARABOLICARC": "V.C.C.",
},
}
label = lookup_table[prev_segment.DesignParameters.PredefinedType][segment.DesignParameters.PredefinedType]
return label
def _cant_label(prev_segment: entity_instance, segment: entity_instance) -> str:
if prev_segment == None and segment != None:
label = "C.P.O.B."
elif prev_segment != None and segment == None:
label = "C.P.O.E."
else:
lookup_table = {
"BLOSSCURVE": {
"BLOSSCURVE": "xx",
"CONSTANTCANT": "xx",
"COSINECURVE": "xx",
"HELMERTCURVE": "xx",
"LINEARTRANSITION": "xx",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"CONSTANTCANT": {
"BLOSSCURVE": "xx",
"CONSTANTCANT": "xx",
"COSINECURVE": "xx",
"HELMERTCURVE": "xx",
"LINEARTRANSITION": "xx",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"COSINECURVE": {
"BLOSSCURVE": "xx",
"CONSTANTCANT": "xx",
"COSINECURVE": "xx",
"HELMERTCURVE": "xx",
"LINEARTRANSITION": "xx",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"HELMERTCURVE": {
"BLOSSCURVE": "xx",
"CONSTANTCANT": "xx",
"COSINECURVE": "xx",
"HELMERTCURVE": "xx",
"LINEARTRANSITION": "xx",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"LINEARTRANSITION": {
"BLOSSCURVE": "xx",
"CONSTANTCANT": "xx",
"COSINECURVE": "xx",
"HELMERTCURVE": "xx",
"LINEARTRANSITION": "xx",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"SINECURVE": {
"BLOSSCURVE": "xx",
"CONSTANTCANT": "xx",
"COSINECURVE": "xx",
"HELMERTCURVE": "xx",
"LINEARTRANSITION": "xx",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
"VIENNESEBEND": {
"BLOSSCURVE": "xx",
"CONSTANTCANT": "xx",
"COSINECURVE": "xx",
"HELMERTCURVE": "xx",
"LINEARTRANSITION": "xx",
"SINECURVE": "xx",
"VIENNESEBEND": "xx",
},
}
label = lookup_table[prev_segment.DesignParameters.PredefinedType][segment.DesignParameters.PredefinedType]
return label
def _get_segment_start_point_label(prev_segment: entity_instance, segment: entity_instance) -> str:
"""
Returns the label for the start point of a segment. Typically used in the name of an IfcReferent
"""
if prev_segment != None and segment != None and prev_segment.is_a() != segment.is_a():
raise TypeError(
f"Expected entity type to be the same type, instead received {prev_segment.is_a()} and {segment.is_a()}"
)
expected_types = ["IfcAlignmentHorizontalSegment", "IfcAlignmentVerticalSegment", "IfcAlignmentCantSegment"]
if prev_segment != None and not prev_segment.DesignParameters.is_a() in expected_types:
raise TypeError(
f"Expected prev_segment.DesignParameters type to be one of {[_ for _ in expected_types]}, instead received {prev_segment.DesignParameters.is_a()}"
)
if segment != None and not segment.DesignParameters.is_a() in expected_types:
raise TypeError(
f"Expected segment.DesignParameters type to be one of {[_ for _ in expected_types]}, instead received {segment.DesignParameters.is_a()}"
)
s = segment if segment != None else prev_segment
if s.DesignParameters.is_a("IfcAlignmentHorizontalSegment"):
global _horizontal_callback
if _horizontal_callback:
label = _horizontal_callback(prev_segment, segment)
else:
label = _horizontal_label(prev_segment, segment)
elif s.DesignParameters.is_a("IfcAlignmentVerticalSegment"):
global _vertical_callback
if _vertical_callback:
label = _vertical_callback(prev_segment, segment)
else:
label = _vertical_label(prev_segment, segment)
elif s.DesignParameters.is_a("IfcAlignmentCantSegment"):
global _cant_callback
if _cant_callback:
label = _cant_callback(prev_segment, segment)
else:
label = _cant_label(prev_segment, segment)
return label
@@ -0,0 +1,473 @@
# 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 math
from collections.abc import Sequence
import ifcopenshell
from ifcopenshell import entity_instance
def _get_axis(file: ifcopenshell.file, Ds: float, rail_head_distance: float) -> entity_instance:
Dy = rail_head_distance
Dz = 2 * Ds
D = math.sqrt(Dy * Dy + Dz * Dz)
return file.createIfcDirection((0.0, Dz / D, Dy / D))
def _map_constant_cant(
file: ifcopenshell.file, design_parameters: entity_instance, rail_head_distance: float
) -> Sequence[entity_instance]:
dist_along = design_parameters.StartDistAlong
length = design_parameters.HorizontalLength
Dsl = design_parameters.StartCantLeft
Dsr = design_parameters.StartCantRight
Ds = 0.5 * (Dsl + Dsr)
transition = "DISCONTINUOUS"
parent_curve = file.createIfcLine(
Pnt=file.createIfcCartesianPoint((0.0, 0.0)),
Dir=file.createIfcVector(Orientation=file.createIfcDirection((1.0, 0.0)), Magnitude=1.0),
)
start_point = file.createIfcCartesianPoint((dist_along, Ds, 0.0))
start_direction = 0.0
curve_segment = file.createIfcCurveSegment(
Transition=transition,
Placement=file.createIfcAxis2Placement3D(
Location=start_point,
Axis=_get_axis(file, Ds, rail_head_distance),
RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction), 0.0)),
),
SegmentStart=file.createIfcLengthMeasure(0.0),
SegmentLength=file.createIfcLengthMeasure(length),
ParentCurve=parent_curve,
)
return (curve_segment, None)
def _map_linear_transition(
file: ifcopenshell.file, design_parameters: entity_instance, rail_head_distance: float
) -> Sequence[entity_instance]:
dist_along = design_parameters.StartDistAlong
length = design_parameters.HorizontalLength
Dsl = design_parameters.StartCantLeft
Del = design_parameters.EndCantLeft
Dsr = design_parameters.StartCantRight
Der = design_parameters.EndCantRight
Ds = 0.5 * (Dsl + Dsr)
De = 0.5 * (Del + Der)
f = De - Ds
a0 = Ds # constant term
a1 = f # linear term
transition = "DISCONTINUOUS"
A0 = math.pow(length, 2.0 / 1.0) * math.pow(math.fabs(a0), -1.0 / 1.0) * (a0 / math.fabs(a0)) if a0 != 0.0 else 0.0
A1 = math.pow(length, 3.0 / 2.0) * math.pow(math.fabs(a1), -1.0 / 2.0) * (a1 / math.fabs(a1)) if a1 != 0.0 else 0.0
parent_curve_location = file.createIfcCartesianPoint((0.0, 0.0))
parent_curve = file.createIfcClothoid(
Position=file.createIfcAxis2Placement2D(
Location=parent_curve_location, RefDirection=file.createIfcDirection((1.0, 0.0))
),
ClothoidConstant=A1,
)
start_point = file.createIfcCartesianPoint(
(dist_along, math.pow(length, 2.0 / 1.0) / A0 if A0 != 0.0 else 0.0, 0.0)
)
start_direction = math.atan(A1 * math.pow(length, 2.0 / 1.0) / math.fabs(math.pow(A1, 3.0 / 1.0)))
curve_segment = file.createIfcCurveSegment(
Transition=transition,
Placement=file.createIfcAxis2Placement3D(
Location=start_point,
Axis=_get_axis(file, Ds, rail_head_distance),
RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction), 0.0)),
),
SegmentStart=file.createIfcLengthMeasure(0.0),
SegmentLength=file.createIfcLengthMeasure(length),
ParentCurve=parent_curve,
)
return (curve_segment, None)
def _map_helmert_curve(
file: ifcopenshell.file, design_parameters: entity_instance, rail_head_distance: float
) -> Sequence[entity_instance]:
dist_along = design_parameters.StartDistAlong
length = design_parameters.HorizontalLength
Dsl = design_parameters.StartCantLeft
Del = design_parameters.EndCantLeft
Dsr = design_parameters.StartCantRight
Der = design_parameters.EndCantRight
Ds = Dsl + Dsr
De = Del + Der
f = De - Ds
transition = "DISCONTINUOUS"
# First half
a0_1 = 2.0 * Ds # constant term
a1_1 = 0.0 # linear term
a2_1 = 4.0 * f # quadratic term
A0_1 = (
math.pow(length, 2.0 / 1.0) * 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 = (
math.pow(length, 3.0 / 2.0) * 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 = (
math.pow(length, 4.0 / 3.0) * math.pow(math.fabs(a2_1), -1.0 / 3.0) * (a2_1 / math.fabs(a2_1))
if a2_1 != 0.0
else 0.0
)
parent_curve_1 = 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,
)
start_point_1 = file.createIfcCartesianPoint((dist_along, Ds / 2.0, 0.0))
start_direction_1 = 0.0
curve_segment_1 = file.createIfcCurveSegment(
Transition=transition,
Placement=file.createIfcAxis2Placement3D(
Location=start_point_1,
Axis=_get_axis(file, Ds / 2.0, rail_head_distance),
RefDirection=file.createIfcDirection((math.cos(start_direction_1), math.sin(start_direction_1), 0.0)),
),
SegmentStart=file.createIfcLengthMeasure(0.0),
SegmentLength=file.createIfcLengthMeasure(length / 2.0),
ParentCurve=parent_curve_1,
)
# Second half
a0_2 = -2.0 * f + 2.0 * Ds # constant term
a1_2 = 8.0 * f # linear term
a2_2 = -4.0 * f # quadratic term
A0_2 = (
math.pow(length, 2.0 / 1.0) * 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 = (
math.pow(length, 3.0 / 2.0) * 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 = (
math.pow(length, 4.0 / 3.0) * math.pow(math.fabs(a2_2), -1.0 / 3.0) * (a2_2 / math.fabs(a2_2))
if a2_2 != 0.0
else 0.0
)
parent_curve_2 = file.createIfcSecondOrderPolynomialSpiral(
Position=file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint((0.0, 0.0)), RefDirection=file.createIfcDirection((1.0, 0.0))
),
QuadraticTerm=A2_2,
LinearTerm=A1_2 if A1_2 != 0.0 else None,
ConstantTerm=A0_2 if A0_2 != 0.0 else None,
)
start_point_2 = file.createIfcCartesianPoint((dist_along + length / 2.0, Ds / 2.0 + f / 4.0, 0.0))
slope = math.pow(length / 2.0, 2.0) * (2.0 * (length / 2.0) / pow(A2_1, 3.0))
start_direction_2 = math.atan(slope)
curve_segment_2 = file.createIfcCurveSegment(
Transition=transition,
Placement=file.createIfcAxis2Placement3D(
Location=start_point_2,
Axis=_get_axis(file, (Ds + De) / 4.0, rail_head_distance),
RefDirection=file.createIfcDirection((math.cos(start_direction_2), math.sin(start_direction_2), 0.0)),
),
SegmentStart=file.createIfcLengthMeasure(length / 2.0),
SegmentLength=file.createIfcLengthMeasure(length / 2.0),
ParentCurve=parent_curve_2,
)
return (curve_segment_1, curve_segment_2)
def _map_bloss_curve(
file: ifcopenshell.file, design_parameters: entity_instance, rail_head_distance: float
) -> Sequence[entity_instance]:
dist_along = design_parameters.StartDistAlong
length = design_parameters.HorizontalLength
Dsl = design_parameters.StartCantLeft
Del = design_parameters.EndCantLeft
Dsr = design_parameters.StartCantRight
Der = design_parameters.EndCantRight
Ds = 0.5 * (Dsl + Dsr)
De = 0.5 * (Del + Der)
f = De - Ds
a0 = Ds # constant term
a1 = 0.0 # linear term
a2 = 3.0 * f # quadratic term
a3 = -2.0 * f # cubic term
transition = "DISCONTINUOUS"
A0 = math.pow(length, 2.0 / 1.0) * math.pow(math.fabs(a0), -1.0 / 1.0) * (a0 / math.fabs(a0)) if a0 != 0.0 else 0.0
A1 = math.pow(length, 3.0 / 2.0) * math.pow(math.fabs(a1), -1.0 / 2.0) * (a1 / math.fabs(a1)) if a1 != 0.0 else 0.0
A2 = math.pow(length, 4.0 / 3.0) * math.pow(math.fabs(a2), -1.0 / 3.0) * (a2 / math.fabs(a2)) if a2 != 0.0 else 0.0
A3 = math.pow(length, 5.0 / 4.0) * 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,
)
start_point = file.createIfcCartesianPoint((dist_along, Ds, 0.0))
start_direction = 0.0
curve_segment = file.createIfcCurveSegment(
Transition=transition,
Placement=file.createIfcAxis2Placement3D(
Location=start_point,
Axis=_get_axis(file, Ds, rail_head_distance),
RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction), 0.0)),
),
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, rail_head_distance: float
) -> Sequence[entity_instance]:
dist_along = design_parameters.StartDistAlong
length = design_parameters.HorizontalLength
Dsl = design_parameters.StartCantLeft
Del = design_parameters.EndCantLeft
Dsr = design_parameters.StartCantRight
Der = design_parameters.EndCantRight
Ds = 0.5 * (Dsl + Dsr)
De = 0.5 * (Del + Der)
f = De - Ds
a0 = Ds + 0.5 * f # constant term
a1 = -0.5 * f # cosine term
A0 = math.pow(length, 2.0) * math.pow(math.fabs(a0), -1.0 / 1.0) * (a0 / math.fabs(a0)) if a0 != 0.0 else 0.0
A1 = math.pow(length, 2.0) * math.pow(math.fabs(a1), -1.0 / 1.0) * (a1 / math.fabs(a1)) if a1 != 0.0 else 0.0
transition = "DISCONTINUOUS"
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,
)
start_point = file.createIfcCartesianPoint((dist_along, Ds, 0.0))
start_direction = 0.0
curve_segment = file.createIfcCurveSegment(
Transition=transition,
Placement=file.createIfcAxis2Placement3D(
Location=start_point,
Axis=_get_axis(file, Ds, rail_head_distance),
RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction), 0.0)),
),
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, rail_head_distance: float
) -> Sequence[entity_instance]:
dist_along = design_parameters.StartDistAlong
length = design_parameters.HorizontalLength
Dsl = design_parameters.StartCantLeft
Del = design_parameters.EndCantLeft
Dsr = design_parameters.StartCantRight
Der = design_parameters.EndCantRight
Ds = 0.5 * (Dsl + Dsr)
De = 0.5 * (Del + Der)
f = De - Ds
a0 = Ds # constant term
a1 = f # linear term
a2 = -(1.0 / (2.0 * math.pi)) * f # sine term
A0 = math.pow(length, 2.0) * math.pow(math.fabs(a0), -1.0 / 1.0) * (a0 / math.fabs(a0)) if a0 != 0.0 else 0.0
A1 = math.pow(length, 1.5) * math.pow(math.fabs(a1), -1.0 / 2.0) * (a1 / math.fabs(a1)) if a1 != 0.0 else 0.0
A2 = math.pow(length, 2.0) * math.pow(math.fabs(a2), -1.0 / 1.0) * (a2 / math.fabs(a2)) if a2 != 0.0 else 0.0
transition = "DISCONTINUOUS"
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 else None,
ConstantTerm=A0 if A0 != 0.0 else None,
)
start_point = file.createIfcCartesianPoint((dist_along, Ds, 0.0))
start_direction = 0.0
curve_segment = file.createIfcCurveSegment(
Transition=transition,
Placement=file.createIfcAxis2Placement3D(
Location=start_point,
Axis=_get_axis(file, Ds, rail_head_distance),
RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction), 0.0)),
),
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, rail_head_distance: float
) -> Sequence[entity_instance]:
dist_along = design_parameters.StartDistAlong
length = design_parameters.HorizontalLength
Dsl = design_parameters.StartCantLeft
Del = design_parameters.EndCantLeft
Dsr = design_parameters.StartCantRight
Der = design_parameters.EndCantRight
Ds = 0.5 * (Dsl + Dsr)
De = 0.5 * (Del + Der)
f = De - Ds
a0 = Ds # constant term
a1 = 0.0 # linear term
a2 = 0.0 * f # quadratic term
a3 = 0.0 * f # cubic term
a4 = 35.0 * f # quartic term
a5 = -84.0 * f # quintic term
a6 = 70.0 * f # sextic term
a7 = -20.0 * f # septic term
transition = "DISCONTINUOUS"
A0 = math.pow(length, 2.0 / 1.0) * math.pow(math.fabs(a0), -1.0 / 1.0) * (a0 / math.fabs(a0)) if a0 != 0.0 else 0.0
A1 = math.pow(length, 3.0 / 2.0) * math.pow(math.fabs(a1), -1.0 / 2.0) * (a1 / math.fabs(a1)) if a1 != 0.0 else 0.0
A2 = math.pow(length, 4.0 / 3.0) * math.pow(math.fabs(a2), -1.0 / 3.0) * (a2 / math.fabs(a2)) if a2 != 0.0 else 0.0
A3 = math.pow(length, 5.0 / 4.0) * math.pow(math.fabs(a3), -1.0 / 4.0) * (a3 / math.fabs(a3)) if a3 != 0.0 else 0.0
A4 = math.pow(length, 6.0 / 5.0) * math.pow(math.fabs(a4), -1.0 / 5.0) * (a4 / math.fabs(a4)) if a4 != 0.0 else 0.0
A5 = math.pow(length, 7.0 / 6.0) * math.pow(math.fabs(a5), -1.0 / 6.0) * (a5 / math.fabs(a5)) if a5 != 0.0 else 0.0
A6 = math.pow(length, 8.0 / 7.0) * math.pow(math.fabs(a6), -1.0 / 7.0) * (a6 / math.fabs(a6)) if a6 != 0.0 else 0.0
A7 = math.pow(length, 9.0 / 8.0) * math.pow(math.fabs(a7), -1.0 / 8.0) * (a7 / math.fabs(a7)) if a7 != 0.0 else 0.0
parent_curve = file.createIfcSeventhOrderPolynomialSpiral(
Position=file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint((0.0, 0.0)), RefDirection=file.createIfcDirection((1.0, 0.0))
),
SepticTerm=A7,
SexticTerm=A6 if A6 != 0.0 else None,
QuinticTerm=A5 if A5 != 0.0 else None,
QuarticTerm=A4 if A4 != 0.0 else None,
CubicTerm=A3 if A3 != 0.0 else None,
QuadraticTerm=A2 if A2 != 0.0 else None,
LinearTerm=A1 if A1 != 0.0 else None,
ConstantTerm=A0 if A0 != 0.0 else None,
)
start_point = file.createIfcCartesianPoint((dist_along, Ds, 0.0))
start_direction = 0.0
curve_segment = file.createIfcCurveSegment(
Transition=transition,
Placement=file.createIfcAxis2Placement3D(
Location=start_point,
Axis=_get_axis(file, Ds, rail_head_distance),
RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction), 0.0)),
),
SegmentStart=file.createIfcLengthMeasure(0.0),
SegmentLength=file.createIfcLengthMeasure(length),
ParentCurve=parent_curve,
)
return (curve_segment, None)
def _map_alignment_cant_segment(
file: ifcopenshell.file, segment: entity_instance, rail_head_distance: float
) -> 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 = "IfcAlignmentSegment"
if not segment.is_a(expected_type):
raise TypeError(f"Expected to see type '{expected_type}', instead received '{segment.is_a()}'.")
predefined_type = segment.DesignParameters.PredefinedType
if predefined_type == "CONSTANTCANT":
result = _map_constant_cant(file, segment.DesignParameters, rail_head_distance)
elif predefined_type == "LINEARTRANSITION":
result = _map_linear_transition(file, segment.DesignParameters, rail_head_distance)
elif predefined_type == "HELMERTCURVE":
result = _map_helmert_curve(file, segment.DesignParameters, rail_head_distance)
elif predefined_type == "BLOSSCURVE":
result = _map_bloss_curve(file, segment.DesignParameters, rail_head_distance)
elif predefined_type == "COSINECURVE":
result = _map_cosine_curve(file, segment.DesignParameters, rail_head_distance)
elif predefined_type == "SINECURVE":
result = _map_sine_curve(file, segment.DesignParameters, rail_head_distance)
elif predefined_type == "VIENNESEBEND":
result = _map_viennese_bend(file, segment.DesignParameters, rail_head_distance)
else:
raise TypeError(f"Unexpected predefined type: '{predefined_type}'.")
return result
@@ -0,0 +1,554 @@
# 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 math
from collections.abc import Sequence
import ifcopenshell
import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper
import ifcopenshell.util.unit
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._get_cant_segment import _get_cant_segment
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
angle_unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file, "PLANEANGLEUNIT")
start_direction *= angle_unit_scale
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
angle_unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file, "PLANEANGLEUNIT")
start_direction *= angle_unit_scale
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
angle_unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file, "PLANEANGLEUNIT")
start_direction *= angle_unit_scale
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
angle_unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file, "PLANEANGLEUNIT")
start_direction *= angle_unit_scale
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
angle_unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file, "PLANEANGLEUNIT")
start_direction *= angle_unit_scale
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(
(
start_point.Coordinates[0] + x1 * math.cos(start_direction) - y1 * math.sin(start_direction),
start_point.Coordinates[1] + x1 * math.sin(start_direction) + y1 * math.cos(start_direction),
)
),
RefDirection=file.createIfcDirection(
(math.cos(start_direction + angle1), math.sin(start_direction + 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
angle_unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file, "PLANEANGLEUNIT")
start_direction *= angle_unit_scale
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
angle_unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file, "PLANEANGLEUNIT")
start_direction *= angle_unit_scale
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
angle_unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file, "PLANEANGLEUNIT")
start_direction *= angle_unit_scale
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, segment: entity_instance) -> Sequence[entity_instance]:
design_parameters = segment.DesignParameters
start_point = design_parameters.StartPoint
start_direction = design_parameters.StartDirection
start_radius = design_parameters.StartRadiusOfCurvature
length = design_parameters.SegmentLength
gravity_centerline_height = (
design_parameters.GravityCenterLineHeight if design_parameters.GravityCenterLineHeight != None else 0.0
)
angle_unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file, "PLANEANGLEUNIT")
start_direction *= angle_unit_scale
transition = "DISCONTINUOUS"
cant_segment = _get_cant_segment(segment)
if cant_segment:
start_cant_left = cant_segment.DesignParameters.StartCantLeft
end_cant_left = cant_segment.DesignParameters.EndCantLeft if cant_segment.DesignParameters.EndCantLeft else 0.0
start_cant_right = cant_segment.DesignParameters.StartCantRight
end_cant_right = (
cant_segment.DesignParameters.EndCantRight if cant_segment.DesignParameters.EndCantRight else 0.0
)
cant_layout = cant_segment.Nests[0].RelatingObject
rail_head_distance = cant_layout.RailHeadDistance
else:
start_cant_left = 0.0
end_cant_left = 0.0
start_cant_right = 0.0
end_cant_right = 0.0
rail_head_distance = 1.0
cant_angle_start = (start_cant_right - start_cant_left) / rail_head_distance if rail_head_distance else 0.0
cant_angle_end = (end_cant_right - end_cant_left) / rail_head_distance if rail_head_distance else 0.0
cant_factor = -420.0 * (gravity_centerline_height / length) * (cant_angle_end - cant_angle_start)
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 = 1.0 * cant_factor # quadratic term
a3 = -4.0 * cant_factor # cubic term
a4 = 5.0 * cant_factor + 35.0 * f # quartic term
a5 = -2.0 * cant_factor - 84.0 * f # quintic term
a6 = 70.0 * f # sextic term
a7 = -20.0 * f # septic 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
A4 = length * math.pow(math.fabs(a4), -1.0 / 5.0) * (a4 / math.fabs(a4)) if a4 != 0.0 else 0.0
A5 = length * math.pow(math.fabs(a5), -1.0 / 6.0) * (a5 / math.fabs(a5)) if a5 != 0.0 else 0.0
A6 = length * math.pow(math.fabs(a6), -1.0 / 7.0) * (a6 / math.fabs(a6)) if a6 != 0.0 else 0.0
A7 = length * math.pow(math.fabs(a7), -1.0 / 8.0) * (a7 / math.fabs(a7)) if a7 != 0.0 else 0.0
parent_curve = file.createIfcSeventhOrderPolynomialSpiral(
Position=file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint((0.0, 0.0)), RefDirection=file.createIfcDirection((1.0, 0.0))
),
SepticTerm=A7,
SexticTerm=A6 if A6 != 0.0 else None,
QuinticTerm=A5 if A5 != 0.0 else None,
QuarticTerm=A4 if A4 != 0.0 else None,
CubicTerm=A3 if A3 != 0.0 else None,
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_alignment_horizontal_segment(file: ifcopenshell.file, segment: 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 = "IfcAlignmentSegment"
if not segment.is_a(expected_type):
raise TypeError(f"Expected to see type '{expected_type}', instead received '{segment.is_a()}'.")
predefined_type = segment.DesignParameters.PredefinedType
if predefined_type == "LINE":
result = _map_line(file, segment.DesignParameters)
elif predefined_type == "CIRCULARARC":
result = _map_circular_arc(file, segment.DesignParameters)
elif predefined_type == "CLOTHOID":
result = _map_clothoid(file, segment.DesignParameters)
elif predefined_type == "CUBIC":
result = _map_cubic(file, segment.DesignParameters)
elif predefined_type == "HELMERTCURVE":
result = _map_helmert_curve(file, segment.DesignParameters)
elif predefined_type == "BLOSSCURVE":
result = _map_bloss_curve(file, segment.DesignParameters)
elif predefined_type == "COSINECURVE":
result = _map_cosine_curve(file, segment.DesignParameters)
elif predefined_type == "SINECURVE":
result = _map_sine_curve(file, segment.DesignParameters)
elif predefined_type == "VIENNESEBEND":
result = _map_viennese_bend(file, segment)
else:
raise TypeError(f"Unexpected predefined type: '{predefined_type}'.")
return result
@@ -0,0 +1,217 @@
# 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 math
from collections.abc import Sequence
import ifcopenshell
from ifcopenshell import entity_instance
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
transition = "DISCONTINUOUS"
parent_curve = file.create_entity(
type="IfcLine",
Pnt=file.createIfcCartesianPoint((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.createIfcCurveSegment(
Transition=transition,
Placement=file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint((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
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.createIfcCartesianPoint((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.createIfcCartesianPoint((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 = 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)
# start and end angles are for the curve tangents
# convert them to be angles of the radii lines
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,
)
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, segment: 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 = "IfcAlignmentSegment"
if not segment.is_a(expected_type):
raise TypeError(f"Expected to see type '{expected_type}', instead received '{segment.is_a()}'.")
predefined_type = segment.DesignParameters.PredefinedType
if predefined_type == "CONSTANTGRADIENT":
result = _map_constant_gradient(file, segment.DesignParameters)
elif predefined_type == "PARABOLICARC":
result = _map_parabolic_arc(file, segment.DesignParameters)
elif predefined_type == "CIRCULARARC":
result = _map_circular_arc(file, segment.DesignParameters)
elif predefined_type == "CLOTHOID":
result = _map_clothoid(file, segment.DesignParameters)
else:
raise TypeError(f"Unexpected predefined type - got {segment.DesignParameters.PredefinedType}")
return result
@@ -0,0 +1,29 @@
# 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.api.alignment
from ifcopenshell import entity_instance
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.
"""
prev_segment.Transition = ifcopenshell.api.alignment.get_curve_segment_transition_code(prev_segment, segment)
@@ -0,0 +1,164 @@
# 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 numpy as np
import ifcopenshell
import ifcopenshell.api.alignment
import ifcopenshell.api.pset
import ifcopenshell.geom
import ifcopenshell.guid
import ifcopenshell.util.element
import ifcopenshell.util.unit
from ifcopenshell import entity_instance, ifcopenshell_wrapper
def add_stationing_referent(
file: ifcopenshell.file,
alignment: entity_instance,
distance_along: float,
station: float,
name: str,
positioned_product: entity_instance,
) -> entity_instance:
"""
Adds an IfcReferent to the alignment with the Pset_Stationing property set.
:param alignment: the alignment to receive the referent
:param distance_along: distance along the alignment basis curve
:param station: station value
:param name: name to assign to IfcReferent.Name, typically a stringized version of the station value
:param positioned_product: the product whose position is informed by the referent
:return: referent
Example:
.. code:: python
alignment = model.by_type("IfcAlignment")[0]
ifcopenshell.api.alignment.add_stationing_referent(model,alignment=alignment,distance_along=0.0,station=100.0)
"""
basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment)
object_placement = None
representation = None
if basis_curve:
object_placement = file.createIfcLinearPlacement(
RelativePlacement=file.createIfcAxis2PlacementLinear(
Location=file.createIfcPointByDistanceExpression(
DistanceAlong=file.createIfcLengthMeasure(distance_along),
OffsetLateral=None,
OffsetVertical=None,
OffsetLongitudinal=None,
BasisCurve=basis_curve,
)
),
)
is_valid_curve = True
if basis_curve.is_a("IfcCompositeCurve") and len(basis_curve.Segments) == 0:
is_valid_curve = False
if basis_curve.is_a("IfcPolyline") and len(basis_curve.Points) < 2:
is_valid_curve = False
elif basis_curve.is_a("IfcIndexedPolyCurve") and len(basis_curve.Points.CoordList) < 2:
is_valid_curve = False
if is_valid_curve:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
settings = ifcopenshell.geom.settings()
fn = ifcopenshell_wrapper.map_shape(settings, basis_curve.wrapped_data)
if basis_curve.is_a("IfcPolyline") or basis_curve.is_a("IfcIndexedPolyCurve"):
fn = ifcopenshell_wrapper.convert_loop_to_function_item(fn)
evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, fn)
p = evaluator.evaluate(distance_along * unit_scale)
p = np.array(p)
x = float(p[0, 3]) / unit_scale
y = float(p[1, 3]) / unit_scale
z = float(p[2, 3]) / unit_scale
rx = float(p[0, 0])
ry = float(p[1, 0])
rz = float(p[2, 0])
ax = float(p[0, 2])
ay = float(p[1, 2])
az = float(p[2, 2])
else:
x = 0.0
y = 0.0
z = 0.0
rx = 1.0
ry = 0.0
rz = 0.0
ax = 0.0
ay = 0.0
az = 1.0
object_placement.CartesianPosition = file.createIfcAxis2Placement3D(
Location=file.createIfcCartesianPoint((x, y, z)),
Axis=file.createIfcDirection((ax, ay, az)),
RefDirection=file.createIfcDirection((rx, ry, rz)),
)
# 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
# representation = file.create_entity(
# name="IfcCircle",
# position=file.createIfcAxis2Placement2D(Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)),
# radius=1.0)
# )
# create referent for the station
referent = file.createIfcReferent(
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=name,
Description=None,
ObjectType=None,
ObjectPlacement=object_placement,
Representation=representation,
PredefinedType="STATION",
)
pset_stationing = ifcopenshell.api.pset.add_pset(file, product=referent, name="Pset_Stationing")
ifcopenshell.api.pset.edit_pset(file, pset=pset_stationing, properties={"Station": station})
nest = ifcopenshell.api.alignment.get_referent_nest(file, alignment)
nest.RelatedObjects += (referent,)
nest.RelatedObjects = sorted(
nest.RelatedObjects, key=lambda x: ifcopenshell.util.element.get_pset(x, name="Pset_Stationing", prop="Station")
)
if len(referent.Positions) == 0:
rel_positions = file.createIfcRelPositions(
GlobalId=ifcopenshell.guid.new(),
RelatingPositioningElement=referent,
RelatedProducts=[
positioned_product,
],
)
else:
referent.Positions[0].RelatedProducts += (positioned_product,)
return referent
@@ -0,0 +1,203 @@
# 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
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._add_zero_length_segment import _add_zero_length_segment
def _move_vertical_layout_to_child_alignment(
file: ifcopenshell.file, parent_alignment: entity_instance, vertical_layout: entity_instance
):
"""
Creates a new child alignment and aggregates it to the parent alignment. Moves the vertical alignment from the parent
alignment to the child alignment. Also moves the "Axis/Curve3D" representation to the child alignment, if present.
This function supports the transition of vertical alignment between CT 4.1.4.4.1.1 and 4.1.4.4.1.2 because a subsequent
vertical alignment is being added and the Alignment Layout - Reusing Horizontal Layout concept applies.
"""
# unhook the vertical layout from the parent alignment
ifcopenshell.api.nest.unassign_object(file, related_objects=[vertical_layout])
# create the child alignment
child_alignment = file.createIfcAlignment(
GlobalId=ifcopenshell.guid.new(), Name=f"Child of {parent_alignment.Name}"
)
# nest the vertical layout onto the child alignment
ifcopenshell.api.nest.assign_object(file, related_objects=[vertical_layout], relating_object=child_alignment)
# aggregate the child alignment to the parent alignment
ifcopenshell.api.aggregate.assign_object(file, products=[child_alignment], relating_object=parent_alignment)
# move all referents positioning segments of the vertical layout to the referent nest of the child alignment
child_referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, child_alignment)
parent_referent_nest = ifcopenshell.api.alignment.get_referent_nest(file, parent_alignment)
for referent in parent_referent_nest.RelatedObjects:
for product in referent.Positions[0].RelatedProducts:
if product.is_a("IfcAlignmentSegment") and product.Nests[0].RelatingObject == vertical_layout:
# ifcopenshell.api.nest.change_nest(file,referent,child_alignment) - this doesn't work because referent is assigned to child_alignment.IsNestedBy[0].RelatedObjects
# and it needs to be assigned to child_alignment.IsNestedBy[1].RelatedObjects
# move the referent manually - unassign it and add it to the child alignment's referent nest
ifcopenshell.api.nest.unassign_object(file, [referent])
child_referent_nest.RelatedObjects += (referent,)
# if the parent alignment has a representation, move the Axis/Curve3D represention to the child alignment
base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment)
if base_curve:
representations = ifcopenshell.util.representation.get_representations_iter(parent_alignment)
for representation in representations:
if representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve3D":
ifcopenshell.api.geometry.unassign_representation(file, parent_alignment, representation)
ifcopenshell.api.geometry.assign_representation(file, child_alignment, representation)
child_alignment.ObjectPlacement = parent_alignment.ObjectPlacement
break
def add_vertical_layout(file: ifcopenshell.file, parent_alignment: entity_instance) -> entity_instance:
"""
Adds a vertical layout to a previously created alignment.
If this is the first vertical layout assigned to the parent_alignment the IFC CT 4.1.4.4.1.1 Alignment Layout - Horizontal, Vertical and Cant
is followed. If this is the second or subsequent vertical layout assigned to the parent_alignment the
IFC CT 4.1.4.4.1.2 Alignment Layout - Reusing Horizontal Layout is followed.
When the second vertical layout is added, the structure of the IFC model must transition from one concept template to the other.
Specifically, the following occurs:
1) The first child IfcAlignment is created and is IfcRelAggregates with the parent alignment.
2) The first vertical layout is unassigned from the IfcRelNests of the parent alignment and is IfcRelNests to the new child alignment.
3) A second child IfcAlignment is created and it is IfcRelAggregates with the parent alignment.
4) The vertical layout is IfcRelNests to the second child alignment
For the third and subsequent vertical layouts, a new child alignment is created and aggregated to the parent alignment.
A zero segment length terminated IfcGradientCurve is created for the new vertical layout
:param parent_alignment: The parent alignment
:return: The new vertical layout, including the manditory zero length segment
"""
vertical_layout = file.createIfcAlignmentVertical(GlobalId=ifcopenshell.guid.new())
# get all the child alignments under alignment
child_alignments = [
c for c in ifcopenshell.util.element.get_decomposition(parent_alignment) if c.is_a("IfcAlignment")
]
# Get all the IfcAlignmentVertical that are nesting alignment (there should be 0 or 1)
# if 0, alignment is just horizontal and we are adding the first vertical so it will nest to the alignment,
# or there are multiple vertical and they nest to the aggregated child alignments
# if 1, there is one vertical alignments. Move it to a child alignment
vertical_layouts_nesting_alignment = [
c for c in ifcopenshell.util.element.get_components(parent_alignment) if c.is_a("IfcAlignmentVertical")
]
# move the vertical layout to a child alignment because there is going to be more than one vertical
assert len(vertical_layouts_nesting_alignment) == 0 or len(vertical_layouts_nesting_alignment) == 1
for vertical_layout_nesting_alignment in vertical_layouts_nesting_alignment:
_move_vertical_layout_to_child_alignment(file, parent_alignment, vertical_layout_nesting_alignment)
if len(child_alignments) == 0 and len(vertical_layouts_nesting_alignment) == 0:
# this is the first vertical layout so nest it into the parent alignment (IFC CT 4.1.4.4.1.1)
ifcopenshell.api.nest.assign_object(file, related_objects=[vertical_layout], relating_object=parent_alignment)
base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment)
# the parent alignment has a Representation so create a representation for the vertical
gradient_curve = file.createIfcGradientCurve(
Segments=[], SelfIntersect=False, BaseCurve=base_curve, EndPoint=None
)
# Per IFC CT 4.1.7.1.1.1, the shape representation for Horizontal geometry only is
# RepresentationIdentifier="Axis" and RepresentationType="Curve2D".
# However, per IFC CT 4.1.7.1.1.2 and 3 the shape represenation with Horizontal, Vertical and Cant
# is RepresentationIdentifier="FootPrint" and RepresentationType="Curve2D" for the horizontal and
# RepresentationIdentifier="Axis" and RepresentationType="Curve3D" for the 2.5D curve.
# Since the alignment is transitioning from horizontal only to horizontal+vertical, the
# RepresentationIdentifier must change from "Axis" to "FootPrint"
representations = ifcopenshell.util.representation.get_representations_iter(parent_alignment)
for representation in representations:
if representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve2D":
representation.RepresentationIdentifier = "FootPrint"
break
# create the Axis,Curve3D representation
axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file)
axis3d_shape_representation = file.createIfcShapeRepresentation(
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="Axis",
RepresentationType="Curve3D",
Items=(gradient_curve,),
)
ifcopenshell.api.geometry.assign_representation(file, parent_alignment, axis3d_shape_representation)
else:
# there are multiple vertical reusing the horizontal (IFC CT 4.1.4.4.1.2)
# this is the second or subsequent vertical reusing the horizontal
# create a new child alignment for the new vertical
child_alignment = file.createIfcAlignment(
GlobalId=ifcopenshell.guid.new(),
OwnerHistory=None,
Name=f"Child of {parent_alignment.Name}",
Description=None,
ObjectType=None,
ObjectPlacement=None,
Representation=None,
PredefinedType=None,
)
# Aggregate the child alignment to the parent alignment
ifcopenshell.api.aggregate.assign_object(file, (child_alignment,), parent_alignment)
# nest the vertical under the child alignment
ifcopenshell.api.nest.assign_object(file, related_objects=[vertical_layout], relating_object=child_alignment)
child_alignment.ObjectPlacement = parent_alignment.ObjectPlacement
# the parent alignment has a Representation so create a representation for the vertical
base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment)
gradient_curve = file.createIfcGradientCurve(
Segments=[], SelfIntersect=False, BaseCurve=base_curve, EndPoint=None
)
axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file)
# create the Curve3D representation
axis3d_shape_representation = file.createIfcShapeRepresentation(
ContextOfItems=axis_geom_subcontext,
RepresentationIdentifier="Axis",
RepresentationType="Curve3D",
Items=(gradient_curve,),
)
# add the representation to the child alignment
ifcopenshell.api.geometry.assign_representation(file, child_alignment, axis3d_shape_representation)
# All alignment layouts must end with a zero length segment. Their geometric representations must also end with a zero length segment.
# Now that all the geometry is setup, add the zero length segment to the layout, which also adds a zero length segment to the representation
_add_zero_length_segment(file, vertical_layout)
return vertical_layout
@@ -0,0 +1,252 @@
# 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 math
import numpy as np
import ifcopenshell
import ifcopenshell.api.alignment
import ifcopenshell.api.nest
import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper as wrapper
import ifcopenshell.util.alignment
import ifcopenshell.util.unit
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._get_segment_start_point_label import (
_get_segment_start_point_label,
)
from ifcopenshell.api.alignment._map_alignment_horizontal_segment import (
_map_alignment_horizontal_segment,
)
from ifcopenshell.api.alignment._map_alignment_vertical_segment import (
_map_alignment_vertical_segment,
)
from ifcopenshell.api.alignment._update_curve_segment_transition_code import (
_update_curve_segment_transition_code,
)
def add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance, include_referent: bool = True) -> bool:
"""
Adds a zero length segment to the end of a layout.
If the layout already has a zero length segment, nothing is changed.
:param layout: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant, IfcCompositeCurve, IfcGradientCurve, IfcSegmentedReferenceCurve
:param include_referent: If True, an IfcReferent representing the ending point of the layout is included for IfcLinearElement layouts (i.e. business logic)
:return: True if segment is added
"""
# These are valid curve types for alignment, but don't have the zero-length segment
if layout.is_a("IfcOffsetCurveByDistances") or layout.is_a("IfcPolyline") or layout.is_a("IfcIndexedPolyCurve"):
return
expected_types = [
"IfcAlignmentHorizontal",
"IfcAlignmentVertical",
"IfcAlignmentCant",
"IfcCompositeCurve",
"IfcGradientCurve",
"IfcSegmentedReferenceCurve",
]
if not layout.is_a() in expected_types:
raise TypeError(
f"Expected layout type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}"
)
if ifcopenshell.api.alignment.has_zero_length_segment(layout):
return False
if layout.is_a("IfcCompositeCurve") or layout.is_a("IfcGradientCurve") or layout.is_a("IfcSegmentedReferenceCurve"):
x = 0.0
y = 0.0
dx = 1.0
dy = 0.0
segment_start = 0.0
last_segment = None
if layout.Segments and 0 < len(layout.Segments):
# If there are segments, get the last segment and compute the end point and tangent direction
# because this becomes of placement of the zero length segment
last_segment = layout.Segments[-1]
settings = ifcopenshell.geom.settings()
fn = wrapper.map_shape(settings, last_segment.wrapped_data)
eval = wrapper.function_item_evaluator(settings, fn)
e = np.array(eval.evaluate(fn.end()))
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
e[:3, 3] /= unit_scale
x = float(e[0, 3])
y = float(e[1, 3])
dx = float(e[0, 0])
dy = float(e[1, 0])
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,
),
)
zero_length_curve_segment = file.createIfcCurveSegment(
Transition="DISCONTINUOUS",
Placement=file.createIfcAxis2Placement2D(
Location=file.createIfcCartesianPoint((x, y)),
RefDirection=file.createIfcDirection((dx, dy)),
),
SegmentStart=file.createIfcLengthMeasure(0.0),
SegmentLength=file.createIfcLengthMeasure(0.0),
ParentCurve=parent_curve,
)
layout.Segments += (zero_length_curve_segment,)
if last_segment:
_update_curve_segment_transition_code(last_segment, zero_length_curve_segment)
# add zero length segments to base curves
if layout.is_a("IfcSegmentedReferenceCurve"):
ifcopenshell.api.alignment.add_zero_length_segment(file, layout.BaseCurve)
elif layout.is_a("IfcGradientCurve"):
ifcopenshell.api.alignment.add_zero_length_segment(file, layout.BaseCurve)
else:
zero_length_curve_segment = None
if layout.is_a("IfcAlignmentHorizontal"):
x = 0.0
y = 0.0
dx = 1.0
dy = 0.0
last_segment = None
for rel in layout.IsNestedBy:
if 0 < len(rel.RelatedObjects):
last_segment = rel.RelatedObjects[-1]
break
if last_segment:
file.begin_transaction() # use a transaction so we can discard any temporary IFC entities created
settings = ifcopenshell.geom.settings()
mapped_segments = _map_alignment_horizontal_segment(file, last_segment)
geometry_segment = mapped_segments[0] if mapped_segments[1] == None else mapped_segments[1]
fn = wrapper.map_shape(settings, geometry_segment.wrapped_data)
eval = wrapper.function_item_evaluator(settings, fn)
e = np.array(eval.evaluate(fn.end()))
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
x = float(e[0, 3]) / unit_scale
y = float(e[1, 3]) / unit_scale
dx = float(e[0, 0])
dy = float(e[1, 0])
file.discard_transaction()
angle_unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file, "PLANEANGLEUNIT")
design_parameters = file.createIfcAlignmentHorizontalSegment(
StartPoint=file.createIfcCartesianPoint((x, y)),
StartDirection=math.atan2(dy, dx) / angle_unit_scale,
StartRadiusOfCurvature=0.0,
EndRadiusOfCurvature=0.0,
SegmentLength=0.0,
PredefinedType="LINE",
)
zero_length_curve_segment = file.createIfcAlignmentSegment(
GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters
)
elif layout.is_a("IfcAlignmentVertical"):
last_segment_dist_along = 0.0
last_segment_height = 0.0
last_segment_end_gradient = 0.0
last_segment = None
for rel in layout.IsNestedBy:
if 0 < len(rel.RelatedObjects):
last_segment = rel.RelatedObjects[-1]
break
if last_segment:
file.begin_transaction()
last_segment_dist_along = (
last_segment.DesignParameters.StartDistAlong + last_segment.DesignParameters.HorizontalLength
)
last_segment_end_gradient = last_segment.DesignParameters.EndGradient
settings = ifcopenshell.geom.settings()
mapped_segments = _map_alignment_vertical_segment(file, last_segment)
geometry_segment = mapped_segments[0] if mapped_segments[1] == None else mapped_segments[1]
fn = wrapper.map_shape(settings, geometry_segment.wrapped_data)
eval = wrapper.function_item_evaluator(settings, fn)
e = np.array(eval.evaluate(fn.end()))
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file)
last_segment_height = float(e[1, 3]) / unit_scale
file.discard_transaction()
design_parameters = file.createIfcAlignmentVerticalSegment(
StartDistAlong=last_segment_dist_along,
HorizontalLength=0.0,
StartHeight=last_segment_height,
StartGradient=last_segment_end_gradient,
EndGradient=last_segment_end_gradient,
PredefinedType="CONSTANTGRADIENT",
)
zero_length_curve_segment = file.createIfcAlignmentSegment(
GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters
)
elif layout.is_a("IfcAlignmentCant"):
last_segment_dist_along = 0.0
last_segment_cant_left = 0.0
last_segment_cant_right = 0.0
for rel in layout.IsNestedBy:
if 0 < len(rel.RelatedObjects):
last_segment = rel.RelatedObjects[-1]
last_segment_dist_along = (
last_segment.DesignParameters.StartDistAlong + last_segment.DesignParameters.HorizontalLength
)
last_segment_cant_left = (
last_segment.DesignParameters.EndCantLeft
if last_segment.DesignParameters.EndCantLeft != None
else last_segment.DesignParameters.StartCantLeft
)
last_segment_cant_right = (
last_segment.DesignParameters.EndCantRight
if last_segment.DesignParameters.EndCantRight != None
else last_segment.DesignParameters.StartCantRight
)
break
design_parameters = file.createIfcAlignmentCantSegment(
StartDistAlong=last_segment_dist_along,
HorizontalLength=0.0,
StartCantLeft=last_segment_cant_left,
StartCantRight=last_segment_cant_right,
PredefinedType="CONSTANTCANT",
)
zero_length_curve_segment = file.createIfcAlignmentSegment(
GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters
)
ifcopenshell.api.nest.assign_object(file, related_objects=[zero_length_curve_segment], relating_object=layout)
if include_referent:
alignment = ifcopenshell.api.alignment.get_alignment(layout)
station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
name = f"{_get_segment_start_point_label(zero_length_curve_segment,None)} ({ifcopenshell.util.alignment.station_as_string(file,station)})"
referent = ifcopenshell.api.alignment.add_stationing_referent(
file, alignment, 0.0, station, name, zero_length_curve_segment
)
referent.Description = f"Positions zero length segment {zero_length_curve_segment.id()}"
return True
@@ -0,0 +1,96 @@
# 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.nest
import ifcopenshell.util.alignment
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._add_zero_length_segment import _add_zero_length_segment
from ifcopenshell.api.alignment._create_geometric_representation import (
_create_geometric_representation,
)
def create(
file: ifcopenshell.file,
name: str,
include_vertical: bool = False,
include_cant: bool = False,
include_geometry: bool = True,
start_station: float = 0.0,
) -> entity_instance:
"""
Creates a new alignment with a horizontal layout. Optionally, vertical and cant layouts can be created as well.
The geometric representations are created as well, unless they are explicitly excluded.
Zero length segments are added at the end of the layouts and geometric representations.
The alignment is automatically aggreated to the project if it exists.
Use get_horizontal_layout(alignment), get_vertical_layout(alignment) and get_cant_layout(alignment) to get the
corresponding IfcAlignmentHorizontal, IfcAlignmentVertical, and IfcAlignmentCant layout entities.
If the alignment has Viennese Bend transition curves, create the segments in the cant layout before the horizontal layout using create_layout_segment().
The horizontal geometry in the Viennese Bend transition curves depends on the Viennese Bend cant parameters. create_layout_segment() automatically creates
the geometric representation from the semantic definition. The horizontal segment geometric representation will fail if the cant segment is not defined.
If geometric representations are created, the alignment stationing referent is also created using the start_station value. IfcReferent.ObjectPlacement
is required for linear positiion elements and IfcLinearPlacement is defined relative to alignment curve geometry.
:param file:
:param name: name assigned to IfcAlignment.Name
:param include_vertical: If True, IfcAlignmentVertical is created. IfcGradientCurve is created if include_geometry is True
:param include_cant: If True, IfcAlignmentCant is created. IfcSegmentedReferenceCurve is created if include_geometry is True
:param include_geometry: If True, the geometric representations are added
:param start_station: station value at the start of the alignment.
:return: Returns an IfcAlignment
"""
alignment = file.createIfcAlignment(
GlobalId=ifcopenshell.guid.new(),
Name=name,
)
alignment_layouts = []
alignment_layouts.append(file.createIfcAlignmentHorizontal(GlobalId=ifcopenshell.guid.new()))
if include_vertical:
alignment_layouts.append(file.createIfcAlignmentVertical(GlobalId=ifcopenshell.guid.new()))
if include_cant:
alignment_layouts.append(file.createIfcAlignmentCant(GlobalId=ifcopenshell.guid.new(), RailHeadDistance=1.0))
ifcopenshell.api.nest.assign_object(file, related_objects=alignment_layouts, relating_object=alignment)
if include_geometry:
_create_geometric_representation(file, alignment)
name = ifcopenshell.util.alignment.station_as_string(file, start_station)
referent = ifcopenshell.api.alignment.add_stationing_referent(
file, alignment, 0.0, start_station, name, alignment
)
for layout in alignment_layouts:
_add_zero_length_segment(file, layout)
# IFC 4.1.4.1.1 Alignment Aggregation To Project
project = file.by_type("IfcProject")[0]
if project:
ifcopenshell.api.aggregate.assign_object(file, products=[alignment], relating_object=project)
return alignment
@@ -0,0 +1,58 @@
# 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/>.
from collections.abc import Sequence
import ifcopenshell
import ifcopenshell.api.aggregate
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._create_offset_curve_representation import (
_create_offset_curve_representation,
)
def create_as_offset_curve(
file: ifcopenshell.file,
name: str,
offsets: Sequence[entity_instance],
start_station: float = 0.0,
) -> entity_instance:
"""
Creates a new IfcAlignment with an IfcOffsetCurveByDistances representation.
The IfcAlignment is aggreated to IfcProject
:param file:
:param name: name assigned to IfcAlignment.Name
:param offsets: offsets from the basis curve that defines the offset curve, expected to be IfcPointByDistanceExpression.
:param start_station: station value at the start of the alignment
:return: Returns an IfcAlignment
"""
alignment = file.createIfcAlignment(
GlobalId=ifcopenshell.guid.new(),
Name=name,
)
_create_offset_curve_representation(file, alignment, offsets)
# IFC 4.1.4.1.1 Alignment Aggregation To Project
project = file.by_type("IfcProject")[0]
if project:
ifcopenshell.api.aggregate.assign_object(file, products=[alignment], relating_object=project)
return alignment
@@ -0,0 +1,151 @@
# 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 math
from collections.abc import Sequence
import ifcopenshell
import ifcopenshell.api.aggregate
import ifcopenshell.api.alignment
import ifcopenshell.api.nest
import ifcopenshell.util.alignment
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._create_polyline_representation import (
_create_polyline_representation,
)
def _create_layout(file: ifcopenshell.file, alignment: entity_instance, points: Sequence[entity_instance]):
"""
I don't believe it is required for polylines, but the validation serivce gives an error if the alignment doesn't have a layout
"""
include_vertical = False if points[0].Dim == 2 else True
alignment_layouts = []
alignment_layouts.append(file.createIfcAlignmentHorizontal(GlobalId=ifcopenshell.guid.new()))
if include_vertical:
alignment_layouts.append(file.createIfcAlignmentVertical(GlobalId=ifcopenshell.guid.new()))
ifcopenshell.api.nest.assign_object(file, related_objects=alignment_layouts, relating_object=alignment)
start_dist_along = 0.0
for p1, p2 in zip(points, points[1:]):
x1, y1, z1 = p1.Coordinates
x2, y2, z2 = p2.Coordinates
dir = math.atan2(y2 - y1, x2 - x1)
gradient = (z2 - z1) / (x2 - x1)
length = math.sqrt(math.pow((x2 - x1), 2.0) + math.pow((y2 - y1), 2.0))
hsegment = file.createIfcAlignmentSegment(
ifcopenshell.guid.new(),
DesignParameters=file.createIfcAlignmentHorizontalSegment(
StartPoint=p1,
StartDirection=dir,
StartRadiusOfCurvature=0.0,
EndRadiusOfCurvature=0.0,
SegmentLength=length,
PredefinedType="LINE",
),
)
ifcopenshell.api.nest.assign_object(file, related_objects=[hsegment], relating_object=alignment_layouts[0])
if include_vertical:
vsegment = file.createIfcAlignmentSegment(
ifcopenshell.guid.new(),
DesignParameters=file.createIfcAlignmentVerticalSegment(
StartDistAlong=start_dist_along,
HorizontalLength=length,
StartHeight=z1,
StartGradient=gradient,
EndGradient=gradient,
PredefinedType="CONSTANTGRADIENT",
),
)
ifcopenshell.api.nest.assign_object(file, related_objects=[vsegment], relating_object=alignment_layouts[1])
start_dist_along += length
# zero length segment
hsegment = file.createIfcAlignmentSegment(
ifcopenshell.guid.new(),
DesignParameters=file.createIfcAlignmentHorizontalSegment(
StartPoint=points[-1],
StartDirection=dir,
StartRadiusOfCurvature=0.0,
EndRadiusOfCurvature=0.0,
SegmentLength=0.0,
PredefinedType="LINE",
),
)
ifcopenshell.api.nest.assign_object(file, related_objects=[hsegment], relating_object=alignment_layouts[0])
if include_vertical:
vsegment = file.createIfcAlignmentSegment(
ifcopenshell.guid.new(),
DesignParameters=file.createIfcAlignmentVerticalSegment(
StartDistAlong=start_dist_along,
HorizontalLength=0.0,
StartHeight=points[-1].Coordinates[-1],
StartGradient=gradient,
EndGradient=gradient,
PredefinedType="CONSTANTGRADIENT",
),
)
ifcopenshell.api.nest.assign_object(file, related_objects=[vsegment], relating_object=alignment_layouts[1])
def create_as_polyline(
file: ifcopenshell.file,
name: str,
points: Sequence[entity_instance],
start_station: float = 0.0,
) -> entity_instance:
"""
Creates a new IfcAlignment with an IfcPolyline representation.
The IfcAlignment is aggreated to IfcProject
:param file:
:param name: name assigned to IfcAlignment.Name
:param points: sequence of points defining the polyline
:param start_station: station value at the start of the alignment
:return: Returns an IfcAlignment
"""
alignment = file.createIfcAlignment(
GlobalId=ifcopenshell.guid.new(),
Name=name,
)
_create_polyline_representation(file, alignment, points)
# define stationing
name = ifcopenshell.util.alignment.station_as_string(file, start_station)
referent = ifcopenshell.api.alignment.add_stationing_referent(file, alignment, 0.0, start_station, name, alignment)
# IFC 4.1.4.1.1 Alignment Aggregation To Project
project = file.by_type("IfcProject")[0]
if project:
ifcopenshell.api.aggregate.assign_object(file, products=[alignment], relating_object=project)
return alignment
@@ -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/>.
from collections.abc import Sequence
import ifcopenshell
import ifcopenshell.api.alignment
from ifcopenshell import entity_instance
def create_by_pi_method(
file: ifcopenshell.file,
name: str,
hpoints: Sequence[Sequence[float]],
radii: Sequence[float],
vpoints: Sequence[Sequence[float]] = None,
lengths: Sequence[float] = None,
start_station: float = 0.0,
) -> 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.
:param 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
:return: Returns an IfcAlignment
"""
include_vertical = True if vpoints and lengths else False
alignment = ifcopenshell.api.alignment.create(
file, name, include_vertical=include_vertical, start_station=start_station
)
horizontal_layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
ifcopenshell.api.alignment.layout_horizontal_alignment_by_pi_method(file, horizontal_layout, hpoints, radii)
if include_vertical:
vertical_layout = ifcopenshell.api.alignment.get_vertical_layout(alignment)
ifcopenshell.api.alignment.layout_vertical_alignment_by_pi_method(file, vertical_layout, vpoints, lengths)
return alignment
@@ -0,0 +1,97 @@
# 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 csv
import ifcopenshell
import ifcopenshell.api.alignment
from ifcopenshell import entity_instance
def create_from_csv(file: ifcopenshell.file, filepath: str) -> entity_instance:
"""
Creates an alignment from PI data stored in a CSV file.
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:
alignment = ifcopenshell.api.alignment.create(file, "Alignment_from_CSV")
horizontal_layout = ifcopenshell.api.alignment.get_horizontal_layout(alignment)
ifcopenshell.api.alignment.layout_horizontal_alignment_by_pi_method(
file, horizontal_layout, coordinates, radii
)
else:
# add all subsequent vertical alignments
vertical_layout = ifcopenshell.api.alignment.add_vertical_layout(file, alignment)
ifcopenshell.api.alignment.layout_vertical_alignment_by_pi_method(
file, vertical_layout, coordinates, radii
)
return alignment
@@ -0,0 +1,87 @@
# 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/>.
from typing import Union
import numpy as np
import ifcopenshell
import ifcopenshell.api.alignment
import ifcopenshell.geom
from ifcopenshell import entity_instance, ifcopenshell_wrapper
from ifcopenshell.api.alignment._add_segment_to_layout import _add_segment_to_layout
def create_layout_segment(
file: ifcopenshell.file, layout: entity_instance, design_parameters: entity_instance
) -> Union[np.array, None]:
"""
Creates a new IfcAlignmentSegment using the IfcAlignmentParameterSegment design parameters.
The new segment is appended to the layout alignment and the corresponding IfcCurveSegment is created in the geometric representation if it exists.
:param layout: The layout to receive the new layout segment. This parameter is expected to be IfcAlignmentHorizontal, IfcAlignmentVertical or IfcAlignmentCant
:param design_parameters: The parameters defining the segment. Expected to be the appropreate subclass of IfcAlignmentParameterSegment
:return: 4x4 matrix at end of segment as np.array intended to be used as the start point geometry for the next segment or None if there is the geometric representation is not defined.
"""
expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"]
if not layout.is_a() in expected_types:
raise TypeError(
f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received {layout.is_a()}"
)
if layout.is_a("IfcAlignmentHorizontal") and not design_parameters.is_a("IfcAlignmentHorizontalSegment"):
raise TypeError("Expected design_parameters to be IfcAlignmentHorizontalSegment")
elif layout.is_a("IfcAlignmentVertical") and not design_parameters.is_a("IfcAlignmentVerticalSegment"):
raise TypeError("Expected design_parameters to be IfcAlignmentVerticalSegment")
elif layout.is_a("IfcAlignmentCant") and not design_parameters.is_a("IfcAlignmentCantSegment"):
raise TypeError("Expected design_parameters to be IfcAlignmentCantSegment")
# create the segment and add it to the layout.
segment = file.createIfcAlignmentSegment(GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters)
_add_segment_to_layout(file, layout, segment) # adds to layout and geometric representation
# compute the 4x4 matrix at the end of the segment so this information can be
# returned and used when defining the next segment
alignment = ifcopenshell.api.alignment.get_alignment(layout)
curve = ifcopenshell.api.alignment.get_curve(alignment)
if curve:
if layout.is_a("IfcAlignmentHorizontal"):
if curve.is_a("IfcGradientCurve"):
curve = curve.BaseCurve
elif curve.is_a("IfcSegmentedReferenceCurve"):
curve = (
curve.BaseCurve.BaseCurve
) # layout is horizontal and curve is segmented ref ... we want the curve's base curve
elif layout.is_a("IfcAlignmentVertical"):
if curve.is_a("IfcSegmentedReferenceCurve"):
curve = curve.BaseCurve
# the new segment is two from the end... the end segment is zero length
curve_segment = curve.Segments[-2]
settings = ifcopenshell.geom.settings()
segment_fn = ifcopenshell_wrapper.map_shape(settings, curve_segment.wrapped_data)
segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn)
e = segment_evaluator.evaluate(segment_fn.end())
end = np.array(e)
return end
else:
return None
@@ -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 ifcopenshell
import ifcopenshell.api.alignment
from ifcopenshell import entity_instance
from ifcopenshell.api.alignment._add_segment_to_curve import _add_segment_to_curve
from ifcopenshell.api.alignment._create_geometric_representation import (
_create_geometric_representation,
)
def create_representation(
file: ifcopenshell.file,
alignment: entity_instance,
) -> None:
"""
Creates the geometric representation of an alignment if it does not already exist.
This function is intended to be used when a model has only the semantic definition of an alignment
and you want to add the geometric representation.
If the alignments are complete, it is recommended that add_zero_length_segment is called after this method to ensure
the proper structure of the semantic and geometric definitions of the alignment
:param alignment: The alignment to create the representation.
"""
expected_type = "IfcAlignment"
if not alignment.is_a(expected_type):
raise TypeError(f"Expected to see type '{expected_type}', instead received '{alignment.is_a()}'.")
if alignment.Representation:
return
_create_geometric_representation(file, alignment)
layouts = ifcopenshell.api.alignment.get_alignment_layouts(alignment)
for layout in layouts:
curve = ifcopenshell.api.alignment.get_layout_curve(layout)
layout_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout)
for segment in layout_nest.RelatedObjects:
_add_segment_to_curve(file, segment, curve)
@@ -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 ifcopenshell
import ifcopenshell.api.alignment
import ifcopenshell.util.element
import ifcopenshell.util.representation
from ifcopenshell import entity_instance
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 = next(
c for c in ifcopenshell.util.element.get_components(alignment) if c.is_a("IfcAlignmentHorizontal")
)
elif representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve3D":
curve = ifcopenshell.api.alignment.get_curve(alignment)
nested_alignment = next(
c for c in ifcopenshell.util.element.get_components(alignment) if c.is_a("IfcAlignmentVertical")
)
curve_segments = curve.Segments
segments = nested_alignment.IsNestedBy[0].RelatedObjects
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

Some files were not shown because too many files have changed in this diff Show More