From 69863ce4a99d59e89faec7a4b01b46ad18e61557 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sun, 26 Oct 2025 10:03:33 +0100 Subject: [PATCH 01/18] Try and fix stub test --- .../ifcopenshell/ifcopenshell_wrapper.pyi | 106 +++++++++++------- src/ifcwrap/IfcParseWrapper.i | 4 + src/ifcwrap/IfcPython.i | 4 + 3 files changed, 75 insertions(+), 39 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi b/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi index 0f71a9ab24..10c08141d5 100644 --- a/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi +++ b/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi @@ -71,6 +71,13 @@ FILTERED_CARTESIAN_QUOTIENT: Any EXACT_PREDICATES: Any EXACT_CONSTRUCTIONS: Any +FT_AUTODETECT: Any +FT_IFCSPF: Any +FT_IFCXML: Any +FT_IFCZIP: Any +FT_ROCKSDB: Any +FT_UNKNOWN: Any + CURVES: Any SURFACES_AND_SOLIDS: Any CURVES_SURFACES_AND_SOLIDS: Any @@ -96,12 +103,9 @@ class IfcSpfHeader: """ def file(self, *args): ... - @property - def file_description(self) -> ifcopenshell.entity_instance: ... - @property - def file_name(self) -> ifcopenshell.entity_instance: ... - @property - def file_schema(self) -> ifcopenshell.entity_instance: ... + def file_description_py(self): ... + def file_name_py(self): ... + def file_schema_py(self): ... def read(self): ... def tryRead(self): ... def write(self, out): ... @@ -145,6 +149,7 @@ class ConversionResult: def Style(self): ... def StylePtr(self): ... def append(self, trsf): ... + def apply_transform(self, unit_scale): ... def hasStyle(self): ... def prepend(self, trsf): ... def setStyle(self, newStyle): ... @@ -157,6 +162,7 @@ class ConversionResultShape: def axis(self): ... def bounding_box(self, *args): ... def box(self): ... + def concat(self, arg2): ... def convex_decomposition(self): ... def convex_tag(self, b): ... def edges(self): ... @@ -178,9 +184,11 @@ class ConversionResultShape: def solid(self): ... def solid_mt(self): ... def subtract(self, arg2): ... + def surface_area_along_direction(self, tol, arg3, along_x, along_y, along_z): ... def surface_genus(self): ... def vertices(self): ... def volume(self): ... + def wrap_in_compound(self): ... class DoubleArray3: def back(self): ... @@ -284,11 +292,6 @@ class HdfSerializer(GeometrySerializer): def write(self, *args): ... def writeHeader(self): ... -class HeaderEntity: - def getArgument(self, index): ... - def getArgumentCount(self): ... - def toString(self, upper): ... - class IfcBaseEntity(entity_instance): def declaration(self): ... def get(self, name): ... @@ -304,9 +307,17 @@ class IfcEntityInstanceData: ... class IfcLateBoundEntity(IfcBaseEntity): def declaration(self): ... -class InstanceReference: - file_offset: Any - v: Any +class InstanceStreamer: + def bypassTypes(self, type_names): ... + def bypassed_instances(self): ... + coerce_attribute_count: bool + def hasSemicolon(self): ... + def inverses(self, *args): ... + def pushPage(self, page): ... + def readInstancePy(self, type_as_declaration_instance): ... + def references(self, *args): ... + def semicolonCount(self): ... + def status(self): ... class Iterator: initialization_outcome_: Any @@ -377,6 +388,17 @@ class Representation: def settings(self): ... +class RocksDBPrefixIterator: + def key(self): ... + def next(self): ... + def valid(self): ... + def value(self): ... +class RocksDbSerializer: + def finalize(self): ... + def ready(self): ... + def setFile(self, arg2): ... + def writeHeader(self): ... + class Serialization(Representation): @property def brep_data(self): ... @@ -829,6 +851,7 @@ class entity_instance: ... def get_attribute_names(self): ... + def get_attribute_value(self, index): ... def get_inverse(self, a): ... def get_inverse_attribute_names(self): ... def id(self) -> int: ... @@ -851,6 +874,7 @@ class entity_instance: def setArgumentAsLogical(self, i, v): ... def setArgumentAsNull(self, i): ... def setArgumentAsString(self, i, a): ... + def set_attribute_value(self, *args): ... def toString(self, arg2, upper): ... def to_string(self, valid_spf): ... def unset_attribute_value(self, i): ... @@ -888,14 +912,10 @@ class face: def print_impl(self, o, indent): ... class file: - INSTANCE_ID: Any - INSTANCE_TYPE: Any - ATTRIBUTE_INDEX: Any - guid_map_: Any - stream: Any def FreshId(self): ... def add(self, entity: entity_instance, id: int) -> entity_instance: ... def addEntities(self, entities): ... + def add_type_ref(self, new_entity): ... def batch(self) -> None: """Enable batch mode. @@ -919,10 +939,15 @@ class file: ... def build_inverses(self): ... + def build_inverses_(self, arg2): ... def by_guid(self, guid: str) -> entity_instance: ... def by_id(self, id: int) -> entity_instance: ... def by_type(self, *args): ... def by_type_excl_subtypes(self, *args): ... + def bypass_type(self, type_name): ... + calculate_unit_factors: bool + check_existance_before_adding: bool + def create(self, decl): ... @staticmethod def createTimestamp(): ... def entity_names(self) -> tuple[int, ...]: @@ -961,24 +986,27 @@ class file: def get_total_inverses(self, e: entity_instance) -> int: ... def good(self): ... - @staticmethod - def guid_map(*args): ... - def header(self) -> IfcSpfHeader: + def header(self, *args) -> IfcSpfHeader: """Internal IfcSpfHeader instance, always prefer ``ifcopenshell.file.header`` instead.""" def ifcroot_type(self) -> entity: ... - def internal_guid_map(self): ... - def load(self, entity_instance_name, entity, arg4, attribute_index): ... + def initialize(self, *args): ... + instantiate_typed_instances: bool + def key_value_store_iter(self, prefix): ... + def key_value_store_query(self, key): ... + def process_deletion_inverse(self, inst): ... def recalculate_id_counter(self): ... def remove(self, entity: entity_instance) -> None: ... + def remove_type_ref(self, new_entity): ... + def reset_identity_cache(self): ... @property def schema(self): ... + def storage_mode(self): ... def to_string(self): ... @staticmethod def traverse(instance: entity_instance, max_level: int) -> tuple[entity_instance, ...]: ... @staticmethod def traverse_breadth_first(instance: entity_instance, max_level: int) -> tuple[entity_instance, ...]: ... - def try_read_semicolon(self): ... def types(self) -> tuple[str, ...]: """Return a tuple of classes present in the file. @@ -986,16 +1014,15 @@ class file: """ ... - def types_begin(self): ... - def types_end(self): ... def write(self, fn): ... class file_open_status: - SUCCESS: Any - READ_ERROR: Any - NO_HEADER: Any - UNSUPPORTED_SCHEMA: Any - INVALID_SYNTAX: Any + SUCCESS: int + READ_ERROR: int + NO_HEADER: int + UNSUPPORTED_SCHEMA: int + INVALID_SYNTAX: int + UNKNOWN: int def value(self): ... class fn_evaluator: @@ -1037,9 +1064,6 @@ class geometry_conversion_result: products_2: Any representation: Any -class geometry_exception: - def what(self): ... - class gradient_function(function_item): def calc_hash(self): ... def clone_(self): ... @@ -1138,6 +1162,8 @@ class matrix4(item): def is_identity(self): ... def kind(self): ... def translation_part(self): ... + def post_multiply_scale(self, s): ... + def pre_multiply_scale(self, s): ... class named_type(parameter_type): def _is(self, *args): ... @@ -1516,13 +1542,11 @@ class sweep(geom_item): class sweep_along_curve(sweep): curve: Any surface: Any + direction: Any def calc_hash(self): ... def clone_(self): ... def kind(self): ... -class too_many_faces_exception(geometry_exception): ... -class topology_error: ... - class torus(surface): radius1: Any radius2: Any @@ -1568,6 +1592,8 @@ class type_declaration(declaration): def as_type_declaration(self) -> type_declaration: ... def declared_type(self): ... +class uninitialized_tag: ... + def arrange_polygons(polygons): ... def clear_schemas(): ... def construct_iterator_with_include_exclude(geometry_library, settings, file, elems, include, num_threads): ... @@ -1581,6 +1607,7 @@ def flatten(deep): ... def get_feature(x): ... def get_info_cpp(v, include_identifier): ... def get_log(): ... +def guess_file_type(fn): ... def helmert_curve_point(A0, A1, A2, s): ... def kind_to_string(k): ... def less(arg1, arg2): ... @@ -1588,7 +1615,7 @@ def line_segments_to_polygons(s, eps, segments): ... def map_shape(settings, instance): ... def nary_union(sequence): ... def new_IfcBaseClass(schema_identifier: str, name: str) -> entity_instance: ... -def open(fn): ... +def open(fn, readonly): ... def parse_ifcxml(filename): ... def polygons_to_svg(*args): ... def read(data): ... @@ -1599,6 +1626,7 @@ def serialise(schema_name, shape_str, advanced): ... def set_feature(x, v): ... def set_log_format_json(): ... def set_log_format_text(): ... +def stream_from_string(data): ... def svg_to_line_segments(data, class_name): ... def svg_to_polygons(data, class_name): ... def taxonomy_item_repr(i): ... diff --git a/src/ifcwrap/IfcParseWrapper.i b/src/ifcwrap/IfcParseWrapper.i index 858705ebf0..1bc43b422c 100644 --- a/src/ifcwrap/IfcParseWrapper.i +++ b/src/ifcwrap/IfcParseWrapper.i @@ -33,6 +33,10 @@ private: %ignore IfcParse::IfcFile::types_end; %ignore IfcParse::IfcFile::internal_guid_map; %ignore IfcParse::IfcFile::storage_; +%ignore IfcParse::IfcFile::byguid_; +%ignore IfcParse::IfcFile::byid_; +%ignore IfcParse::IfcFile::byref_excl_; +%ignore IfcParse::IfcFile::types_to_bypass_loading_; %ignore IfcParse::InstanceStreamer::InstanceStreamer(const IfcParse::schema_definition* schema, IfcParse::IfcSpfLexer* lexer); diff --git a/src/ifcwrap/IfcPython.i b/src/ifcwrap/IfcPython.i index 7a3fd48139..8595e264e2 100644 --- a/src/ifcwrap/IfcPython.i +++ b/src/ifcwrap/IfcPython.i @@ -93,6 +93,10 @@ %ignore curve_to_face_upgrade_impl; %ignore loop_to_function_item_upgrade_impl; +%ignore IfcGeom::geometry_exception; +%ignore IfcGeom::too_many_faces_exception; +%ignore ifcopenshell::geometry::taxonomy::topology_error; + // settings, can this done more generally? // GeometrySerializer.h %ignore UseElementNames; From 6f3065f6ce46c3074f2cc13f0a11917a03aa54e0 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sun, 26 Oct 2025 10:15:52 +0100 Subject: [PATCH 02/18] ci.yml USE_MMAP=On --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0185aa8077..ac5dfe2516 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,6 +114,7 @@ jobs: -DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \ -DPYTHON_LIBRARY:FILEPATH=${{ env.pythonLocation }}/lib/libpython3.11.so \ -DCOLLADA_SUPPORT=Off \ + -DUSE_MMAP=On \ "-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \ -DGLTF_SUPPORT=On \ -DWITH_ROCKSDB=On \ From 459aad54aa781a1a3d0feac6c6b5824881f069f0 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sun, 26 Oct 2025 10:17:49 +0100 Subject: [PATCH 03/18] Black --- src/ifcopenshell-python/ifcopenshell/file.py | 2 +- src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/file.py b/src/ifcopenshell-python/ifcopenshell/file.py index c7350a13da..fd95004aad 100644 --- a/src/ifcopenshell-python/ifcopenshell/file.py +++ b/src/ifcopenshell-python/ifcopenshell/file.py @@ -251,7 +251,7 @@ INVALID_SYNTAX = ifcopenshell_wrapper.file_open_status.INVALID_SYNTAX try: UNKNOWN = ifcopenshell_wrapper.file_open_status.UNKNOWN except: - UNKNOWN = 5 # Workaround + UNKNOWN = 5 # Workaround import struct diff --git a/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi b/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi index 10c08141d5..47901a182e 100644 --- a/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi +++ b/src/ifcopenshell-python/ifcopenshell/ifcopenshell_wrapper.pyi @@ -393,6 +393,7 @@ class RocksDBPrefixIterator: def next(self): ... def valid(self): ... def value(self): ... + class RocksDbSerializer: def finalize(self): ... def ready(self): ... From 89defa9b6b0a0606a1bcb02bab2223062fa84512 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sun, 26 Oct 2025 10:19:53 +0100 Subject: [PATCH 04/18] build-all.py ruff: noqa: UP035 --- nix/build-all.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nix/build-all.py b/nix/build-all.py index 0351300717..b7c1be556a 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -18,6 +18,8 @@ # # ############################################################################### +# ruff: noqa: UP035 + """ Example usage: # Build all targets by default. From 0bb03750dc42458337572e8dd19f871b4c9cfbfa Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 26 Oct 2025 20:26:19 +1100 Subject: [PATCH 05/18] Fix #4914. Write docs for syntax and schema validation and schema querying --- src/ifcopenshell-python/docs/bsdd.rst | 1 + .../docs/ifcopenshell-python.rst | 3 +- .../ifcopenshell-python/developer_guide.rst | 15 ---- .../ifcopenshell-python/schema_querying.rst | 73 +++++++++++++++++ .../docs/ifcopenshell-python/validation.rst | 78 +++++++++++++++++++ 5 files changed, 154 insertions(+), 16 deletions(-) delete mode 100644 src/ifcopenshell-python/docs/ifcopenshell-python/developer_guide.rst create mode 100644 src/ifcopenshell-python/docs/ifcopenshell-python/schema_querying.rst create mode 100644 src/ifcopenshell-python/docs/ifcopenshell-python/validation.rst diff --git a/src/ifcopenshell-python/docs/bsdd.rst b/src/ifcopenshell-python/docs/bsdd.rst index 568c685983..2f20e5e95d 100644 --- a/src/ifcopenshell-python/docs/bsdd.rst +++ b/src/ifcopenshell-python/docs/bsdd.rst @@ -43,6 +43,7 @@ Examples Learning how to use the bSDD is best done by reading the official Swagger API docs. .. code-block:: python + from bsdd import Client,apply_ifc_classification_properties from pprint import pprint diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python.rst b/src/ifcopenshell-python/docs/ifcopenshell-python.rst index 9eebe129d4..bbe503c64b 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python.rst @@ -17,4 +17,5 @@ capabilities of the C++ core are available in Python. ifcopenshell-python/geometry_creation ifcopenshell-python/geometry_tree ifcopenshell-python/selector_syntax - ifcopenshell-python/developer_guide + ifcopenshell-python/schema_querying + ifcopenshell-python/validation diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/developer_guide.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/developer_guide.rst deleted file mode 100644 index d664cbe111..0000000000 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/developer_guide.rst +++ /dev/null @@ -1,15 +0,0 @@ -Developer Guide -=============== - -The core module implements low-level functionality to read and write IFC data. This includes: - -- Reading IFC data from different serialisations into Python objects -- Accessing direct and indirect attributes of IFC entities -- Creating IFC entities -- Generating GlobalIds -- Removing IFC entities and all references -- Modifying IFC direct attributes -- Checking IFC class inheritance -- Validating IFC data - -TODO diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/schema_querying.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/schema_querying.rst new file mode 100644 index 0000000000..1cd6fbd1a8 --- /dev/null +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/schema_querying.rst @@ -0,0 +1,73 @@ +Schema querying +=============== + +Schema declarations +------------------- + +IfcOpenShell can query the IFC schema itself without instantiating or loading an IFC dataset. + +.. code-block:: python + + import ifcopenshell + ifc4 = ifcopenshell.schema_by_name("IFC4") + +A schema definition is known as a declaration. You may loop through all declarations or retrieve a declaration by name. All declarations have a name. + +.. code-block:: python + + for declaration in ifc4.declarations(): + print(declaration.name()) # 'IfcAbsorbedDoseMeasure', 'IfcAccelerationMeasure', 'IfcActionRequest', ... + + ifcwall = ifc4.declaration_by_name("IfcWall") + +You can check if an entity is abstract, and retrive both the supertype and subtypes of an entity: + +.. code-block:: python + + print(ifcwall.is_abstract()) # False + print(ifcwall.supertype()) # + print(ifcwall.subtypes()) # (, ) + +You can retrieve only the direct attributes of an entity, or all the direct attributes including inherited attributes, or inverse attributes: + +.. code-block:: python + + print(ifcwall.attributes()) + print(ifcwall.all_attributes()) + print(ifcwall.all_inverse_attributes()) + +buildingSMART property set templates +------------------------------------ + +For each IFC schema version, buildingSMART publishes built in property and quantity set templates for standardised properties. These define property names, property sets, data types, and which IFC class they are applicable to. You can query these templates. + +.. code-block:: python + + import ifcopenshell.util.pset + templates = ifcopenshell.util.pset.PsetQto("IFC4") + +To get just the names of applicable templates for an entity: + +.. code-block:: python + + # ['Pset_EnvironmentalImpactIndicators', 'Pset_EnvironmentalImpactValues', 'Pset_WallCommon', 'Qto_WallBaseQuantities', ...] + print(templates.get_applicable_names("IfcWall")) + +They may also be retrieved as an ``IfcPropertySetTemplate`` entity: + +.. code-block:: python + + print(templates.get_applicable("IfcWall")) + +A single template may be retrieved by name: + +.. code-block:: python + + templates.get_by_name('Pset_WallCommon') + +You may add your own IFC files containing pset template definitions: + +.. code-block:: python + + my_pset_library = ifcopenshell.open('/path/to/library.ifc') + templates.templates.append(my_pset_library) diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/validation.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/validation.rst new file mode 100644 index 0000000000..3a0847d80a --- /dev/null +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/validation.rst @@ -0,0 +1,78 @@ +Validation +========== + +SPF syntax validation +--------------------- + +IfcOpenShell can validate whether or not an IFC-SPF file contains correct SPF syntax. + +.. code-block:: + + $ python -m ifcopenshell.simple_spf path/to/model.ifc + Valid + +Here are some examples of failures: + +.. code-block:: + + $ python -m ifcopenshell.simple_spf fixtures/fail_double_comma.ifc + On line 8 column 21: + Unexpected comma (',') + Expecting one of DBLQUOTE DOT HASH INT LPAR NONE QUOTE REAL STAR UPPER + 00008 | #1=IFCPERSON($,$,'',,$,$,$,$); + ^ + + $ python -m ifcopenshell.simple_spf fixtures/fail_double_semi.ifc + On line 27 column 66: + Unexpected semicolon (';') + Expecting one of ENDSEC HASH + 00027 | #20=IFCPROJECT('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);; + ^ + + $ python -m ifcopenshell.simple_spf fixtures/fail_duplicate_id.ifc + On line 27: + Duplicate instance name #19 + 00027 | #19=IFCPROJECT('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19); + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + $ python -m ifcopenshell.simple_spf fixtures/fail_no_header.ifc + On line 2 column 1: + Unexpected hex ('F') + Expecting HEADER + 00002 | FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); + ^ + +The optional ``--json`` argument may be used to instead get results in JSON. + +.. code-block:: + + $ python -m ifcopenshell.simple_spf test.ifc + {"type": "unexpected_token", "lineno": 8, "column": 48, "found_type": "semicolon", "found_value": ";", "expected": ["ENDSEC"], "line": "#1= IFCPERSON($,'Nicht definiert',$,$,$,$,$,$);;", "message": "On line 8 column 48:\nUnexpected semicolon (';')\nExpecting ENDSEC\n00008 | #1= IFCPERSON($,'Nicht definiert',$,$,$,$,$,$);;\n ^"} + +IFC schema validation +--------------------- + +IfcOpenShell can validate models against the IFC schema itself. It checks against attributes, entity names, data types, cardinality, and where rules. + +.. code-block:: console + + $ python -m ifcopenshell.validate -h + + usage: validate.py [-h] [--rules] [--json] [--fields] [--spf] files [files ...] + + positional arguments: + files The IFC file to validate. + + options: + -h, --help show this help message and exit + --rules Run express rules. + --json Output in JSON format. + --fields Output more detailed information about failed entities (only with --json). + --spf Output entities in SPF format (only with --json). + + +For example: + +.. code-block:: bash + + python -m ifcopenshell.validate /path/to/model.ifc --rules From 3a0622e9df64b12fc7b32b6922f6584bca9c699d Mon Sep 17 00:00:00 2001 From: daniel-augusto Date: Sun, 26 Oct 2025 06:29:30 -0300 Subject: [PATCH 06/18] Update introduction with hyperlinks for tools (#7290) --- src/ifcopenshell-python/docs/introduction.rst | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/ifcopenshell-python/docs/introduction.rst b/src/ifcopenshell-python/docs/introduction.rst index 76032d334d..58430b6da8 100644 --- a/src/ifcopenshell-python/docs/introduction.rst +++ b/src/ifcopenshell-python/docs/introduction.rst @@ -54,27 +54,27 @@ IfcOpenShell is a modular ecosystem of tools that work together, where each tool .. csv-table:: :header: "Name", "Description" - "**IfcOpenShell**", "The core library for C++ developers. The library includes the ability to parse schemas, tessellate and process implicit geometry." - "**IfcOpenShell-Python**", "Python bindings to the core IfcOpenShell C++ system, as well as high level analysis and authoring functions." - "**IfcConvert**", "A command-line application for converting IFC geometry into file formats such as OBJ, DAE, GLB, STP, IGS, XML, SVG, H5, and IFC itself." - "**Bonsai**", "A graphical add-on for Blender that lets you analyse, author, and modify IFC with Blender. Graphically create BIM models from scratch!" - "**BCF**", "BIM Collaboration Format (BCF) is a standard to manage and exchange coordination topics between disciplines collaborating on a project by changing XML files or querying an API." - "**BIMServer-Plugin**", "A plugin to the open source BIMServer CDE to allow you to use IfcOpenShell to parse, view, and audit models." - "**BIMTester**", "A utility that allows you to write Gherkin-based tests for models." - "**bSDD**", "A Python library to query the buildingSMART Data Dictionary API to search for standardised classifications and properties." - "**Ifc2CA**", "Converts IFC models to FEM structural analytical models to be used in Code_Aster." - "**Ifc4D**", "A series of utilities for converting to and from various 4D software like MS Project, PowerProject, and Oracle P6." - "**Ifc5D**", "A collection of utilities of manipulating cost-related data to and from formats, reports, and optimisation engines." - "**IfcCityJSON**", "A converter for CityJSON files and IFC. It currently only supports one-way conversion from CityJSON to IFC." - "**IfcClash**", "A CLI utility and library that lets you perform clash detection on one or more IFC models. Clashes are defined in terms of clash sets with filters using the IFC query syntax." - "**IfcCSV**", "View and edit IFC data using spreadsheets or tabular datasets, such as CSV, ODS, XLSX, Pandas DataFrames, and regular Python lists." - "**IfcDiff**", "A CLI utility and library that lets you compare the changes between two IFC models." - "**IfcFM**", "A highly standards-compliant tool (e.g. COBie 2.4, COBie 3.0, AOH-BSEM) to convert FM data in IFC databases to spreadsheets and other machine readable formats, such as ODS, XLSX, CSV, Pandas, XML, and JSON." - "**IfcMax**", "A 3ds Max importer plugin able to import the IFC file format." - "**IfcPatch**", "A CLI utility and library that lets you run and distribute predetermined modifications on an IFC file, known as a patch recipe. Useful in deploying a data pipeline or batch-fixing external models." - "**IfcSverchok**", "A node based visual programming add-on for Blender to interact with IFC and Sverchok." - "**IfcTester**", "Author and read Information Delivery Specification (IDS) files. You can validate IFC models against IDS and generate reports in multiple formats. It works from the command line, as a web app, or as a library." - "**VoxelisationToolkit**", "Converts .ifc geometry into voxels, and lets you perform voxel based geometric analysis." + "`IfcOpenShell `_", "The core library for C++ developers. The library includes the ability to parse schemas, tessellate and process implicit geometry." + "`IfcOpenShell-Python `_", "Python bindings to the core IfcOpenShell C++ system, as well as high level analysis and authoring functions." + "`IfcConvert `_", "A command-line application for converting IFC geometry into file formats such as OBJ, DAE, GLB, STP, IGS, XML, SVG, H5, and IFC itself." + "`Bonsai `_", "A graphical add-on for Blender that lets you analyse, author, and modify IFC with Blender. Graphically create BIM models from scratch!" + "`BCF `_", "BIM Collaboration Format (BCF) is a standard to manage and exchange coordination topics between disciplines collaborating on a project by changing XML files or querying an API." + "`BIMServer-Plugin `_", "A plugin to the open source BIMServer CDE to allow you to use IfcOpenShell to parse, view, and audit models." + "`BIMTester `_", "A utility that allows you to write Gherkin-based tests for models." + "`bSDD `_", "A Python library to query the buildingSMART Data Dictionary API to search for standardised classifications and properties." + "`Ifc2CA `_", "Converts IFC models to FEM structural analytical models to be used in Code_Aster." + "`Ifc4D `_", "A series of utilities for converting to and from various 4D software like MS Project, PowerProject, and Oracle P6." + "`Ifc5D `_", "A collection of utilities of manipulating cost-related data to and from formats, reports, and optimisation engines." + "`IfcCityJSON `_", "A converter for CityJSON files and IFC. It currently only supports one-way conversion from CityJSON to IFC." + "`IfcClash `_", "A CLI utility and library that lets you perform clash detection on one or more IFC models. Clashes are defined in terms of clash sets with filters using the IFC query syntax." + "`IfcCSV `_", "View and edit IFC data using spreadsheets or tabular datasets, such as CSV, ODS, XLSX, Pandas DataFrames, and regular Python lists." + "`IfcDiff `_", "A CLI utility and library that lets you compare the changes between two IFC models." + "`IfcFM `_", "A highly standards-compliant tool (e.g. COBie 2.4, COBie 3.0, AOH-BSEM) to convert FM data in IFC databases to spreadsheets and other machine readable formats, such as ODS, XLSX, CSV, Pandas, XML, and JSON." + "`IfcMax `_", "A 3ds Max importer plugin able to import the IFC file format." + "`IfcPatch `_", "A CLI utility and library that lets you run and distribute predetermined modifications on an IFC file, known as a patch recipe. Useful in deploying a data pipeline or batch-fixing external models." + "`IfcSverchok `_", "A node based visual programming add-on for Blender to interact with IFC and Sverchok." + "`IfcTester `_", "Author and read Information Delivery Specification (IDS) files. You can validate IFC models against IDS and generate reports in multiple formats. It works from the command line, as a web app, or as a library." + "`VoxelisationToolkit `_", "Converts .ifc geometry into voxels, and lets you perform voxel based geometric analysis." .. note:: From c40ecdd550fa08c347422cd422bcacae5915e6e1 Mon Sep 17 00:00:00 2001 From: Louis Casteleyn <73949503+lfcastel@users.noreply.github.com> Date: Sun, 26 Oct 2025 10:32:28 +0100 Subject: [PATCH 07/18] deleted deprecated entities for v1.4 (#6511) --- .../ifcopenshell/util/ifc4_to_brick.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/ifc4_to_brick.json b/src/ifcopenshell-python/ifcopenshell/util/ifc4_to_brick.json index 5c07e00bf7..b8fba566ed 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/ifc4_to_brick.json +++ b/src/ifcopenshell-python/ifcopenshell/util/ifc4_to_brick.json @@ -26,11 +26,5 @@ "IfcFlowMeter.GASMETER": "Gas_Meter", "IfcFlowMeter.WATERMETER": "Water_Meter", "IfcElectricMotor": "Motor", - "IfcSolarDevice.SOLARPANEL": "PV_Panel", - "IfcBuilding": "Building", - "IfcBuildingStorey": "Floor", - "IfcSpace": "Space", - "IfcSpatialZone": "Zone", - "IfcSpatialZone.THERMAL": "HVAC_Zone", - "IfcSpatialZone.LIGHTING": "Lighting_Zone" + "IfcSolarDevice.SOLARPANEL": "PV_Panel" } From 05ff47fcff632ff62c836dbd2595b9c410c7b327 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 26 Oct 2025 20:38:58 +1100 Subject: [PATCH 08/18] Remap deprecated entities in Brick 1.4 to new RealEstateCore URIs. --- src/ifcopenshell-python/ifcopenshell/util/brick.py | 2 ++ .../ifcopenshell/util/ifc4_to_brick.json | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/brick.py b/src/ifcopenshell-python/ifcopenshell/util/brick.py index 8c11dacdfe..04e0be5452 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/brick.py +++ b/src/ifcopenshell-python/ifcopenshell/util/brick.py @@ -53,6 +53,8 @@ def get_brick_type(element: ifcopenshell.entity_instance) -> Union[str, None]: if not result: result = ifc4_to_brick_map.get(ifc_type_class, None) if result: + if result.startswith("http"): + return result return f"https://brickschema.org/schema/Brick#{result}" # Generic fallback if element.is_a("IfcDistributionElement"): diff --git a/src/ifcopenshell-python/ifcopenshell/util/ifc4_to_brick.json b/src/ifcopenshell-python/ifcopenshell/util/ifc4_to_brick.json index b8fba566ed..d76d2c0c80 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/ifc4_to_brick.json +++ b/src/ifcopenshell-python/ifcopenshell/util/ifc4_to_brick.json @@ -26,5 +26,10 @@ "IfcFlowMeter.GASMETER": "Gas_Meter", "IfcFlowMeter.WATERMETER": "Water_Meter", "IfcElectricMotor": "Motor", - "IfcSolarDevice.SOLARPANEL": "PV_Panel" + "IfcSolarDevice.SOLARPANEL": "PV_Panel", + "IfcBuilding": "https://w3id.org/rec#Building", + "IfcBuildingStorey": "https://w3id.org/rec#Level", + "IfcSpace": "https://w3id.org/rec#Room", + "IfcSpatialZone": "https://w3id.org/rec#Zone", + "IfcSpatialZone.THERMAL": "https://w3id.org/rec#HVACZone" } From 90ccaf4aab1bf5f05a6b586e01b5cf06a7afc60a Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 26 Oct 2025 20:46:32 +1100 Subject: [PATCH 09/18] Let users manually create automatic references too. --- src/bonsai/bonsai/core/drawing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py index 2e2d07c96c..e9cb310a60 100644 --- a/src/bonsai/bonsai/core/drawing.py +++ b/src/bonsai/bonsai/core/drawing.py @@ -487,7 +487,8 @@ def sync_references( continue if (obj := ifc.get_object(element)) and ifc.is_moved(obj): drawing_tool.sync_object_placement(obj) - reference_element = drawing_tool.get_assigned_product(element) + if not (reference_element := drawing_tool.get_assigned_product(element)): + continue reference_obj = ifc.get_object(reference_element) if reference_element not in potential_reference_elements: # It was auto created, so it makes sense to auto delete From b503061dc0a20fa88c1d42098aea17b3c1c630ef Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 26 Oct 2025 20:58:15 +1100 Subject: [PATCH 10/18] Fix #3868. Automatic annotation references are now auto deleted if their reference is also gone. --- src/bonsai/bonsai/core/drawing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py index e9cb310a60..d769079acc 100644 --- a/src/bonsai/bonsai/core/drawing.py +++ b/src/bonsai/bonsai/core/drawing.py @@ -488,6 +488,9 @@ def sync_references( if (obj := ifc.get_object(element)) and ifc.is_moved(obj): drawing_tool.sync_object_placement(obj) if not (reference_element := drawing_tool.get_assigned_product(element)): + if obj := ifc.get_object(element): + drawing_tool.delete_object(obj) + ifc.run("root.remove_product", product=element) continue reference_obj = ifc.get_object(reference_element) if reference_element not in potential_reference_elements: From 1789406299ba799e8cf8b4a5feb6627dee8db501 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sun, 26 Oct 2025 12:04:22 +0100 Subject: [PATCH 11/18] Ruff --- src/ifcfm/ifcfm/__init__.py | 3 ++- src/opencdeserver/api/app/api/logging.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ifcfm/ifcfm/__init__.py b/src/ifcfm/ifcfm/__init__.py index ff36e5702c..14fa76146d 100644 --- a/src/ifcfm/ifcfm/__init__.py +++ b/src/ifcfm/ifcfm/__init__.py @@ -24,7 +24,8 @@ import importlib import ifcopenshell.util.selector from pathlib import Path from collections import defaultdict -from typing import Literal, Union, Any, Callable, TYPE_CHECKING +from typing import Literal, Union, Any, TYPE_CHECKING +from collections.abc import Callable try: from openpyxl import Workbook diff --git a/src/opencdeserver/api/app/api/logging.py b/src/opencdeserver/api/app/api/logging.py index 49131c0ce1..de368ccb9c 100644 --- a/src/opencdeserver/api/app/api/logging.py +++ b/src/opencdeserver/api/app/api/logging.py @@ -3,7 +3,7 @@ from starlette.background import BackgroundTask from starlette.responses import StreamingResponse from fastapi.routing import APIRoute from starlette.types import Message -from typing import Callable +from collections.abc import Callable import logging import httpx From eea585a3cd53fd025520c5bba54c02a1a3222edc Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 26 Oct 2025 22:33:39 +1100 Subject: [PATCH 12/18] Fix #6258. Fix #5727 properly. Only assume layerset axis on "standard case" elements, nothing else. --- src/bonsai/bonsai/tool/model.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 543106f937..ea6cb176bb 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -786,17 +786,21 @@ class Model(bonsai.core.tool.Model): elif material.is_a("IfcMaterialLayerSet"): axis = ifcopenshell.util.element.get_pset(element, "EPset_Parametric", "LayerSetDirection") if axis is None: - if element.is_a() in [ + if element.is_a() in ( "IfcSlabType", "IfcRoofType", "IfcRampType", "IfcPlateType", - "IfcCovering", - "IfcFurniture", - ]: + "IfcSlab", + "IfcRoof", + "IfcRamp", + "IfcPlate", + ): axis = "AXIS3" - else: + elif element.is_a() in ("IfcWallType", "IfcWall"): axis = "AXIS2" + else: + return return f"LAYER{axis[-1]}" elif material.is_a("IfcMaterialProfileSetUsage"): # TODO: remove after we support editing profile usages with IfcRevolvedAreaSolid. From 0d3be9d101d9d86557b932f2bba1f53402ff2b80 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 26 Oct 2025 22:52:31 +1100 Subject: [PATCH 13/18] Fix #5108. Selector now supports both GlobalId and Name as filter values. --- .../ifcopenshell/util/selector.py | 34 ++++++++++++++----- .../test/util/test_selector.py | 4 +++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index aa6789865f..8bb7955d91 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -856,8 +856,10 @@ class FacetTransformer(lark.Transformer): comparison, value = args def filter_function(element: ifcopenshell.entity_instance) -> bool: - element_value = getattr(ifcopenshell.util.element.get_type(element), "Name", None) - return self.compare(element_value, comparison, value) + element_type = ifcopenshell.util.element.get_type(element) + return self.compare(getattr(element_type, "Name", None), comparison, value) or self.compare( + getattr(element_type, "GlobalId", None), comparison, value + ) self.add_default_elements() self.elements = set(filter(filter_function, self.elements)) @@ -940,7 +942,7 @@ class FacetTransformer(lark.Transformer): containers = self.get_container_tree(container) result = False if containers else None for container in containers: - if self.compare(container.Name, "=", value): + if self.compare(container.Name, "=", value) or self.compare(container.GlobalId, "=", value): result = True if result is not None: return result if comparison == "=" else not result @@ -958,6 +960,8 @@ class FacetTransformer(lark.Transformer): if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup: if self.compare(rel.RelatingGroup.Name, "=", value): result = True + elif self.compare(rel.RelatingGroup.GlobalId, "=", value): + result = True return result if comparison == "=" else not result self.add_default_elements() @@ -969,32 +973,44 @@ class FacetTransformer(lark.Transformer): parents = set() for rel in self.file.by_type("IfcRelAggregates"): parent = rel.RelatingObject - if parent and self.compare(parent.Name, comparison, value): + if parent and ( + self.compare(parent.Name, comparison, value) or self.compare(parent.GlobalId, comparison, value) + ): parents.add(parent) for rel in self.file.by_type("IfcRelContainedInSpatialStructure"): parent = rel.RelatingStructure - if parent and self.compare(parent.Name, comparison, value): + if parent and ( + self.compare(parent.Name, comparison, value) or self.compare(parent.GlobalId, comparison, value) + ): parents.add(parent) for rel in self.file.by_type("IfcRelNests"): parent = rel.RelatingObject - if parent and self.compare(parent.Name, comparison, value): + if parent and ( + self.compare(parent.Name, comparison, value) or self.compare(parent.GlobalId, comparison, value) + ): parents.add(parent) for rel in self.file.by_type("IfcRelVoidsElement"): parent = rel.RelatingBuildingElement - if parent and self.compare(parent.Name, comparison, value): + if parent and ( + self.compare(parent.Name, comparison, value) or self.compare(parent.GlobalId, comparison, value) + ): parents.add(parent) for rel in self.file.by_type("IfcRelVoidsElement"): parent = rel.RelatingBuildingElement - if parent and self.compare(parent.Name, comparison, value): + if parent and ( + self.compare(parent.Name, comparison, value) or self.compare(parent.GlobalId, comparison, value) + ): parents.add(parent) for rel in self.file.by_type("IfcRelFillsElement"): parent = rel.RelatingOpeningElement - if parent and self.compare(parent.Name, comparison, value): + if parent and ( + self.compare(parent.Name, comparison, value) or self.compare(parent.GlobalId, comparison, value) + ): parents.add(parent) children: set[ifcopenshell.entity_instance] = set() diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index 50b032fe8e..36d200539a 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -196,6 +196,7 @@ class TestFilterElements(test.bootstrap.IFC4): assert subject.filter_elements(self.file, "IfcWall, type=Foo") == {element} assert subject.filter_elements(self.file, 'IfcWall, type="Foo"') == {element} assert subject.filter_elements(self.file, "IfcWall, type=/Fo.*/") == {element} + assert subject.filter_elements(self.file, f"IfcWall, type={element_type.GlobalId}") == {element} def test_selecting_by_material(self): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") @@ -273,6 +274,7 @@ class TestFilterElements(test.bootstrap.IFC4): assert subject.filter_elements(self.file, "IfcWall, location=G") == {element, element2} assert subject.filter_elements(self.file, "IfcWall, location=Building") == {element, element2} assert subject.filter_elements(self.file, "IfcWall, location!=Space") == {element2} + assert subject.filter_elements(self.file, f"IfcWall, location={space.GlobalId}") == {element} def test_selecting_by_group(self): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") @@ -281,6 +283,7 @@ class TestFilterElements(test.bootstrap.IFC4): ifcopenshell.api.group.assign_group(self.file, products=[element], group=group) assert subject.filter_elements(self.file, "IfcWall, group=Foo") == {element} assert subject.filter_elements(self.file, "IfcWall, group!=Foo") == {element2} + assert subject.filter_elements(self.file, f"IfcWall, group={group.GlobalId}") == {element} def test_selecting_by_parent(self): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall", name="Element1") @@ -300,6 +303,7 @@ class TestFilterElements(test.bootstrap.IFC4): assert subject.filter_elements(self.file, "IfcWall, parent=Space") == {element} assert subject.filter_elements(self.file, "IfcWall, parent=G") == {element, element2, element3} assert subject.filter_elements(self.file, "IfcWall, parent=Element2") == {element3} + assert subject.filter_elements(self.file, "IfcWall, parent=Space") == {element} def test_selecting_multiple_filter_groups(self): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") From 5f65e218789d01e84b8a221ea024e69d84cbb548 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 26 Oct 2025 23:05:05 +1100 Subject: [PATCH 14/18] Fix bug where you couldn't get elements using a predefined property set. --- src/ifcopenshell-python/ifcopenshell/util/element.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index bf870cb1ed..7bbe8bdc99 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -474,7 +474,7 @@ def get_elements_by_pset(pset: ifcopenshell.entity_instance) -> set[ifcopenshell """Retrieve the elements (or element types) that are using the provided property set.""" is_ifc2x3 = pset.file.schema == "IFC2X3" elements = set() - if pset.is_a("IfcPropertySet") or pset.is_a("IfcElementQuantity"): + if pset.is_a("IfcPropertySet") or pset.is_a("IfcPreDefinedPropertySet") or pset.is_a("IfcElementQuantity"): rels = pset.PropertyDefinitionOf if is_ifc2x3 else pset.DefinesOccurrence for rel in rels: elements.update(rel.RelatedObjects) From b1c44d42e39d5a2a3e5b8529566562b6bc94706c Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sun, 26 Oct 2025 13:26:15 +0100 Subject: [PATCH 15/18] Reset logger product to none --- src/ifcgeom/Iterator.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ifcgeom/Iterator.cpp b/src/ifcgeom/Iterator.cpp index fa36ce1e34..2ea32947a3 100644 --- a/src/ifcgeom/Iterator.cpp +++ b/src/ifcgeom/Iterator.cpp @@ -367,11 +367,13 @@ void IfcGeom::Iterator::create_element_(ifcopenshell::geometry::Converter* kerne })); if (!brep) { + Logger::SetProduct(boost::none); return; } auto elem = process_based_on_settings(settings, brep); if (!elem) { + Logger::SetProduct(boost::none); return; } @@ -394,6 +396,8 @@ void IfcGeom::Iterator::create_element_(ifcopenshell::geometry::Converter* kerne } } } + + Logger::SetProduct(boost::none); } IfcGeom::Element* IfcGeom::Iterator::process_based_on_settings(ifcopenshell::geometry::Settings settings, IfcGeom::BRepElement* elem, IfcGeom::TriangulationElement* previous) From b10415b1b3c3b13ce23c6d86684913879c9f047f Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sun, 26 Oct 2025 13:21:06 +0100 Subject: [PATCH 16/18] Rework iterator constructor #7291 --- src/ifcopenshell-python/ifcopenshell/geom/main.py | 9 +++------ src/ifcwrap/IfcGeomWrapper.i | 5 +++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index 5f3e2079f2..fa1cbd22f2 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -22,7 +22,7 @@ import os import sys import operator -from .. import ifcopenshell_wrapper +from .. import open, ifcopenshell_wrapper from ..file import file from ..entity_instance import entity_instance @@ -307,10 +307,7 @@ class iterator(ifcopenshell_wrapper.Iterator): self.file = file file_or_filename = file_or_filename.wrapped_data else: - # @todo? - self.file = None - # Makes sure people are able to use python's platform agnostic paths - file_or_filename = os.path.abspath(file_or_filename) + file_or_filename = self.file = open(file_or_filename) if include is not None and exclude is not None: raise ValueError("include and exclude cannot be specified simultaneously") @@ -342,7 +339,7 @@ class iterator(ifcopenshell_wrapper.Iterator): geometry_library, self.settings, file_or_filename, include_or_exclude, include is not None, num_threads ) else: - ifcopenshell_wrapper.Iterator.__init__(self, geometry_library, settings, file_or_filename, num_threads) + self.this = ifcopenshell_wrapper.construct_iterator(geometry_library, self.settings, file_or_filename, num_threads) if has_occ: diff --git a/src/ifcwrap/IfcGeomWrapper.i b/src/ifcwrap/IfcGeomWrapper.i index 5b7b4aba0b..6991abbdaf 100644 --- a/src/ifcwrap/IfcGeomWrapper.i +++ b/src/ifcwrap/IfcGeomWrapper.i @@ -628,6 +628,7 @@ struct ShapeRTTI : public boost::static_visitor $result = boost::apply_visitor(ShapeRTTI(), (boost::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; @@ -635,6 +636,10 @@ struct ShapeRTTI : public boost::static_visitor // 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, IfcParse::IfcFile* 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, IfcParse::IfcFile* file, std::vector elems, bool include, int num_threads) { std::set elems_set(elems.begin(), elems.end()); IfcGeom::entity_filter ef{ include, false, elems_set }; From 391a412e65c0cc9afe68c1067cfc34ae6cb9b358 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sun, 26 Oct 2025 13:21:35 +0100 Subject: [PATCH 17/18] test cases #7291 --- .../test/test_create_shape.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/ifcopenshell-python/test/test_create_shape.py b/src/ifcopenshell-python/test/test_create_shape.py index 57ffa0b78b..46d02c5699 100644 --- a/src/ifcopenshell-python/test/test_create_shape.py +++ b/src/ifcopenshell-python/test/test_create_shape.py @@ -1,3 +1,8 @@ +import functools +import itertools +import multiprocessing +import operator +import os import pytest import test.bootstrap import ifcopenshell @@ -12,6 +17,7 @@ import ifcopenshell.util.shape from ifcopenshell.util.shape_builder import ShapeBuilder from typing import get_args +fn = os.path.join(os.path.dirname(__file__), "fixtures/ColumnPSetsOfSets.ifc") class TestGeomSettings: def test_settings(self): @@ -185,6 +191,28 @@ class TestAssignObject: assert len(set(vs)) == 12 +def test_iterator(): + # just test some permutations of invocation + settings = ifcopenshell.geom.settings() + file_or_filename = [fn, ifcopenshell.open(fn)] + with_or_without_threads = [[], [multiprocessing.cpu_count()]] + includes = [ + {}, + {'include': ['IfcColumn']}, + {'include': [file_or_filename[1].by_type('IfcColumn')[0]]}, + ] + for args in itertools.product(file_or_filename, with_or_without_threads, includes): + kwargs = functools.reduce(operator.or_, (a for a in args if isinstance(a, dict))) + pargs = [] + for a in (_ for _ in args if not isinstance(_, dict)): + if isinstance(a, list): + pargs.extend(a) + else: + pargs.append(a) + iterator = ifcopenshell.geom.iterator(settings, *pargs, **kwargs) + assert iterator.initialize() + + if __name__ == "__main__": import pytest From 6df5632f91653cc3bec676178b88ca2789139aa5 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sun, 26 Oct 2025 13:33:10 +0100 Subject: [PATCH 18/18] black --- src/ifcopenshell-python/ifcopenshell/geom/main.py | 4 +++- src/ifcopenshell-python/test/test_create_shape.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index fa1cbd22f2..a1f00bb137 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -339,7 +339,9 @@ class iterator(ifcopenshell_wrapper.Iterator): geometry_library, self.settings, file_or_filename, include_or_exclude, include is not None, num_threads ) else: - self.this = ifcopenshell_wrapper.construct_iterator(geometry_library, self.settings, file_or_filename, num_threads) + self.this = ifcopenshell_wrapper.construct_iterator( + geometry_library, self.settings, file_or_filename, num_threads + ) if has_occ: diff --git a/src/ifcopenshell-python/test/test_create_shape.py b/src/ifcopenshell-python/test/test_create_shape.py index 46d02c5699..bc743217bf 100644 --- a/src/ifcopenshell-python/test/test_create_shape.py +++ b/src/ifcopenshell-python/test/test_create_shape.py @@ -19,6 +19,7 @@ from typing import get_args fn = os.path.join(os.path.dirname(__file__), "fixtures/ColumnPSetsOfSets.ifc") + class TestGeomSettings: def test_settings(self): settings = ifcopenshell.geom.settings() @@ -198,8 +199,8 @@ def test_iterator(): with_or_without_threads = [[], [multiprocessing.cpu_count()]] includes = [ {}, - {'include': ['IfcColumn']}, - {'include': [file_or_filename[1].by_type('IfcColumn')[0]]}, + {"include": ["IfcColumn"]}, + {"include": [file_or_filename[1].by_type("IfcColumn")[0]]}, ] for args in itertools.product(file_or_filename, with_or_without_threads, includes): kwargs = functools.reduce(operator.or_, (a for a in args if isinstance(a, dict)))