mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-05 23:41:44 +00:00
824c1fc280
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>
744 lines
27 KiB
Python
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)
|