Files
IfcOpenShell/src/ifcopenshell-python/ifcopenshell/geom/main.py
T
Petru Conduraru 824c1fc280 ifcwrap: keep geometry's owning element alive to fix silent data corruption (#1124)
create_shape() returns a Python-owned Element (SWIG_POINTER_OWN in the
boost::variant out typemap). Its .geometry property calls Element::geometry(),
which returns a reference into the element's boost::shared_ptr<Representation>
_geometry member. SWIG wraps that reference as a non-owning pointer, so the
returned Triangulation/BRep/Serialization proxy does not keep the element alive.

When a caller keeps only .geometry (e.g. create_shape(s, e).geometry) and drops
the parent element, Python garbage-collects the element, destroying its
shared_ptr and freeing the underlying representation. Subsequent reads of
verts/faces then return freed memory: empty or implausible float/int garbage,
non-deterministically depending on GC and allocator timing. This is silent data
corruption, not a crash, and has bitten users since 2020.

Fix: in the TriangulationElement/SerializedElement/BRepElement pythoncode, wrap
the geometry getter so the returned geometry stores a backreference to its
owning element (result._parent = self). This makes the parent's lifetime at
least as long as the geometry's, automatically and transparently, so no caller
has to remember to hold the element. This is aothms's suggested backreference,
applied generically in the binding rather than left as a workaround.

Reproduced deterministically (washBasin fixture): before, verts len 0 vs 133500
across repeated GC-pressure runs; after, 133500 every run for all three element
types. test_create_shape passes; no regressions.

Note: tree.select_ray()'s ray_intersection_result (2024 follow-up in #1124) is a
separate ownership mechanism (std::vector element reference + std::array member
pointer) and is left as follow-up scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 10:04:48 +02:00

744 lines
27 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
import ifcopenshell
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",
"svg-emit-flush-edges",
"svg-render-crease-edges",
"svg-render-sharp-edges",
"svg-ridge-angle-min-degrees",
"svg-use-edge-classification",
"svg-valley-angle-min-degrees",
"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: Union[SETTING, SERIALIZER_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",
logger=None,
):
self.settings = settings
logger = ifcopenshell.logger_or_root(logger)
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, logger=logger)
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)
for inst in include_or_exclude:
if not inst.is_a("IfcProduct"):
raise ValueError(
f"include and exclude need to be an aggregate of IfcProduct. Violating element: '{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
args = (
geometry_library,
self.settings,
file_or_filename,
include_or_exclude,
include is not None,
num_threads,
)
self.this = initializer(*args, *ifcopenshell.optional_logger_args(logger))
else:
args = (geometry_library, self.settings, file_or_filename, num_threads)
self.this = ifcopenshell_wrapper.construct_iterator(*args, *ifcopenshell.optional_logger_args(logger))
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",
logger: Optional[ifcopenshell.logger] = None,
) -> Union[ShapeType, ShapeElementType, ifcopenshell_wrapper.Transformation, utils.shape_tuple, TopoDS.TopoDS_Shape]:
"""
Returns a geometric interpretation of the IFC entity instance
The returned element's ``geometry`` keeps a reference to its owning element, so accessing children
(e.g. ``create_shape(...).geometry.verts``) no longer requires holding onto the element. 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,
*((logger,) if logger is not None else ()),
),
)
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",
logger=None,
) -> 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",
logger=None,
) -> 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",
logger=None,
) -> 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",
logger=None,
) -> 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, logger)
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)