mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 02:47:48 +00:00
768 lines
27 KiB
Python
768 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 os import PathLike, fspath
|
|
from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union, cast, overload
|
|
|
|
import ifcopenshell
|
|
|
|
from .. import ifcopenshell_wrapper, open
|
|
from .. import entity_instance
|
|
from .. 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",
|
|
"auto-elevation",
|
|
"auto-section",
|
|
"base-uri",
|
|
"boolean-attempt-2d",
|
|
"building-local-placement",
|
|
"bounds",
|
|
"cache-shapes",
|
|
"cgal-original-edges",
|
|
"cgal-smooth-angle-degrees",
|
|
"circle-segments",
|
|
"compute-curvature",
|
|
"context-identifiers",
|
|
"context-ids",
|
|
"context-priorities",
|
|
"context-types",
|
|
"convert-back-units",
|
|
"debug",
|
|
"defer-processing-first-element",
|
|
"dimensionality",
|
|
"digits",
|
|
"disable-boolean-result",
|
|
"disable-opening-subtractions",
|
|
"edge-arrows",
|
|
"ecef",
|
|
"elevation-ref",
|
|
"elevation-ref-guid",
|
|
"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",
|
|
"max-voids-per-element",
|
|
"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",
|
|
"print-space-areas",
|
|
"print-space-names",
|
|
"precision-factor",
|
|
"precision",
|
|
"profile-threshold",
|
|
"reorient-shells",
|
|
"site-local-placement",
|
|
"scale",
|
|
"section-height",
|
|
"section-height-from-storeys",
|
|
"section-ref",
|
|
"separate-z-up-node",
|
|
"space-name-transform",
|
|
"storey-height-line-length",
|
|
"surface-colour",
|
|
"svg-emit-flush-edges",
|
|
"svg-mirror-x",
|
|
"svg-mirror-y",
|
|
"svg-no-css",
|
|
"svg-poly",
|
|
"svg-prefilter",
|
|
"svg-project",
|
|
"svg-render-crease-edges",
|
|
"svg-render-sharp-edges",
|
|
"svg-ridge-angle-min-degrees",
|
|
"svg-segment-projection",
|
|
"svg-subtract-before",
|
|
"svg-unify-inputs",
|
|
"svg-use-edge-classification",
|
|
"svg-valley-angle-min-degrees",
|
|
"svg-without-storeys",
|
|
"svg-write-poly",
|
|
"svg-xmlns",
|
|
"triangulation-type",
|
|
"unify-shapes",
|
|
"use-material-names",
|
|
"use-element-guids",
|
|
"use-element-names",
|
|
"use-element-step-ids",
|
|
"use-element-types",
|
|
"use-python-opencascade",
|
|
"use-world-coords",
|
|
"validate",
|
|
"weld-vertices",
|
|
"y-up",
|
|
"wkt-use-section",
|
|
"center",
|
|
"draw-storey-heights",
|
|
"door-arcs",
|
|
]
|
|
|
|
# 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) -> SETTING:
|
|
return k.lower().replace("_", "-")
|
|
|
|
@staticmethod
|
|
def rname(k: SETTING) -> str:
|
|
return k.upper().replace("-", "_")
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
def setting_names(self) -> tuple[str, ...]:
|
|
setting_names = super().setting_names()
|
|
if isinstance(self, settings):
|
|
setting_names += ("use-python-opencascade",)
|
|
return setting_names
|
|
|
|
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 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
|
|
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)
|
|
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, 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]],
|
|
**kwargs,
|
|
) -> list[entity_instance]:
|
|
def unwrap(value):
|
|
if isinstance(value, entity_instance):
|
|
return value
|
|
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"])
|
|
return ifcopenshell_wrapper.tree.select(*args)
|
|
|
|
def select_box(self, value, **kwargs) -> list[entity_instance]:
|
|
def unwrap(value):
|
|
if isinstance(value, entity_instance):
|
|
return value
|
|
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 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, set_a, 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, set_a, 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, set_a, 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,
|
|
repr,
|
|
geometry_library,
|
|
*ifcopenshell.optional_logger_args(logger),
|
|
)
|
|
if repr
|
|
else ifcopenshell_wrapper.create_shape(
|
|
settings,
|
|
inst,
|
|
geometry_library,
|
|
*ifcopenshell.optional_logger_args(logger),
|
|
)
|
|
),
|
|
)
|
|
|
|
|
|
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)
|
|
|
|
|
|
@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,
|
|
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,
|
|
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,
|
|
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,
|
|
geometry_library: GEOMETRY_LIBRARY = "opencascade",
|
|
logger=None,
|
|
) -> Generator[Union[IteratorOutput, tuple[int, IteratorOutput]], None, None]:
|
|
"""Get a geometry iterator for the provided file."""
|
|
it = iterator(settings, file_or_filename, num_threads, include, exclude, geometry_library)
|
|
yield from consume_iterator(it, with_progress=with_progress)
|
|
|
|
|
|
def make_shape_function(fn):
|
|
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 fn(schema, string_or_shape, *args)
|
|
|
|
else:
|
|
|
|
def _(schema, string, *args):
|
|
return fn(schema, string, *args)
|
|
|
|
return _
|
|
|
|
|
|
# @todo, plug-ins now
|
|
# serialise = make_shape_function(ifcopenshell_wrapper.serialise)
|
|
# tesselate = make_shape_function(ifcopenshell_wrapper.tesselate)
|
|
|
|
|
|
class _serializer_factory:
|
|
_stream_serializers = {"obj", "svg", "ttl"}
|
|
|
|
def __init__(self, name: str, extension: str):
|
|
self.name = name
|
|
self.extension = extension
|
|
self.__name__ = name
|
|
|
|
def __call__(self, out_filename: Union[str, PathLike[str]], *args: Any) -> ifcopenshell_wrapper.GeometrySerializer:
|
|
if self.name == "obj" and len(args) == 2:
|
|
output_filename = args[0]
|
|
output_temp_filename = out_filename
|
|
settings = args[1]
|
|
elif len(args) == 1:
|
|
output_filename = out_filename
|
|
output_temp_filename = out_filename
|
|
settings = args[0]
|
|
else:
|
|
obj_signature = " or (out_filename, mtl_filename, settings)"
|
|
raise TypeError(
|
|
f"serializers.{self.name}() expects (out_filename, settings)"
|
|
+ (obj_signature if self.name == "obj" else "")
|
|
)
|
|
|
|
if self._is_buffer(output_filename) or self._is_buffer(output_temp_filename):
|
|
if self.name not in self._stream_serializers:
|
|
raise TypeError(f"serializers.{self.name}() requires a filesystem path")
|
|
output_filename = self._buffer(output_filename)
|
|
output_temp_filename = self._buffer(output_temp_filename)
|
|
else:
|
|
output_filename = self._path(output_filename)
|
|
output_temp_filename = self._path(output_temp_filename)
|
|
|
|
return ifcopenshell_wrapper.create_geometry_serializer(
|
|
self.extension, output_filename, output_temp_filename, settings
|
|
)
|
|
|
|
def _is_buffer(self, value: Any) -> bool:
|
|
return isinstance(value, ifcopenshell_wrapper.buffer)
|
|
|
|
def _buffer(self, value: Union[str, PathLike[str], ifcopenshell_wrapper.buffer]) -> ifcopenshell_wrapper.buffer:
|
|
if self._is_buffer(value):
|
|
return value
|
|
return ifcopenshell_wrapper.buffer(self._path(value))
|
|
|
|
def _path(self, value: Union[str, PathLike[str]]) -> str:
|
|
try:
|
|
path = fspath(value)
|
|
except TypeError:
|
|
raise TypeError(f"serializers.{self.name}() requires a filesystem path") from None
|
|
if not isinstance(path, str):
|
|
raise TypeError(f"serializers.{self.name}() requires a text filesystem path")
|
|
return path
|
|
|
|
|
|
class _serializers_meta(type):
|
|
_extensions = {
|
|
"obj": "obj",
|
|
"svg": "svg",
|
|
"ttl": "ttl",
|
|
"gltf": "glb",
|
|
"glb": "glb",
|
|
"collada": "dae",
|
|
"dae": "dae",
|
|
"stp": "stp",
|
|
"step": "stp",
|
|
"igs": "igs",
|
|
"iges": "igs",
|
|
"usd": "usd",
|
|
"usda": "usd",
|
|
"usdc": "usd",
|
|
}
|
|
|
|
def __getattr__(cls, name: str) -> _serializer_factory:
|
|
extension = cls._extensions.get(name)
|
|
if extension is None:
|
|
raise AttributeError(f"type object 'serializers' has no attribute '{name}'")
|
|
factory = _serializer_factory(name, extension)
|
|
setattr(cls, name, factory)
|
|
return factory
|
|
|
|
def __dir__(cls) -> list[str]:
|
|
return sorted(set(super().__dir__()) | set(cls._extensions))
|
|
|
|
|
|
class serializers(metaclass=_serializers_meta):
|
|
buffer = ifcopenshell_wrapper.buffer
|
|
|
|
@classmethod
|
|
def guess_from_extension(cls, filepath: str):
|
|
ext = filepath.rsplit(".", 1)[-1].lower()
|
|
mapping = {
|
|
"glb": "gltf",
|
|
"obj": "obj",
|
|
"svg": "svg",
|
|
"ttl": "ttl",
|
|
"dae": "collada",
|
|
"stp": "stp",
|
|
"step": "step",
|
|
"igs": "igs",
|
|
"iges": "iges",
|
|
"usd": "usd",
|
|
"usda": "usda",
|
|
"usdc": "usdc",
|
|
}
|
|
serializer_name = mapping.get(ext)
|
|
if not serializer_name:
|
|
raise ValueError(f"No serializer available for .{ext} file")
|
|
return getattr(cls, serializer_name)
|