From bfea57c61719d1a40d1d4e878feb6eaf31e793be Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Fri, 8 May 2026 10:58:09 +0200 Subject: [PATCH] Wire up serializer plug-ins in python --- .../ifcopenshell/geom/main.py | 134 ++++++++++-------- .../ifcopenshell/ifcopenshell_wrapper.pyi | 7 + src/ifcwrap/IfcGeomWrapper.i | 39 +++-- 3 files changed, 113 insertions(+), 67 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index a2c9fb9c9f..05701d9bc7 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -20,6 +20,7 @@ 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 from .. import ifcopenshell_wrapper, open @@ -630,69 +631,84 @@ def make_shape_function(fn): # 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 _serializer_factory: + 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) == 3: + output_filename = args[0] + output_temp_filename = out_filename + geometry_settings, serializer_settings = args[1], args[2] + elif len(args) == 2: + output_filename = out_filename + output_temp_filename = out_filename + geometry_settings, serializer_settings = args + else: + obj_signature = " or (out_filename, mtl_filename, geometry_settings, serializer_settings)" + raise TypeError( + f"serializers.{self.name}() expects (out_filename, geometry_settings, serializer_settings)" + + (obj_signature if self.name == "obj" 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, geometry_settings, serializer_settings + ) + + 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: - # 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) +class _serializers_meta(type): + _extensions = { + "obj": "obj", + "svg": "svg", + "ttl": "ttl", + "gltf": "glb", + "glb": "glb", + "hdf": "h5", + "hdf5": "h5", + "h5": "h5", + "collada": "dae", + "dae": "dae", + "stp": "stp", + "step": "stp", + "igs": "igs", + "iges": "igs", + "usd": "usd", + "usda": "usd", + "usdc": "usd", + } - @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) + 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 - # 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 + def __dir__(cls) -> list[str]: + return sorted(set(super().__dir__()) | set(cls._extensions)) + + +class serializers(metaclass=_serializers_meta): 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] + ext = filepath.rsplit(".", 1)[-1].lower() mapping = { "glb": "gltf", "hdf": "hdf5", @@ -701,8 +717,14 @@ class serializers: "obj": "obj", "svg": "svg", "ttl": "ttl", - "xml": "xml", "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: diff --git a/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi b/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi index 1b2c66cc06..bf6e830500 100644 --- a/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi +++ b/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi @@ -1710,6 +1710,13 @@ def construct_iterator_with_include_exclude_id(geometry_library, settings, file, def convert_loop_to_function_item(loop): ... def create_box(*args): ... def create_epeck(*args): ... +def create_geometry_serializer( + extension: str, + output_filename: str, + output_temp_filename: str, + geometry_settings: Settings, + serializer_settings: SerializerSettings, +) -> GeometrySerializer: ... def create_shape(*args): ... def flatten(deep): ... def get_feature(x): ... diff --git a/src/ifcwrap/IfcGeomWrapper.i b/src/ifcwrap/IfcGeomWrapper.i index 846634fcc6..d645fa61fa 100644 --- a/src/ifcwrap/IfcGeomWrapper.i +++ b/src/ifcwrap/IfcGeomWrapper.i @@ -634,17 +634,34 @@ struct ShapeRTTI : public boost::static_visitor $result = std::visit(ShapeRTTI(), (std::variant) $1); } -%newobject construct_iterator; -%newobject construct_iterator_with_include_exclude; -%newobject construct_iterator_with_include_exclude_globalid; -%newobject construct_iterator_with_include_exclude_id; - -// I couldn't get the vector typemap to be applied when %extending Iterator constructor. -// anyway it does not matter as SWIG generates C code without actual constructors -%inline %{ - IfcGeom::Iterator* construct_iterator(const std::string& geometry_library, ifcopenshell::geometry::Settings settings, ifcopenshell::file* file, int num_threads) { - return new IfcGeom::Iterator(ifcopenshell::geometry::kernels::construct(file, geometry_library, settings), settings, file, num_threads); - } +%newobject construct_iterator; +%newobject construct_iterator_with_include_exclude; +%newobject construct_iterator_with_include_exclude_globalid; +%newobject construct_iterator_with_include_exclude_id; +%newobject create_geometry_serializer; + +// I couldn't get the vector typemap to be applied when %extending Iterator constructor. +// anyway it does not matter as SWIG generates C code without actual constructors +%inline %{ + GeometrySerializer* create_geometry_serializer( + const std::string& extension, + const std::string& output_filename, + const std::string& output_temp_filename, + ifcopenshell::geometry::Settings& geometry_settings, + const ifcopenshell::geometry::SerializerSettings& serializer_settings + ) { + return new PythonPluginGeometrySerializer( + extension, + output_filename, + output_temp_filename, + geometry_settings, + serializer_settings + ); + } + + IfcGeom::Iterator* construct_iterator(const std::string& geometry_library, ifcopenshell::geometry::Settings settings, ifcopenshell::file* file, int num_threads) { + return new IfcGeom::Iterator(ifcopenshell::geometry::kernels::construct(file, geometry_library, settings), settings, file, num_threads); + } IfcGeom::Iterator* construct_iterator_with_include_exclude(const std::string& geometry_library, ifcopenshell::geometry::Settings settings, ifcopenshell::file* file, std::vector elems, bool include, int num_threads) { std::set elems_set(elems.begin(), elems.end());