mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 10:06:47 +00:00
7ae6bf4374
Apply the rename manifest, normalize serializer filenames to the classes they define, and update includes and CMake source lists. Generated with the assistance of an AI coding tool.
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 abstract_kernel.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)
|