Files
2026-05-31 10:17:09 +07:00

719 lines
26 KiB
Python

# 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/>.
from __future__ import annotations
from collections.abc import Generator, Iterable
from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union, cast, overload
from .. import ifcopenshell_wrapper, open
from ..entity_instance import entity_instance
from ..file import file
from . import has_occ
if TYPE_CHECKING:
from OCC.Core import TopoDS # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
IteratorOutput = Union["ShapeElementType", "utils.shape_tuple"]
T = TypeVar("T")
ShapeElementType = Union[
ifcopenshell_wrapper.BRepElement, ifcopenshell_wrapper.TriangulationElement, ifcopenshell_wrapper.SerializedElement
]
ShapeType = Union[ifcopenshell_wrapper.BRep, ifcopenshell_wrapper.Triangulation, ifcopenshell_wrapper.Serialization]
def wrap_shape_creation(settings, shape):
return shape
if has_occ:
from . import occ_utils as utils
try:
from OCC.Core import TopoDS # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
except ImportError:
from OCC import TopoDS # pyright: ignore[reportMissingImports] # ty:ignore[unresolved-import]
def wrap_shape_creation(settings: settings, shape: ifcopenshell_wrapper.Element):
if getattr(settings, "use_python_opencascade", False):
return utils.create_shape_from_serialization(shape)
else:
return shape
SETTING = Literal[
"angle-unit",
"apply-default-materials",
"apply-offset",
"boolean-attempt-2d",
"building-local-placement",
"cache-shapes",
"cgal-original-edges",
"cgal-smooth-angle-degrees",
"circle-segments",
"compute-curvature",
"context-identifiers",
"context-ids",
"context-types",
"convert-back-units",
"debug",
"defer-processing-first-element",
"dimensionality",
"disable-boolean-result",
"disable-opening-subtractions",
"edge-arrows",
"element-hierarchy",
"enable-layerset-slicing",
"force-space-transparency",
"function-step-param",
"function-step-type",
"generate-uvs",
"iterator-output",
"keep-bounding-boxes",
"layerset-first",
"length-unit",
"make-volume",
"max-offset-deviation",
"max-offset",
"mesher-angular-deflection",
"mesher-linear-deflection",
"model-offset",
"model-rotation",
"no-clean-triangulation",
"no-normals",
"no-parallel-mapping",
"no-wire-intersection-check",
"no-wire-intersection-tolerance",
"permissive-shape-reuse",
"precision-factor",
"precision",
"reorient-shells",
"site-local-placement",
"surface-colour",
"triangulation-type",
"unify-shapes",
"use-material-names",
"use-python-opencascade",
"use-world-coords",
"validate",
"weld-vertices",
]
SERIALIZER_SETTING = Literal[
"base-uri",
"use-element-names",
"use-element-guids",
"use-element-step-ids",
"use-element-types",
"y-up",
"ecef",
"digits",
"wkt-use-section",
"separate-z-up-node",
]
# NOTE: hybrid-cgal-simple-opencascade is added just as an example
# It's possible to use any hybrid combination by the format below:
# "hybrid-library1-library2".
# List is updated from AbstractKernel.cpp.
GEOMETRY_LIBRARY = Literal["cgal", "cgal-simple", "opencascade", "hybrid-cgal-simple-opencascade"]
class missing_setting:
def __repr__(self):
return "-"
class settings_mixin:
"""
Pythonic interface mixin to the settings modules and
to provide an additional setting to enable pythonOCC
when available
"""
def __init__(self, **kwargs):
super().__init__()
for k, v in kwargs.items():
self.set(getattr(self, k), v)
def __repr__(self):
def safe_get(x):
try:
return self.get(x)
except RuntimeError:
return missing_setting()
fmt_pair = lambda x: "%s = %r" % (self.rname(x), safe_get(x))
return "%s(%s)" % (type(self).__name__, ", ".join(map(fmt_pair, self.setting_names())))
@staticmethod
def name(k: str) -> Union[SETTING, SERIALIZER_SETTING]:
return k.lower().replace("_", "-")
@staticmethod
def rname(k: Union[SETTING, SERIALIZER_SETTING]) -> str:
return k.upper().replace("-", "_")
@overload
def set(self: settings, k: SETTING, v: Any) -> None: ...
@overload
def set(self: serializer_settings, k: SERIALIZER_SETTING, v: Any) -> None: ...
def set(self, k: SETTING, v: Any) -> None:
"""
Set value of the setting named `k` to `v`.
:raises RuntimeError: If there is no setting with name `k`.
"""
k = self.name(k)
if isinstance(self, settings) and k == "use-python-opencascade":
if not has_occ:
raise AttributeError("Python OpenCASCADE is not installed")
if v:
self.set_("iterator-output", ifcopenshell_wrapper.SERIALIZED)
self.set_("use-world-coords", True)
self.use_python_opencascade = True
else:
self.set_(self.name(k), v)
@overload
def get(self: settings, k: SETTING) -> Any: ...
@overload
def get(self: serializer_settings, k: SERIALIZER_SETTING) -> Any: ...
def get(self, k: str) -> Any:
"""
Return value of the setting named `k`.
:raises RuntimeError: If there is no setting with name `k`.
"""
k = self.name(k)
if isinstance(self, settings) and k == "use-python-opencascade":
return self.use_python_opencascade
return self.get_(k)
@overload
def setting_names(self: settings) -> tuple[SETTING, ...]: ...
@overload
def setting_names(self: serializer_settings) -> tuple[SERIALIZER_SETTING, ...]: ...
def setting_names(self) -> tuple[str, ...]:
setting_names = super().setting_names()
if isinstance(self, settings):
setting_names += ("use-python-opencascade",)
return setting_names
@overload
def __getattr__(self: settings, k: str) -> SETTING: ...
@overload
def __getattr__(self: serializer_settings, k: str) -> SERIALIZER_SETTING: ...
def __getattr__(self, k: str) -> str:
# Swig wrapper will try to access "this",
# ensure we won't accidentally call any c-extension methods
# like .setting_names() until wrapper is not completely initialized.
# See #4861.
if k == "this":
raise AttributeError("Swig wrapper's 'this' is unset.")
if k in map(self.rname, self.setting_names()):
return k
else:
raise AttributeError("'Settings' object has no attribute '%s'" % k)
def build_parser(self, parser) -> None:
"""
Accepts an argparse.ArgumentParser object, enumerates the settings in this container and
adds argument parser rules for each.
"""
type_factories = {
"bool": bool,
"int": int,
"double": float,
"std::string": str,
"std::set<int>": lambda s: list(map(int, s.split(";"))),
"std::set<std::string>": lambda s: s.split(";"),
"std::vector<double>": lambda s: list(map(float, s.split(";"))),
"IteratorOutputOptions": int,
"FunctionStepMethod": int,
"OutputDimensionalityTypes": int,
"TriangulationMethod": int,
}
for nm in self.setting_names():
if nm == "use-python-opencascade":
ty == "bool"
else:
ty = self.get_type(nm)
if ty == "bool":
group = parser.add_mutually_exclusive_group()
group.add_argument(
f"--{nm}",
dest=nm.replace("-", "_"),
action="store_true",
)
group.add_argument(
f"--no-{nm}",
dest=nm.replace("-", "_"),
action="store_false",
)
parser.set_defaults(**{nm.replace("-", "_"): None})
else:
parser.add_argument(f"--{nm}", dest=nm.replace("-", "_"), type=type_factories[ty])
def apply_namespace(self, namespace) -> None:
"""
Accepts an argparse.Namespace object, enumerates over the values in this namespace and
writes them to the settings when available
"""
names = set(self.setting_names())
for k, v in namespace._get_kwargs():
if k.replace("_", "-") in names and v is not None:
self.set(k.replace("_", "-"), v)
class serializer_settings(settings_mixin, ifcopenshell_wrapper.SerializerSettings):
pass
class settings(settings_mixin, ifcopenshell_wrapper.Settings):
use_python_opencascade = False
class iterator(ifcopenshell_wrapper.Iterator):
def __init__(
self,
settings: settings,
file_or_filename: Union[file, str],
num_threads: int = 1,
include: Optional[Union[list[entity_instance], list[str]]] = None,
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
):
self.settings = settings
if isinstance(file_or_filename, file):
self.file = file
file_or_filename = file_or_filename.wrapped_data
else:
file_or_filename = self.file = open(file_or_filename)
if include is not None and exclude is not None:
raise ValueError("include and exclude cannot be specified simultaneously")
if include is not None or exclude is not None:
# Couldn't get the typemaps properly applied using %extend so we
# replicate the SWIG-generated __init__ call on the output of a
# free function.
# @todo verify this works with SWIG 4
include_or_exclude = include if exclude is None else exclude
include_or_exclude_type = set(x.__class__.__name__ for x in include_or_exclude)
if include_or_exclude_type == {"entity_instance"}:
include_or_exclude = cast(set[entity_instance], include_or_exclude)
if not all((last_inst := inst).is_a("IfcProduct") for inst in include_or_exclude):
raise ValueError(
f"include and exclude need to be an aggregate of IfcProduct. Violating element: '{last_inst}'."
)
initializer = ifcopenshell_wrapper.construct_iterator_with_include_exclude_id
include_or_exclude = [i.id() for i in include_or_exclude]
else:
initializer = ifcopenshell_wrapper.construct_iterator_with_include_exclude
self.this = initializer(
geometry_library, self.settings, file_or_filename, include_or_exclude, include is not None, num_threads
)
else:
self.this = ifcopenshell_wrapper.construct_iterator(
geometry_library, self.settings, file_or_filename, num_threads
)
if has_occ:
def get(self):
return wrap_shape_creation(self.settings, ifcopenshell_wrapper.Iterator.get(self))
def __iter__(self) -> Generator[IteratorOutput, None, None]:
if self.initialize():
while True:
yield self.get()
if not self.next():
break
def get_task_products(self):
return entity_instance.wrap_value(ifcopenshell_wrapper.Iterator.get_task_products(self), self.file)
ClashType = Literal["protrusion", "pierce", "collision", "clearance"]
CLASH_TYPE_ITEMS = ("protrusion", "pierce", "collision", "clearance")
class tree(ifcopenshell_wrapper.tree):
def __init__(self, file: Optional[file] = None, settings: Optional[settings] = None):
args = [self]
if file is not None:
args.append(file.wrapped_data)
if settings is not None:
args.append(settings)
ifcopenshell_wrapper.tree.__init__(*args)
def add_file(self, file: file, settings: settings) -> None:
ifcopenshell_wrapper.tree.add_file(self, file.wrapped_data, settings)
def add_iterator(self, iterator: iterator) -> None:
ifcopenshell_wrapper.tree.add_file(self, iterator)
def select(
self,
value: Union[
entity_instance, ifcopenshell_wrapper.BRepElement, tuple[float, float, float], TopoDS.TopoDS_Shape
],
**kwargs,
) -> list[entity_instance]:
def unwrap(value):
if isinstance(value, entity_instance):
return value.wrapped_data
elif all(map(lambda v: hasattr(value, v), "XYZ")):
return value.X(), value.Y(), value.Z()
return value
args = [self, unwrap(value)]
if isinstance(value, (entity_instance, ifcopenshell_wrapper.BRepElement)):
args.append(kwargs.get("completely_within", False))
if "extend" in kwargs:
args.append(kwargs["extend"])
elif isinstance(value, (list, tuple)) and len(value) == 3 and set(map(type, value)) == {float}:
if "extend" in kwargs:
args.append(kwargs["extend"])
elif has_occ:
if isinstance(value, TopoDS.TopoDS_Shape):
args[1] = utils.serialize_shape(value)
args.append(kwargs.get("completely_within", False))
if "extend" in kwargs:
args.append(kwargs["extend"])
return [entity_instance(e) for e in ifcopenshell_wrapper.tree.select(*args)]
def select_box(self, value, **kwargs) -> list[entity_instance]:
def unwrap(value):
if isinstance(value, entity_instance):
return value.wrapped_data
elif hasattr(value, "Get"):
return value.Get()[:3], value.Get()[3:]
return value
args = [self, unwrap(value)]
if "extend" in kwargs or "completely_within" in kwargs:
args.append(kwargs.get("completely_within", False))
if "extend" in kwargs:
args.append(kwargs.get("extend", -1.0e-5))
return [entity_instance(e) for e in ifcopenshell_wrapper.tree.select_box(*args)]
def clash_intersection_many(
self,
set_a: Iterable[entity_instance],
set_b: Iterable[entity_instance],
tolerance: float = 0.002,
check_all: bool = True,
) -> tuple[ifcopenshell_wrapper.clash, ...]:
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], tolerance, check_all]
return ifcopenshell_wrapper.tree.clash_intersection_many(*args)
def clash_collision_many(
self, set_a: Iterable[entity_instance], set_b: Iterable[entity_instance], allow_touching=False
) -> tuple[ifcopenshell_wrapper.clash, ...]:
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], allow_touching]
return ifcopenshell_wrapper.tree.clash_collision_many(*args)
def clash_clearance_many(
self,
set_a: Iterable[entity_instance],
set_b: Iterable[entity_instance],
clearance: float = 0.05,
check_all: bool = False,
) -> tuple[ifcopenshell_wrapper.clash, ...]:
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], clearance, check_all]
return ifcopenshell_wrapper.tree.clash_clearance_many(*args)
@staticmethod
def get_clash_type(clash_type_i: int) -> ClashType:
"""Convert clash type index to a readable string format.
:param clash_type_i: Type index that comes from ``clash.clash_type``.
"""
return CLASH_TYPE_ITEMS[clash_type_i]
def create_shape(
settings: settings,
inst: entity_instance,
repr: Optional[entity_instance] = None,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Union[ShapeType, ShapeElementType, ifcopenshell_wrapper.Transformation, utils.shape_tuple, TopoDS.TopoDS_Shape]:
"""
Returns a geometric interpretation of the IFC entity instance
Note that in Python, you must store a reference to the element returned by this function to prevent garbage
collection when you access its children. See #1124.
:raises RuntimeError: If failed to process shape. You can turn detailed logging to get more details.
:return:
- `inst` is IfcProduct and `repr` provided / None -> ShapeElementType\n
- `inst` is IfcRepresentation and `repr` is None -> ShapeType\n
- `inst` is IfcRepresentationItem and `repr` is None -> ShapeType\n
- `inst` is IfcProfileDef and `repr` is None -> ShapeType\n
- `inst` is IfcPlacement / IfcObjectPlacement -> Transformation\n
- `inst` is IfcTypeProduct and `repr` is None -> None\n
- `inst` is IfcTypeProduct and `repr` is provided -> RuntimeError
(for IfcTypeProducts provide just IfcRepresentation as `inst`).\n
If 'use-python-opencascade' is enabled in settings then\n
- instead of ShapeElementType it returns shape_tuple, \n
- instead of ShapeType it returns TopoDS.TopoDS_Shape.
Example:
.. code:: python
settings = ifcopenshell.geom.settings()
settings.set("use-python-opencascade", True)
ifc_file = ifcopenshell.open(file_path)
products = ifc_file.by_type("IfcProduct")
for i, product in enumerate(products):
if product.Representation is not None:
try:
created_shape = geom.create_shape(settings, inst=product)
shape = created_shape.geometry # see #1124
shape_gpXYZ = shape.Location().Transformation().TranslationPart() # These are methods of the TopoDS_Shape class from pythonOCC
print(shape_gpXYZ.X(), shape_gpXYZ.Y(), shape_gpXYZ.Z()) # These are methods of the gpXYZ class from pythonOCC
except:
print("Shape creation failed")
"""
return wrap_shape_creation(
settings,
ifcopenshell_wrapper.create_shape(
settings, inst.wrapped_data, repr.wrapped_data if repr is not None else None, geometry_library
),
)
def map_shape(settings: settings, inst: entity_instance) -> ifcopenshell_wrapper.item:
"""
Returns an interpretation of the geometry encoded as per IfcOpenShell's taxonomy layer.
In many cases this is somewhat equivalent to the raw IFC data (but schema-agnostic in C++), but
in other cases such as IfcParameterizedProfileDef the returned item is the equivalent
of an explicit composite curve.
>>> point = ifc_file.by_type('IfcCartesianPoint')[0]
>>> ifcopenshell.geom.map_shape(ifcopenshell.geom.settings(), point).components
(0.0, 0.0, 0.0)
"""
return ifcopenshell_wrapper.map_shape(settings, inst.wrapped_data)
@overload
def consume_iterator(it: iterator, with_progress: Literal[False] = False) -> Generator[IteratorOutput, None, None]: ...
@overload
def consume_iterator(
it: iterator, with_progress: Literal[True]
) -> Generator[tuple[int, IteratorOutput], None, None]: ...
@overload
def consume_iterator(
it: iterator, with_progress: bool
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]: ...
def consume_iterator(
it: iterator, with_progress: bool = False
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]:
if it.initialize():
while True:
if with_progress:
yield it.progress(), it.get()
else:
yield it.get()
if not it.next():
break
# Overloads need to cover different return types
# based on `with_progress` argument.
@overload
def iterate(
settings: settings,
file_or_filename: Union[file, str],
num_threads: int = 1,
include: Optional[Union[list[entity_instance], list[str]]] = None,
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
*,
with_progress: Literal[False] = False,
cache: Optional[str] = None,
serializer_settings: Optional[serializer_settings] = None,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Generator[IteratorOutput, None, None]: ...
@overload
def iterate(
settings: settings,
file_or_filename: Union[file, str],
num_threads: int = 1,
include: Optional[Union[list[entity_instance], list[str]]] = None,
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
*,
with_progress: Literal[True] = True,
cache: Optional[str] = None,
serializer_settings: Optional[serializer_settings] = None,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Generator[tuple[int, IteratorOutput], None, None]: ...
@overload
def iterate(
settings: settings,
file_or_filename: Union[file, str],
num_threads: int = 1,
include: Optional[Union[list[entity_instance], list[str]]] = None,
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
*,
with_progress: bool = False,
cache: Optional[str] = None,
serializer_settings: Optional[serializer_settings] = None,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]: ...
def iterate(
settings: settings,
file_or_filename: Union[file, str],
num_threads: int = 1,
include: Optional[Union[list[entity_instance], list[str]]] = None,
exclude: Optional[Union[list[entity_instance], list[str]]] = None,
*,
with_progress: bool = False,
cache: Optional[str] = None,
serializer_settings: Optional[serializer_settings] = None,
geometry_library: GEOMETRY_LIBRARY = "opencascade",
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]:
"""Get a geometry iterator for the provided file.
:param cache: .h5 cache filepath (might not exist, will be created).
:param serializer_settings: Settings for cache serializer. Required if `cache` is provided.
"""
it = iterator(settings, file_or_filename, num_threads, include, exclude, geometry_library)
if cache:
assert serializer_settings, "`serializer_settings` argument is not optional if `cache` is provided."
hdf5_cache = serializers.hdf5(cache, settings, serializer_settings)
it.set_cache(hdf5_cache)
yield from consume_iterator(it, with_progress=with_progress)
def make_shape_function(fn):
def entity_instance_or_none(e):
return None if e is None else entity_instance(e)
if has_occ:
def _(schema, string_or_shape, *args):
if isinstance(string_or_shape, TopoDS.TopoDS_Shape):
string_or_shape = utils.serialize_shape(string_or_shape)
return entity_instance_or_none(fn(schema, string_or_shape, *args))
else:
def _(schema, string, *args):
return entity_instance_or_none(fn(schema, string, *args))
return _
serialise = make_shape_function(ifcopenshell_wrapper.serialise)
tesselate = make_shape_function(ifcopenshell_wrapper.tesselate)
def transform_string(v: Union[str, serializers.buffer]) -> serializers.buffer:
if isinstance(v, str):
return ifcopenshell_wrapper.buffer(v)
return v
class serializers:
# Python does not have automatic casts. The C++ serializers accept a stream_or_filename
# which in C++ can be automatically constructed from a filename string. In Python we
# have to implement this cast/construction explicitly by transform_string.
@staticmethod
def obj(
out_filename: Union[str, serializers.buffer],
mtl_filename: Union[str, serializers.buffer],
geometry_settings: settings,
settings: serializer_settings,
) -> ifcopenshell_wrapper.WaveFrontOBJSerializer:
out_filename = transform_string(out_filename)
mtl_filename = transform_string(mtl_filename)
return ifcopenshell_wrapper.WaveFrontOBJSerializer(out_filename, mtl_filename, geometry_settings, settings)
@staticmethod
def svg(
out_filename: Union[str, serializers.buffer], geometry_settings: settings, settings: serializer_settings
) -> ifcopenshell_wrapper.SvgSerializer:
out_filename = transform_string(out_filename)
return ifcopenshell_wrapper.SvgSerializer(out_filename, geometry_settings, settings)
# Hdf- Xml- and glTF- serializers don't support writing to a buffer, only to filename
# so no wrap_buffer_creation() for these serializers
xml = ifcopenshell_wrapper.XmlSerializer
buffer = ifcopenshell_wrapper.buffer
# gltf, hdf5, collada and json availability depend on IfcOpenShell configuration settings
try:
gltf = ifcopenshell_wrapper.GltfSerializer
except:
pass
try:
hdf5 = ifcopenshell_wrapper.HdfSerializer
except:
pass
try:
collada = ifcopenshell_wrapper.ColladaSerializer
except:
pass
try:
json = ifcopenshell_wrapper.JsonSerializer
except:
pass
# ttl is always available since it doesn't depend on any C++ libraries,
# just people might be using an outdated binary
if hasattr(ifcopenshell_wrapper, "TtlWktSerializer"):
@staticmethod
def ttl(
out_filename: Union[str, serializers.buffer], geometry_settings: settings, settings: serializer_settings
) -> ifcopenshell_wrapper.SvgSerializer:
out_filename = transform_string(out_filename)
return ifcopenshell_wrapper.TtlWktSerializer(out_filename, geometry_settings, settings)
@classmethod
def guess_from_extension(cls, filepath: str):
ext = filepath.split(".")[-1]
mapping = {
"glb": "gltf",
"hdf": "hdf5",
"h5": "hdf5",
"hdf5": "hdf5",
"obj": "obj",
"svg": "svg",
"ttl": "ttl",
"xml": "xml",
"dae": "collada",
}
serializer_name = mapping.get(ext)
if not serializer_name:
raise ValueError(f"No serializer available for .{ext} file")
return getattr(cls, serializer_name)