First Commit
This commit is contained in:
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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))
|
||||
Binary file not shown.
@@ -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",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
+158
@@ -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
|
||||
+76
@@ -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
|
||||
+554
@@ -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
|
||||
+217
@@ -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
|
||||
+29
@@ -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
|
||||
@@ -0,0 +1,48 @@
|
||||
# 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
|
||||
|
||||
|
||||
def distance_along_from_station(file: ifcopenshell.file, alignment: entity_instance, station: float) -> float:
|
||||
"""
|
||||
Given a station, returns the distance along the horizontal alignment.
|
||||
|
||||
If the alignment does not have stationing defined with an IfcReferent, the start of the alignment is assumed
|
||||
to be at station 0.0. That is, the station is the distance along.
|
||||
|
||||
.. note:: The current implementation does not account for station equations and assumes stationing is increasing along the alignment.
|
||||
|
||||
:param alignment: the alignment
|
||||
:param station: station value
|
||||
:return: distance along the horizontal alignment
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
alignment = model.by_type("IfcAlignment")[0] # alignment with start station 1+00.00
|
||||
dist_along = ifcopenshell.api.alignment.distance_along_from_station(model,alignment=alignment,station=200.0)
|
||||
print(dist_along) # 100.00
|
||||
"""
|
||||
|
||||
start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, alignment)
|
||||
dist_along = station - start_station
|
||||
return dist_along
|
||||
@@ -0,0 +1,32 @@
|
||||
# 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
|
||||
|
||||
|
||||
def get_alignment(layout: entity_instance) -> entity_instance:
|
||||
"""
|
||||
Returns the alignment that nests this layout
|
||||
"""
|
||||
alignment = None
|
||||
for nest in layout.Nests:
|
||||
if nest.RelatingObject.is_a("IfcAlignment"):
|
||||
alignment = nest.RelatingObject
|
||||
break
|
||||
|
||||
return alignment
|
||||
@@ -0,0 +1,36 @@
|
||||
# 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
|
||||
|
||||
|
||||
def get_alignment_layout_nest(alignment: entity_instance) -> entity_instance:
|
||||
"""
|
||||
Searches for the IfcRelNest that contains IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant
|
||||
|
||||
:param alignment: the alignment
|
||||
:return: Returns the IfcRelNests containing the alignment layout
|
||||
"""
|
||||
layout_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"]
|
||||
|
||||
for nest in alignment.IsNestedBy:
|
||||
for related_object in nest.RelatedObjects:
|
||||
if related_object.is_a() in layout_types:
|
||||
return nest
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,38 @@
|
||||
# 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
|
||||
|
||||
from ifcopenshell import entity_instance
|
||||
|
||||
|
||||
def get_alignment_layouts(alignment: entity_instance) -> Sequence[entity_instance]:
|
||||
"""
|
||||
Returns the layout alignments nested to this alignment
|
||||
"""
|
||||
layouts = []
|
||||
for rel in alignment.IsNestedBy:
|
||||
for layout in rel.RelatedObjects:
|
||||
if (
|
||||
layout.is_a("IfcAlignmentHorizontal")
|
||||
or layout.is_a("IfcAlignmentVertical")
|
||||
or layout.is_a("IfcAlignmentCant")
|
||||
):
|
||||
layouts.append(layout)
|
||||
|
||||
return layouts
|
||||
@@ -0,0 +1,33 @@
|
||||
# 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
|
||||
|
||||
|
||||
def get_alignment_segment_nest(layout: entity_instance) -> entity_instance:
|
||||
"""
|
||||
Searches for the IfcRelNest that contains IfcAlignmentSegment
|
||||
|
||||
:param layout: an alignment layout, expected to be one of IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant
|
||||
:return: Returns the IfcRelNests
|
||||
"""
|
||||
for nest in layout.IsNestedBy:
|
||||
for related_object in nest.RelatedObjects:
|
||||
if related_object.is_a("IfcAlignmentSegment"):
|
||||
return nest
|
||||
return None
|
||||
@@ -0,0 +1,48 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2025 Thomas Krijnen <thomas@aecgeeks.com>
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.alignment
|
||||
import ifcopenshell.util.element
|
||||
from ifcopenshell import entity_instance
|
||||
|
||||
|
||||
def get_alignment_start_station(file: ifcopenshell.file, alignment: entity_instance) -> float:
|
||||
"""
|
||||
Returns the start station of the alignment. The starting station is defined by the first nested IfcReferent.
|
||||
This is interpreted to mean the first IfcReferent with an occurance of Pset_Stationing.Station,
|
||||
otherwise returns 0.0.
|
||||
"""
|
||||
|
||||
if not alignment.is_a("IfcAlignment"):
|
||||
raise TypeError(f"Expected entity type to be IfcAlignment, instead received {alignment.is_a()}")
|
||||
|
||||
start_station = 0.0
|
||||
|
||||
parent_alignment = ifcopenshell.api.alignment.get_parent_alignment(alignment)
|
||||
if parent_alignment:
|
||||
start_station = ifcopenshell.api.alignment.get_alignment_start_station(file, parent_alignment)
|
||||
else:
|
||||
components = ifcopenshell.util.element.get_components(alignment)
|
||||
for c in components:
|
||||
if c.is_a("IfcReferent"):
|
||||
start_station = ifcopenshell.util.element.get_pset(c, name="Pset_Stationing", prop="Station")
|
||||
if not start_station == None:
|
||||
break
|
||||
|
||||
return start_station
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user