diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98efade0ba..0185aa8077 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,6 +50,7 @@ jobs: pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely pip install src/bcf --no-deps pip install git+https://github.com/zdhoward/aud + pip install pytest-xdist==3.8.0 - name: Install C++ dependencies run: | @@ -120,20 +121,23 @@ jobs: sudo make -j $(nproc) sudo make install - - name: Run IfcConvert on Sample files - run: | - (find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \ - echo $i | tee -a log; \ - timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \ - echo $i $? >> statuses; \ - done) || true - echo Failed - grep -v 0$ statuses - grep -v 0$ statuses | wc -l - echo Succeeded - grep 0$ statuses - grep 0$ statuses | wc -l + # - name: Run IfcConvert on Sample files + # run: | + # (find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \ + # echo $i | tee -a log; \ + # timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \ + # echo $i $? >> statuses; \ + # done) || true + # echo Failed + # grep -v 0$ statuses + # grep -v 0$ statuses | wc -l + # echo Succeeded + # grep 0$ statuses + # grep 0$ statuses | wc -l + - name: Run IfcConvert on Sample file + run: | + IfcConvert test/input/acad2010_walls.ifc test/input/acad2010_walls.obj - name: Test ifcopenshell-python run: | @@ -143,7 +147,7 @@ jobs: mv ifcopenshell ifcopenshell-local # Force testing on installed module pip install -e ../ifcpatch --no-deps # Needed for sql.py tests. ERROR=0 - make test || ERROR=1 + make test-parallel || ERROR=1 cd ../bcf && make test || ERROR=1 pip install requests cd ../bsdd && make test || ERROR=1 diff --git a/.gitignore b/.gitignore index 608fdd3e74..37f7ea9c98 100644 --- a/.gitignore +++ b/.gitignore @@ -109,4 +109,7 @@ src/bonsai/bonsai/bim/schema/Brick.ttl bonsaiDecoratorForLoads.code-workspace dev_environment.bat -.pixi/ \ No newline at end of file +.pixi/ + +src/ifcopenshell-python/ifcopenshell/express/*.exp +src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat diff --git a/README.md b/README.md index 9d5160b4fa..4e7285f53f 100644 --- a/README.md +++ b/README.md @@ -37,25 +37,25 @@ Contents | Name | Description | License | Service | | ------------------------- | --------------------------------------------------------------------- | ------------------- | ------- | -| bcf | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bcf-client?label=PyPI&color=006dad)](https://pypi.org/project/bcf-client/) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/bcf-client/badges/version.svg)](https://anaconda.org/conda-forge/bcf-client) | -| bonsai | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [![Official](https://img.shields.io/badge/BonsaiBIM.org-Download-70ba35)](https://bonsaibim.org/download.html) [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=bonsai-*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [![Chocolatey](https://img.shields.io/chocolatey/v/blenderbim-nightly?label=Chocolatey&color=5c9fd8)](https://community.chocolatey.org/packages/blenderbim-nightly/) | -| bsdd | Library to query the bSDD API | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bsdd?label=PyPI&color=006dad)](https://pypi.org/project/bsdd/) | -| ifc2ca | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later | -| ifc4d | Convert to and from IFC and project management software | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc4d?label=PyPI&color=006dad)](https://pypi.org/project/ifc4d/) | -| ifc5d | Report and optimise cost information from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc5d?label=PyPI&color=006dad)](https://pypi.org/project/ifc5d/) | -| ifcbimtester | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later | +| [bcf](https://docs.ifcopenshell.org/bcf.html) | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bcf-client?label=PyPI&color=006dad)](https://pypi.org/project/bcf-client/) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/bcf-client/badges/version.svg)](https://anaconda.org/conda-forge/bcf-client) | +| [bonsai](https://docs.ifcopenshell.org/bonsai.html) | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [![Official](https://img.shields.io/badge/BonsaiBIM.org-Download-70ba35)](https://bonsaibim.org/download.html) [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=bonsai-*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [![Chocolatey](https://img.shields.io/chocolatey/v/blenderbim-nightly?label=Chocolatey&color=5c9fd8)](https://community.chocolatey.org/packages/blenderbim-nightly/) | +| [bsdd](https://docs.ifcopenshell.org/bsdd.html) | Library to query the bSDD API | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/bsdd?label=PyPI&color=006dad)](https://pypi.org/project/bsdd/) | +| [ifc2ca](https://docs.ifcopenshell.org/ifc2ca.html) | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later | +| [ifc4d](https://docs.ifcopenshell.org/ifc4d.html) | Convert to and from IFC and project management software | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc4d?label=PyPI&color=006dad)](https://pypi.org/project/ifc4d/) | +| [ifc5d](https://docs.ifcopenshell.org/ifc5d.html) | Report and optimise cost information from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifc5d?label=PyPI&color=006dad)](https://pypi.org/project/ifc5d/) | +| [ifcbimtester](https://docs.ifcopenshell.org/bimtester.html) | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later | | ifcblender | Historic Blender IFC import add-on | LGPL-3.0-or-later\* | -| ifccityjson | Convert CityJSON to IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccityjson?label=PyPI&color=006dad)](https://pypi.org/project/ifccityjson/) | -| ifcclash | Clash detection library and CLI app | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcclash?label=PyPI&color=006dad)](https://pypi.org/project/ifcclash/) | -| ifcconvert | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcconvert/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcconvert-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true) -| ifccsv | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccsv?label=PyPI&color=006dad)](https://pypi.org/project/ifccsv/) | -| ifcdiff | Compare changes between IFC models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcdiff?label=PyPI&color=006dad)](https://pypi.org/project/ifcdiff/) | -| ifcfm | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcfm?label=PyPI&color=006dad)](https://pypi.org/project/ifcfm/) | -| ifcmax | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcmax.html) -| ifcopenshell-python | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) | -| ifcpatch | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcpatch?label=PyPI&color=006dad)](https://pypi.org/project/ifcpatch/) | -| ifcsverchok | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcsverchok-*.*.*.*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true) -| ifctester | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifctester?label=PyPI&color=006dad)](https://pypi.org/project/ifctester/) | +| [ifccityjson](https://docs.ifcopenshell.org/ifccityjson.html) | Convert CityJSON to IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccityjson?label=PyPI&color=006dad)](https://pypi.org/project/ifccityjson/) | +| [ifcclash](https://docs.ifcopenshell.org/ifcclash.html) | Clash detection library and CLI app | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcclash?label=PyPI&color=006dad)](https://pypi.org/project/ifcclash/) | +| [ifcconvert](https://docs.ifcopenshell.org/ifcconvert.html) | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcconvert/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcconvert-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true) +| [ifccsv](https://docs.ifcopenshell.org/ifccsv.html) | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccsv?label=PyPI&color=006dad)](https://pypi.org/project/ifccsv/) | +| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcdiff?label=PyPI&color=006dad)](https://pypi.org/project/ifcdiff/) | +| [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcfm?label=PyPI&color=006dad)](https://pypi.org/project/ifcfm/) | +| [ifcmax](https://docs.ifcopenshell.org/ifcmax.html) | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcmax.html) +| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) | +| [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcpatch?label=PyPI&color=006dad)](https://pypi.org/project/ifcpatch/) | +| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcsverchok-*.*.*.*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true) +| [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifctester?label=PyPI&color=006dad)](https://pypi.org/project/ifctester/) | The IfcOpenShell C++ codebase is split into multiple interal libraries: diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 6ccf38cd16..560c3b02b8 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -426,7 +426,7 @@ if(WASM_BUILD) else() # @todo review this, shouldn't this be all possible header-only now? # ... or rewritten using C++17 features? - set(BOOST_COMPONENTS system program_options regex thread date_time) + set(BOOST_COMPONENTS system program_options regex thread date_time iostreams) endif() if(USE_MMAP) @@ -1044,11 +1044,7 @@ if(BUILD_IFCGEOM) foreach(schema ${SCHEMA_VERSIONS}) set(IFCGEOM_SCHEMA_LIBRARIES ${IFCGEOM_SCHEMA_LIBRARIES} geometry_mapping_ifc${schema}) endforeach() - if(WASM_BUILD) - set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES}) - else() - set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES}) - endif() + set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES}) endif() if(BUILD_CONVERT OR BUILD_IFCPYTHON) @@ -1177,7 +1173,10 @@ if(BUILD_IFCGEOM) add_library(geometry_mapping_ifc${schema} STATIC ${IFCGEOM_FILES}) set_target_properties(geometry_mapping_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}") - target_link_libraries(geometry_mapping_ifc${schema} IfcParse IfcGeom) + target_link_libraries(geometry_mapping_ifc${schema} IfcParse) + if (NOT BUILD_SHARED_LIBS) + target_link_libraries(geometry_mapping_ifc${schema} IfcGeom) + endif() list(APPEND mapping_libraries geometry_mapping_ifc${schema}) endforeach() diff --git a/nix/build-all.py b/nix/build-all.py index 701f1e4858..5226562557 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -154,7 +154,7 @@ SWIG_VERSION = "4.1.0" OPENCOLLADA_VERSION = "v1.6.68" HDF5_VERSION = "1.13.1" -GMP_VERSION = "6.2.1" +GMP_VERSION = "6.3.0" MPFR_VERSION = "3.1.6" # latest is 4.1.0 CGAL_VERSION = "v5.6.3" USD_VERSION = "23.05" diff --git a/src/bonsai/bonsai/bim/data/assets/patterns.svg b/src/bonsai/bonsai/bim/data/assets/patterns.svg index 11be0ee021..1f62673bda 100644 --- a/src/bonsai/bonsai/bim/data/assets/patterns.svg +++ b/src/bonsai/bonsai/bim/data/assets/patterns.svg @@ -1,85 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + - + - + @@ -94,16 +117,19 @@ + + + @@ -112,22 +138,23 @@ - + + - + - + diff --git a/src/bonsai/bonsai/bim/data/icons/dm_trim.png b/src/bonsai/bonsai/bim/data/icons/dm_trim.png new file mode 100644 index 0000000000..235f85935d Binary files /dev/null and b/src/bonsai/bonsai/bim/data/icons/dm_trim.png differ diff --git a/src/bonsai/bonsai/bim/data/icons/lm_trim.png b/src/bonsai/bonsai/bim/data/icons/lm_trim.png new file mode 100644 index 0000000000..ff116439df Binary files /dev/null and b/src/bonsai/bonsai/bim/data/icons/lm_trim.png differ diff --git a/src/bonsai/bonsai/bim/data/pset/EPset_Drawing.ifc b/src/bonsai/bonsai/bim/data/pset/EPset_Drawing.ifc index a516b9765d..70d6049ad7 100644 --- a/src/bonsai/bonsai/bim/data/pset/EPset_Drawing.ifc +++ b/src/bonsai/bonsai/bim/data/pset/EPset_Drawing.ifc @@ -13,10 +13,10 @@ DATA; #6=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); #7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); #8=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); -#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); +#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','Whether or not this drawing can be referenced in other drawings.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); #10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','Comma separated list of selector expressions to evaluate for each drawing elementand add results to their ''class'' attribute.\X2\000A\X0\E.g. ''Name, id'' would add to ''class'' value similar to ''Name-Wall id-1220''.\X2\000A\X0\Then it can be used to applied css styles based on the resulting class.\X2\000A\X0\If attribute is not present on the element, then it won''t be added to it''s ''class''.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); -#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); +#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); #13=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); #14=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); #15=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); diff --git a/src/bonsai/bonsai/bim/export_ifc.py b/src/bonsai/bonsai/bim/export_ifc.py index 5fe5a1df97..d507481141 100644 --- a/src/bonsai/bonsai/bim/export_ifc.py +++ b/src/bonsai/bonsai/bim/export_ifc.py @@ -36,6 +36,7 @@ from bonsai.bim.ifc import IfcStore from mathutils import Vector from typing import Union from logging import Logger +from math import radians class IfcExporter: @@ -104,7 +105,16 @@ class IfcExporter: def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]: element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id) - if tool.Geometry.is_scaled(obj): + # Handle camera scales specially + if obj.type == "CAMERA": + # Check if this is a reflected ceiling plan camera + camera = tool.Ifc.get_entity(obj) + if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW": + # Ensure reflected ceiling cameras have the correct scale + if obj.scale != (-1, -1, -1): + obj.scale = (-1, -1, -1) + # Skip all other scale handling for cameras + elif tool.Geometry.is_scaled(obj): bpy.ops.bim.update_representation(obj=obj.name) # update_representation might not apply scale if the object has openings # reset it, so let user know that the scale wasn't saved. diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index 7b10f8c3fd..c7cb10cdde 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -222,8 +222,6 @@ def refresh_ui_data(): if isinstance(ifc_file := tool.Ifc.get(), ifcopenshell.sqlite): ifc_file.clear_cache() - props = tool.Drawing.get_document_props() - props.should_draw_decorations = props.should_draw_decorations if tool.Web.get_web_props().is_connected: tool.Web.send_webui_data() diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 9252ecb94b..1765b1ca24 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -952,6 +952,9 @@ class IfcImporter: if not props.ifc_file: props.ifc_file = self.ifc_import_settings.input_file self.file = tool.Ifc.get() + # IFC4 Reference View shall have no booleans https://github.com/BuildingSMART/IFC4-CV/issues/14 + if self.file.schema == "IFC4" and "ReferenceView" in str(self.file.header.file_description.description): + self.ifc_import_settings.void_limit = 0 def calculate_unit_scale(self): self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) diff --git a/src/bonsai/bonsai/bim/module/aggregate/operator.py b/src/bonsai/bonsai/bim/module/aggregate/operator.py index 6e902e76f5..54147c9e34 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/operator.py +++ b/src/bonsai/bonsai/bim/module/aggregate/operator.py @@ -94,6 +94,9 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): + aggregates_to_check = set() + + # First pass: unassign all parts and track their aggregates for obj in tool.Blender.get_selected_objects(): element = tool.Ifc.get_entity(obj) if not element: @@ -101,6 +104,10 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator): aggregate = ifcopenshell.util.element.get_aggregate(element) if not aggregate: continue + + # Track this aggregate for later checking + aggregates_to_check.add(aggregate) + core.unassign_object( tool.Ifc, tool.Aggregate, @@ -116,6 +123,29 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator): pset = tool.Ifc.get().by_id(pset["id"]) ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset) + # Second pass: delete aggregates that now have no parts + deleted_aggregates = [] + for aggregate in aggregates_to_check: + related_objects = ifcopenshell.util.element.get_parts(aggregate) + if len(related_objects) == 0: + aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}" + deleted_aggregates.append(aggregate_name) + + aggregate_obj = tool.Ifc.get_object(aggregate) + if aggregate_obj: + ifcopenshell.api.root.remove_product(tool.Ifc.get(), product=aggregate) + bpy.data.objects.remove(aggregate_obj, do_unlink=True) + + # Show info message if aggregates were deleted + if deleted_aggregates: + if len(deleted_aggregates) == 1: + self.report( + {"INFO"}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts" + ) + else: + aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates) + self.report({"INFO"}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts") + class BIM_OT_enable_editing_aggregate(bpy.types.Operator): """Enable editing aggregation relationship""" diff --git a/src/bonsai/bonsai/bim/module/classification/ui.py b/src/bonsai/bonsai/bim/module/classification/ui.py index 7cbfb0ed93..344c247af2 100644 --- a/src/bonsai/bonsai/bim/module/classification/ui.py +++ b/src/bonsai/bonsai/bim/module/classification/ui.py @@ -282,7 +282,11 @@ class BIM_PT_classification_references(Panel, ReferenceUI): @classmethod def poll(cls, context): - return bool((obj := context.active_object) and tool.Ifc.get_entity(obj)) + return ( + (obj := tool.Blender.get_active_object()) + and (element := tool.Ifc.get_entity(obj)) + and element.is_a("IfcObjectDefinition") + ) def get_object_name(self, context: bpy.types.Context) -> str: assert (obj := context.active_object) diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 3b527b198b..0820d5e3d6 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -818,7 +818,12 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator): class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.merge_identical_objects" bl_label = "Merge Identical Objects" - bl_description = "For materials currently only IfcMaterials are supported" + bl_description = ( + "Merge identical IFC objects (that match all attributes).\n" + "\n" + "SHIFT + CLICK to merge by name/identification attribute only.\n" + "Merges names with number suffix, as well (ex: foo, foo.001, foo.002)\n" + ) bl_options = {"REGISTER", "UNDO"} object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] @@ -826,18 +831,36 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator): items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)), ) + by_name_or_identification_only: bpy.props.BoolProperty( + name="By Name/Identification Only", + description="Merge based only on Name or Identification attribute, ignoring other properties", + default=False, + ) + if TYPE_CHECKING: object_type: tool.Debug.PurgeMergeObjectType + def invoke(self, context, event): + # Check if shift key is pressed + if event.shift: + self.by_name_or_identification_only = True + else: + self.by_name_or_identification_only = False + + return self.execute(context) + def _execute(self, context): object_type: str = self.object_type if object_type in ("PROFILE", "TYPE"): self.report({"ERROR"}, f"Unsupported object type {object_type}.") return {"CANCELLED"} - merged_data = tool.Debug.merge_identical_objects(object_type) + merged_data = tool.Debug.merge_identical_objects( + object_type, by_name_or_identification_only=self.by_name_or_identification_only + ) plural_object_type = f"{object_type.lower().replace('_', ' ')}s" if merged_data: + merge_mode = " by name/identification" if self.by_name_or_identification_only else "" for element_type, element_names in merged_data.items(): print(f"- {element_type}:") for name in element_names: @@ -846,7 +869,8 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator): merged = sum(len(v) for v in merged_data.values()) msg = " See system console for details." if merged else "" - self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged.{msg}") + merge_mode = " (by name/identification)" if self.by_name_or_identification_only else "" + self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged{merge_mode}.{msg}") if merged == 0: return diff --git a/src/bonsai/bonsai/bim/module/drawing/__init__.py b/src/bonsai/bonsai/bim/module/drawing/__init__.py index 7ca0e4bd93..187020fc77 100644 --- a/src/bonsai/bonsai/bim/module/drawing/__init__.py +++ b/src/bonsai/bonsai/bim/module/drawing/__init__.py @@ -61,6 +61,7 @@ classes = ( operator.EnableEditingAssignedProduct, operator.EnableEditingElementFilter, operator.EnableEditingText, + operator.ExcludeAnnotation, operator.ExpandSheet, operator.LoadDrawings, operator.LoadReferences, diff --git a/src/bonsai/bonsai/bim/module/drawing/data.py b/src/bonsai/bonsai/bim/module/drawing/data.py index b31b1371f4..cd24989981 100644 --- a/src/bonsai/bonsai/bim/module/drawing/data.py +++ b/src/bonsai/bonsai/bim/module/drawing/data.py @@ -235,13 +235,18 @@ class DecoratorData: cut_cache = {} slice_cache = {} fill_cache = {} + camera_location_checksum = None + camera_rotation_checksum = None + + @classmethod + def clear_cache(cls): + cls.cut_cache = {} + cls.layerset_cache = {} + cls.fill_cache = {} @classmethod def load(cls, handler): cls.is_loaded = True - cls.cut_cache = {} - cls.layerset_cache = {} - cls.fill_cache = {} text = {} dimension = {} diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index 26bb251035..2e5a75c9be 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -22,6 +22,7 @@ import blf import math import bmesh import shapely +import numpy as np import ifcopenshell import ifcopenshell.util.element import ifcopenshell.util.representation @@ -1625,6 +1626,7 @@ class CutDecorator: if cls.installed: cls.uninstall() handler = cls() + handler.cache_camera_matrix() cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW") @classmethod @@ -1727,6 +1729,30 @@ class CutDecorator: shader.uniform_float("color", color) batch.draw(shader) + def cache_camera_matrix(self): + obj = bpy.context.scene.camera + DecoratorData.camera_location_checksum = repr(np.array(obj.matrix_world.translation).tobytes()) + DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes()) + + def is_camera_moved(self): + if not DecoratorData.camera_location_checksum: + self.cache_camera_matrix() + return True # Let's be conservative + obj = bpy.context.scene.camera + loc_check = np.frombuffer(eval(DecoratorData.camera_location_checksum)) + loc_real = np.array(obj.matrix_world.translation).flatten() + if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm + self.cache_camera_matrix() + return True + rot_check = np.frombuffer(eval(DecoratorData.camera_rotation_checksum)).reshape(3, 3) + rot_real = np.array(obj.matrix_world.to_3x3()) + rot_dot = np.dot(rot_check, rot_real.T) + angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1)) + if angle_rad > 0.0017453292519943296: # 0.1 degrees + self.cache_camera_matrix() + return True + return False + def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None: has_cut_cache = element.id() in DecoratorData.cut_cache has_fill_cache = element.id() in DecoratorData.fill_cache @@ -1734,9 +1760,9 @@ class CutDecorator: # Currently selected objects must be recalculated as they may be being moved / edited. # If the camera is selected, we also recalculate as the user may be moving the camera. - if not has_cut_cache or obj.select_get() or context.scene.camera.select_get(): + if not has_cut_cache or obj.select_get() or self.is_camera_moved(): self.recalculate_cut(context, obj, element) - if not has_fill_cache or obj.select_get() or context.scene.camera.select_get(): + if not has_fill_cache or obj.select_get() or self.is_camera_moved(): self.recalculate_fill(context, obj, element) def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None: @@ -1933,8 +1959,7 @@ class DecorationsHandler: # NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR # and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL") - if not DecoratorData.is_loaded: - DecoratorData.load(handler) + DecoratorData.clear_cache() @classmethod def uninstall(cls): @@ -1961,5 +1986,8 @@ class DecorationsHandler: if not DrawingsData.is_loaded: DrawingsData.load() + if not DecoratorData.is_loaded: + DecoratorData.load(self) + for obj, decorator in DecoratorData.data["object_decorators"]: decorator.decorate(context, obj) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 7a04e9f9ad..1ca45f155a 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -210,12 +210,6 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator): should_duplicate_annotations=self.should_duplicate_annotations, ) - # TODO: Why need to resync active drawing, if it wasn't changed. - drawing = props.get_active_drawing() - if drawing is None: - return - core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing) - class CreateDrawing(bpy.types.Operator): """Creates/refreshes a .svg drawing @@ -442,6 +436,8 @@ class CreateDrawing(bpy.types.Operator): context.scene.render.filepath = str(Path(svg_path).with_suffix(".png")) assert (drawing_style := self.cprops.get_active_drawing_style()) + tool.Blender.sync_render_visibility() + if drawing_style.render_type == "DEFAULT": bpy.ops.render.render(write_still=True) else: @@ -625,7 +621,7 @@ class CreateDrawing(bpy.types.Operator): path.attrib["d"] = d group.append(g) - def generate_wall_layers(self, context: bpy.types.Context, root) -> None: + def generate_material_layers(self, context: bpy.types.Context, root) -> None: for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"): if "projection" in el.get("class", "").split(): continue @@ -794,8 +790,9 @@ class CreateDrawing(bpy.types.Operator): edge_bm.to_mesh(edge_mesh) edge_bm.free() - actual_path = svg_path[0:-4] + "0001.svg" + freestyle_svg_exporter = tool.Blender.get_addon("freestyle_svg_exporter") context.scene.render.filepath = svg_path[0:-4] + actual_path = freestyle_svg_exporter.create_path(bpy.context.scene) bpy.ops.render.render(write_still=False) os.replace(actual_path, svg_path) @@ -839,6 +836,8 @@ class CreateDrawing(bpy.types.Operator): if tool.Drawing.is_camera_orthographic(): self.generate_bisect_linework(context, root) + if self.cprops.generate_material_layers: + self.generate_material_layers(context, root) self.merge_linework_and_add_metadata(root) self.move_elements_to_top(root) @@ -936,12 +935,14 @@ class CreateDrawing(bpy.types.Operator): if self.cprops.cut_mode == "BISECT": self.remove_cut_linework(root) self.generate_bisect_linework(context, root) - self.generate_wall_layers(context, root) + if self.cprops.generate_material_layers: + self.generate_material_layers(context, root) self.merge_linework_and_add_metadata(root) self.move_elements_to_top(root) elif self.cprops.cut_mode == "OPENCASCADE": self.move_projection_to_bottom(root) - self.generate_wall_layers(context, root) + if self.cprops.generate_material_layers: + self.generate_material_layers(context, root) self.merge_linework_and_add_metadata(root) self.move_elements_to_top(root) @@ -1251,6 +1252,8 @@ class CreateDrawing(bpy.types.Operator): def get_svg_classes(self, element, layer=None): classes = [element.is_a()] + + # ─── Material ────────────────────────────────────────────── material = ifcopenshell.util.element.get_material(element, should_skip_usage=True) material_name = "" if material: @@ -1263,6 +1266,7 @@ class CreateDrawing(bpy.types.Operator): else: classes.append("material-null") + # ─── Layer ───────────────────────────────────────────────── if layer: classes.append(layer.is_a()) layer_material = layer.Material @@ -1275,12 +1279,19 @@ class CreateDrawing(bpy.types.Operator): layer_material_category = tool.Drawing.canonicalise_class_name(layer_material.Category) classes.append(f"layer-material-category-{layer_material_category}") + # ─── Metadata ────────────────────────────────────────────── for key in self.metadata: value = ifcopenshell.util.selector.get_element_value(element, key) if value: classes.append( tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value)) ) + + # ─── Target View ─────────────────────────────────────────── + if getattr(self.cprops, "target_view", None): + target_view_class = tool.Drawing.canonicalise_class_name(str(self.cprops.target_view)) + classes.append(f"target-view-{target_view_class}") + return classes def is_manifold(self, obj) -> bool: @@ -1347,7 +1358,13 @@ class CreateDrawing(bpy.types.Operator): join_criteria = join_criteria.split(",") else: # Drawing convention states that same objects classes with the same material are merged when cut. - join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status", "Material.Name"] + join_criteria = [ + "class", + "material.Name", + "/Pset_.*Common/.Status", + "EPset_Status.Status", + "EPset_Status.UserDefinedStatus", + ] group = root.find("{http://www.w3.org/2000/svg}g") joined_paths = {} @@ -1476,11 +1493,10 @@ class CreateDrawing(bpy.types.Operator): joined_paths.setdefault(hash_keys, []).append(el) for key, els in joined_paths.items(): - polygons = [] - classes = set() + queue = [] for el in els: - classes.update(el.attrib["class"].split()) + classes = set(el.attrib["class"].split()) classes.add(el.attrib["{http://www.ifcopenshell.org/ns}guid"]) is_closed_polygon = False for path in el.findall("{http://www.w3.org/2000/svg}path"): @@ -1495,31 +1511,30 @@ class CreateDrawing(bpy.types.Operator): coords.append(coords[0]) if len(coords) > 2 and coords[0] == coords[-1]: is_closed_polygon = True - polygons.append(shapely.Polygon(coords)) + queue.append((shapely.Polygon(coords), classes)) if is_closed_polygon: el.getparent().remove(el) - try: - merged_polygons = shapely.ops.unary_union(polygons) - except: - print("Warning. Portions of the merge failed. Please report a bug!", polygons) - merged_polygons = polygons + while queue: + polygon, polygon_classes = queue.pop() + for polygon2, polygon2_classes in queue[:]: + try: + merged_polygon = shapely.union(polygon, polygon2) + except: + print("Warning. Portions of the merge failed. Please report a bug!", polygon, polygon2) + continue + if type(merged_polygon) == shapely.Polygon: + polygon = merged_polygon + polygon_classes.update(polygon2_classes) + queue.remove((polygon2, polygon2_classes)) - if type(merged_polygons) == shapely.MultiPolygon: - merged_polygons = merged_polygons.geoms - elif type(merged_polygons) == shapely.Polygon: - merged_polygons = [merged_polygons] - else: - merged_polygons = [] - - for polygon in merged_polygons: g = etree.Element("g") path = etree.SubElement(g, "path") d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z" for interior in polygon.interiors: d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z" path.attrib["d"] = d - g.set("class", " ".join(list(classes))) + g.set("class", " ".join(list(polygon_classes))) group.append(g) def drawing_to_model_co(self, x: float, y: float) -> Vector: @@ -1861,13 +1876,15 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): props = tool.Drawing.get_document_props() - # Won't be visible in UI anyway. - prefs = tool.Blender.get_addon_preferences() - if not props.sheets or not prefs.data_dir: - return False if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") return False + if not props.sheets: + cls.poll_message_set("No sheets available.") + return False + if not tool.Blender.get_user_data_dir(): + cls.poll_message_set("BIM data directory not set.") + return False return True def _execute(self, context): @@ -2221,6 +2238,7 @@ class ActivateModel(bpy.types.Operator): ) tool.Blender.reset_object_visibility() + tool.Drawing.hide_all_drawing_collections() tool.Blender.update_viewport() bonsai.bim.handler.refresh_ui_data() @@ -2317,9 +2335,8 @@ class ActivateDrawingBase(tool.Ifc.Operator): dprops.active_drawing_id = self.drawing dprops.drawing_styles.clear() - if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"): - bpy.ops.bim.reload_drawing_styles() - bpy.ops.bim.activate_drawing_style() + bpy.ops.bim.reload_drawing_styles() + bpy.ops.bim.activate_drawing_style() if tool.Drawing.is_camera_orthographic(): core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing)) @@ -2332,10 +2349,22 @@ class ActivateDrawingBase(tool.Ifc.Operator): camera = context.scene.camera assert camera camera_props = tool.Drawing.get_camera_props(camera) + # Check if this is a reflected ceiling camera and preserve its scale + camera_element = tool.Ifc.get_entity(camera) + is_reflected = False + if camera_element: + is_reflected = ( + ifcopenshell.util.element.get_pset(camera_element, "EPset_Drawing", "TargetView") + == "REFLECTED_PLAN_VIEW" + ) + if is_reflected and camera.scale != (-1, -1, -1): + camera.scale = (-1, -1, -1) + if camera_props.update_representation(camera.matrix_world): bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="") - # See 6452 and 6478. - # bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT") + # Restore the scale after update if needed + if is_reflected: + camera.scale = (-1, -1, -1) return {"FINISHED"} @@ -2811,8 +2840,13 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator): if not props.schedules: cls.poll_message_set("No schedule selected.") return False - prefs = tool.Blender.get_addon_preferences() - return props.schedules and props.sheets and prefs.data_dir + if not props.sheets: + cls.poll_message_set("No sheets available.") + return False + if not tool.Blender.get_user_data_dir(): + cls.poll_message_set("BIM data directory not set.") + return False + return True def _execute(self, context): props = tool.Drawing.get_document_props() @@ -2879,8 +2913,13 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator): if not props.references: cls.poll_message_set("No reference selected.") return False - bim_props = tool.Blender.get_bim_props() - return props.references and props.sheets and bim_props.data_dir + if not props.sheets: + cls.poll_message_set("No sheets available.") + return False + if not tool.Blender.get_user_data_dir(): + cls.poll_message_set("BIM data directory not set.") + return False + return True def _execute(self, context): props = tool.Drawing.get_document_props() @@ -3729,3 +3768,19 @@ class OpenDocumentationWebUi(bpy.types.Operator): else: bpy.ops.bim.open_web_browser(page="documentation") return {"FINISHED"} + + +class ExcludeAnnotation(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.exclude_annotation" + bl_label = "Exclude Annotation" + bl_description = "Excludes the automatic annotation reference from the drawing" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + if not (obj := bpy.context.scene.camera) or not (drawing := tool.Ifc.get_entity(obj)): + return + for obj in tool.Blender.get_selected_objects(include_active=False): + if (element := tool.Ifc.get_entity(obj)) and tool.Drawing.is_auto_annotation(element): + if referenced_element := tool.Drawing.get_annotation_element(element): + tool.Drawing.exclude_annotation_from_drawing(referenced_element, drawing) + core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing) diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index fb5c05679a..4dd025afc0 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -498,6 +498,9 @@ class BIMCameraProperties(PropertyGroup): name="Linework Mode", update=get_update_layer_callback("linework_mode", "LineworkMode"), ) + generate_material_layers: bpy.props.BoolProperty( + name="Generate Material Layers", description="Generate material layer linework in drawings", default=True + ) fill_mode: EnumProperty( items=[ ("NONE", "None", "Disable filling areas seen in projection"), @@ -859,6 +862,19 @@ class BIMAnnotationProperties(PropertyGroup): ) is_adding_type: bpy.props.BoolProperty(default=False) type_name: bpy.props.StringProperty(name="Name", default="TYPEX") + tag_rotation_mode: bpy.props.EnumProperty( + name="Tag Rotation Mode", + description="How to orient the tag relative to the tagged object", + items=[ + ("NONE", "No Rotation", "Keep tag in default orientation"), + ("LOCAL_X", "Local X Axis", "Align tag with object's local X axis"), + ("LOCAL_Y", "Local Y Axis", "Align tag with object's local Y axis"), + ("LOCAL_Z", "Local Z Axis", "Align tag with object's local Z axis"), + ("CAMERA_Horizontal", "Camera Horizontal", "Align tag with camera X axis"), + ("CAMERA_Vertical", "Camera Vertical", "Align tag with camera Y axis"), + ], + default="NONE", + ) if TYPE_CHECKING: object_type: str diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index 905bc8e287..82ddfef55a 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -99,12 +99,13 @@ class BIM_PT_camera(Panel): row = self.layout.row() row.prop(props, "linework_mode") + row = self.layout.row() + row.prop(props, "generate_material_layers") if props.linework_mode == "OPENCASCADE": row = self.layout.row() row.prop(props, "fill_mode") row = self.layout.row() row.prop(props, "cut_mode") - row = self.layout.row() row.prop(props, "width") row = self.layout.row() @@ -193,6 +194,7 @@ class BIM_PT_element_filters(Panel): text = "Exclude Filter" if ElementFiltersData.data["has_exclude_filter"] else "No Exclude Filter Found" icon = "GREASEPENCIL" if ElementFiltersData.data["has_exclude_filter"] else "ADD" row.label(text=text, icon="FILTER") + row.operator("bim.exclude_annotation", icon="REMOVE", text="") row.operator("bim.enable_editing_element_filter", icon=icon, text="").filter_mode = "EXCLUDE" diff --git a/src/bonsai/bonsai/bim/module/drawing/workspace.py b/src/bonsai/bonsai/bim/module/drawing/workspace.py index bf2f026358..b938514c70 100644 --- a/src/bonsai/bonsai/bim/module/drawing/workspace.py +++ b/src/bonsai/bonsai/bim/module/drawing/workspace.py @@ -228,7 +228,6 @@ class AnnotationToolUI: def draw_type_selection_interface(cls): # shared by both sidebar and header object_type = cls.props.object_type - row = cls.layout.row(align=True) row.label(text="", icon="FILE_VOLUME") prop_with_search(row, cls.props, "object_type", text="") @@ -248,6 +247,9 @@ class AnnotationToolUI: add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation") if object_type in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP: + row = cls.layout.row(align=True) + row.label(text="", icon="DRIVER_ROTATIONAL_DIFFERENCE") + row.prop(cls.props, "tag_rotation_mode", text="") add_layout_hotkey_operator( cls.layout, "Bulk Tag", @@ -299,6 +301,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): return related_objects = bpy.context.selected_objects + created_objects = [] + for related_object in related_objects: obj = core.add_annotation( tool.Ifc, @@ -311,7 +315,15 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): ), enable_editing=False, ) - tool.Drawing.setup_annotation_object(obj, object_type, related_object) + tool.Drawing.setup_annotation_object(obj, object_type, related_object, props.tag_rotation_mode) + created_objects.append(obj) + + # Select the created annotation objects + bpy.ops.object.select_all(action="DESELECT") + for obj in created_objects: + obj.select_set(True) + if created_objects: + bpy.context.view_layer.objects.active = created_objects[-1] def hotkey_S_A(self): if bpy.ops.bim.add_annotation.poll(): @@ -341,4 +353,5 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): continue related_object = tool.Ifc.get_object(related_product) - tool.Drawing.setup_annotation_object(obj, annotation_type, related_object) + rotation_mode = tool.Drawing.get_annotation_props().tag_rotation_mode + tool.Drawing.setup_annotation_object(obj, annotation_type, related_object, rotation_mode) diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py index bba979af30..77a2c8b63b 100644 --- a/src/bonsai/bonsai/bim/module/geometry/__init__.py +++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py @@ -109,8 +109,9 @@ def block_scale(scene: bpy.types.Scene) -> None: if obj.type == "CAMERA": camera = tool.Ifc.get_entity(obj) if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW": - obj.scale = (-1, -1, -1) - obj.rotation_euler = (0.0, 0.0, math.radians(180)) + # Only update if scale isn't already (-1, -1, -1) + if obj.scale != (-1, -1, -1): + obj.scale = (-1, -1, -1) else: if obj.scale != (1, 1, 1): obj.scale = (1, 1, 1) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 401428cddb..248fc9c948 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -858,6 +858,10 @@ class OverrideDelete(bpy.types.Operator): objects_to_remove = context.selected_objects self.process_arrays(context) + + # Track aggregates before deleting their parts + aggregates_to_check = self.track_aggregates(objects_to_remove) + clear_active_object = True for i, obj in enumerate(objects_to_remove, 1): @@ -882,13 +886,28 @@ class OverrideDelete(bpy.types.Operator): continue if ifcopenshell.util.element.get_pset(element, "BBIM_Array"): self.report({"INFO"}, "Elements that are part of an array cannot be deleted.") - return {"FINISHED"} + continue + if element.is_a("IfcGridAxis"): + # Deleting the last W axis is OK + if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or ( + (grid := element.PartOfV) and len(grid[0].VAxes) == 1 + ): + self.report( + {"INFO"}, "The last grid axis of a grid cannot be deleted. Delete the grid instead." + ) + continue + if tool.Drawing.is_auto_annotation(element): + self.report({"INFO"}, "References cannot be deleted. Exclude the referenced element instead.") + continue tool.Geometry.delete_ifc_object(obj) elif tool.Geometry.is_representation_item(obj): tool.Geometry.delete_ifc_item(obj) else: bpy.data.objects.remove(obj) + # Delete empty aggregates after deleting their parts + self.delete_empty_aggregates(aggregates_to_check) + for opening in tool.Model.get_model_props().openings: if opening.obj is not None and not tool.Ifc.get_entity(opening.obj): bpy.data.objects.remove(opening.obj) @@ -921,6 +940,49 @@ class OverrideDelete(bpy.types.Operator): data["old_file"].redo() tool.Ifc.set(data["new_file"]) + def track_aggregates(self, objects_to_remove): + """Track aggregates that contain objects being deleted""" + aggregates_to_check = set() + for obj in objects_to_remove: + if not tool.Blender.is_valid_data_block(obj): + continue + element = tool.Ifc.get_entity(obj) + if not element: + continue + aggregate = ifcopenshell.util.element.get_aggregate(element) + if aggregate: + aggregates_to_check.add(aggregate) + return aggregates_to_check + + def delete_empty_aggregates(self, aggregates_to_check): + """Delete aggregates that now have no parts""" + deleted_aggregates = [] + for aggregate in aggregates_to_check: + # Check if aggregate still exists (might have been deleted already) + try: + aggregate.id() + except: + continue + + related_objects = ifcopenshell.util.element.get_parts(aggregate) + if len(related_objects) == 0: + aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}" + deleted_aggregates.append(aggregate_name) + + aggregate_obj = tool.Ifc.get_object(aggregate) + if aggregate_obj and tool.Blender.is_valid_data_block(aggregate_obj): + tool.Geometry.delete_ifc_object(aggregate_obj) + + # Show info message if aggregates were deleted + if deleted_aggregates: + if len(deleted_aggregates) == 1: + self.report( + {"INFO"}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts" + ) + else: + aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates) + self.report({"INFO"}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts") + def process_arrays(self, context: bpy.types.Context) -> None: ifc_file = tool.Ifc.get() selected_objects = set(context.selected_objects) @@ -2973,6 +3035,7 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): item_mesh = bpy.data.meshes.new("tmp") tool.Ifc.link(item, item_mesh) item_obj = bpy.data.objects.new("tmp", item_mesh) + tool.Geometry.lock_scale(item_obj) tool.Geometry.name_item_object(item_obj, item) item_obj.matrix_world = obj.matrix_world bpy.context.collection.objects.link(item_obj) diff --git a/src/bonsai/bonsai/bim/module/light/data.py b/src/bonsai/bonsai/bim/module/light/data.py index 55bea350d2..abe9d9e87d 100644 --- a/src/bonsai/bonsai/bim/module/light/data.py +++ b/src/bonsai/bonsai/bim/module/light/data.py @@ -41,7 +41,7 @@ class SolarData: @classmethod def sun_position(cls): - return tool.Blender.get_sun_position_addon() + return tool.Blender.get_addon("sun_position") @classmethod def sites(cls): diff --git a/src/bonsai/bonsai/bim/module/light/operator.py b/src/bonsai/bonsai/bim/module/light/operator.py index f57ec3a4e0..4a8d993e0e 100644 --- a/src/bonsai/bonsai/bim/module/light/operator.py +++ b/src/bonsai/bonsai/bim/module/light/operator.py @@ -202,7 +202,7 @@ class RadianceRender(bpy.types.Operator): print(f"Camera position: {camera_position}") print(f"Camera direction: {camera_direction}") - # sun_position = tool.Blender.get_sun_position_addon() + # sun_position = tool.Blender.get_addon("sun_position") # azimuth, elevation = sun_position.sun_calc.get_sun_coordinates( # sun_pos_props.time, # sun_pos_props.latitude, diff --git a/src/bonsai/bonsai/bim/module/light/prop.py b/src/bonsai/bonsai/bim/module/light/prop.py index 54e5f6f73c..1cb36f82eb 100644 --- a/src/bonsai/bonsai/bim/module/light/prop.py +++ b/src/bonsai/bonsai/bim/module/light/prop.py @@ -40,7 +40,7 @@ from bpy.types import PropertyGroup from bonsai.bim.module.light.data import SolarData from bonsai.bim.module.light.decorator import SolarDecorator -sun_position = tool.Blender.get_sun_position_addon() +sun_position = tool.Blender.get_addon("sun_position") now = datetime.datetime.now() with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f: diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 7245dfb443..a286e3daee 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -373,7 +373,12 @@ class BIMStairProperties(PropertyGroup): if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0: self["nosing_length"] = 0 - non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type") + def update_custom_tread_lock(self, context: bpy.types.Context) -> None: + """When lock is enabled, sync custom treads with tread_run""" + if self.custom_tread_lock: + self["custom_first_last_tread_run"] = (self.tread_run, self.tread_run) + + non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type", "custom_tread_lock") is_editing: bpy.props.BoolProperty(default=False) width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE") @@ -407,6 +412,12 @@ class BIMStairProperties(PropertyGroup): default="CONCRETE", update=validate_nosing_value, ) + custom_tread_lock: bpy.props.BoolProperty( + name="Lock First/Last Treads to Tread Run", + description="When enabled, first and last treads automatically use the Tread Run value", + default=True, + update=update_custom_tread_lock, + ) custom_first_last_tread_run: bpy.props.FloatVectorProperty( name="Custom First / Last Treads Widths", description='Specify custom first / last treads widths, different from the general "Tread Run". Leave 0 to disable.', @@ -442,6 +453,7 @@ class BIMStairProperties(PropertyGroup): top_slab_depth: float has_top_nib: bool stair_type: str + custom_tread_lock: bool custom_first_last_tread_run: tuple[float, float] nosing_length: float nosing_depth: float @@ -480,8 +492,11 @@ class BIMStairProperties(PropertyGroup): } stair_kwargs.update(generic_props) - # defined here to appear last in UI - stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run + # If locked, use tread_run for both first and last treads + if self.custom_tread_lock: + stair_kwargs["custom_first_last_tread_run"] = (self.tread_run, self.tread_run) + else: + stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run if not convert_to_project_units: return stair_kwargs @@ -489,8 +504,24 @@ class BIMStairProperties(PropertyGroup): stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, self.non_si_units_props) return stair_kwargs + def get_props_kwargs_for_ifc_export(self, convert_to_project_units=False, stair_type=None): + """Get props including custom_tread_lock for saving to IFC""" + stair_kwargs = self.get_props_kwargs(convert_to_project_units, stair_type) + # Add the lock state for IFC storage (after getting base kwargs to avoid passing to generate function) + stair_kwargs["custom_tread_lock"] = self.custom_tread_lock + return stair_kwargs + def set_props_kwargs_from_ifc_data(self, kwargs): kwargs = tool.Model.convert_data_to_si_units(kwargs, self.non_si_units_props) + + # Determine lock state based on whether custom treads match tread_run + # If custom_tread_lock wasn't saved (old files), infer it from the data + if "custom_tread_lock" not in kwargs: + custom_treads = kwargs.get("custom_first_last_tread_run", (0.0, 0.0)) + tread_run = kwargs.get("tread_run", 0.3) + # Lock is off if either custom tread differs from tread_run and is not 0 + kwargs["custom_tread_lock"] = not any(ct != 0.0 and ct != tread_run for ct in custom_treads) + for prop_name in kwargs: setattr(self, prop_name, kwargs[prop_name]) diff --git a/src/bonsai/bonsai/bim/module/model/stair.py b/src/bonsai/bonsai/bim/module/model/stair.py index d2b5184c76..71bfa97a9a 100644 --- a/src/bonsai/bonsai/bim/module/model/stair.py +++ b/src/bonsai/bonsai/bim/module/model/stair.py @@ -188,7 +188,8 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator): props = tool.Model.get_stair_props(obj) ifc_file = tool.Ifc.get() - stair_data = props.get_props_kwargs(convert_to_project_units=True) + # Use the special method that includes custom_tread_lock for IFC storage + stair_data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True) pset = tool.Pset.get_element_pset(element, "BBIM_Stair") if not pset: pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="BBIM_Stair") @@ -241,7 +242,8 @@ class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator): assert element props = tool.Model.get_stair_props(obj) - data = props.get_props_kwargs(convert_to_project_units=True) + # Use the special method that includes custom_tread_lock for IFC storage + data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True) props.is_editing = False regenerate_stair_mesh(obj) tool.Model.add_body_representation(obj) diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index 0b17f6d9fb..1e1ff2cc2f 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -302,13 +302,38 @@ class BIM_PT_stair(bpy.types.Panel): row.operator("bim.cancel_editing_stair", icon="CANCEL", text="") row = self.layout.row(align=True) for prop_name in props.get_props_kwargs(): + # Skip custom_tread_lock as it's handled with custom_first_last_tread_run + if prop_name == "custom_tread_lock": + continue + prop_value = getattr(props, prop_name) - if isinstance(prop_value, Iterable) and not isinstance(prop_value, str): + + # Special handling for custom_first_last_tread_run + if prop_name == "custom_first_last_tread_run": + # Draw the lock toggle + row_lock = self.layout.row(align=True) + lock_text = ( + "Lock First/Last Treads" if not props.custom_tread_lock else "Unlock First/Last Treads" + ) + row_lock.prop( + props, + "custom_tread_lock", + text=lock_text, + icon="LOCKED" if props.custom_tread_lock else "UNLOCKED", + ) + + # Only show the custom values input if unlocked + if not props.custom_tread_lock: + prop_readable_name = props.bl_rna.properties[prop_name].name + self.layout.label(text=f"{prop_readable_name}:") + self.layout.prop(props, prop_name, text="") + elif isinstance(prop_value, Iterable) and not isinstance(prop_value, str): prop_readable_name = props.bl_rna.properties[prop_name].name self.layout.label(text=f"{prop_readable_name}:") self.layout.prop(props, prop_name, text="") else: self.layout.prop(props, prop_name) + if prop_name == "height": # Weak but we just want to insert this inside props drawing row_length = self.layout.row(align=True) row_length.prop(props, "total_length_target") diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 148937e577..2f5b9c906e 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2522,6 +2522,7 @@ class FlipClippingPlane(bpy.types.Operator): obj = context.active_object if obj in tool.Project.get_project_props().clipping_planes_objs: obj.rotation_euler[0] += radians(180) + obj.rotation_euler[0] %= radians(360) context.view_layer.update() return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index 5682c00c81..2cbdeb6e7b 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -450,9 +450,9 @@ class BIMProjectProperties(PropertyGroup): self.mvd = header_data.mvd self.author_name = header_data.author_name self.author_email = header_data.author_email - self.organisation_name = header_data.organization_name - self.organisation_email = header_data.organization_email - self.authorisation = header_data.authorization + self.organisation_name = header_data.organisation_name + self.organisation_email = header_data.organisation_email + self.authorisation = header_data.authorisation if TYPE_CHECKING: is_editing: bool diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 75d114e204..03b4222008 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -653,4 +653,5 @@ class BIM_PT_purge(Panel): row = layout.row(align=True) row.label(text=f"{object_type.replace('_', ' ').capitalize()}:") row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = object_type - row.operator("bim.merge_identical_objects", text="Merge Identical").object_type = object_type + merge_op = row.operator("bim.merge_identical_objects", text="Merge Identical") + merge_op.object_type = object_type diff --git a/src/bonsai/bonsai/bim/module/pset/ui.py b/src/bonsai/bonsai/bim/module/pset/ui.py index ab11c5f1ce..abd3a8b90f 100644 --- a/src/bonsai/bonsai/bim/module/pset/ui.py +++ b/src/bonsai/bonsai/bim/module/pset/ui.py @@ -242,14 +242,11 @@ class BIM_PT_object_psets(Panel): @classmethod def poll(cls, context): - if not (obj := context.active_object): - return False - ifc_id = tool.Blender.get_ifc_definition_id(obj) - if not ifc_id: - return False - if not tool.Ifc.get_object_by_identifier(ifc_id): - return False - return True + return ( + (obj := tool.Blender.get_active_object()) + and (element := tool.Ifc.get_entity(obj)) + and element.is_a("IfcObjectDefinition") + ) def draw(self, context): if not ObjectPsetsData.is_loaded: @@ -388,14 +385,11 @@ class BIM_PT_object_qtos(Panel): @classmethod def poll(cls, context): - if not (obj := context.active_object): - return False - ifc_id = tool.Blender.get_ifc_definition_id(obj) - if not ifc_id: - return False - if not tool.Ifc.get_object_by_identifier(ifc_id): - return False - return True + return ( + (obj := tool.Blender.get_active_object()) + and (element := tool.Ifc.get_entity(obj)) + and element.is_a("IfcObjectDefinition") + ) def draw(self, context): if not ObjectQtosData.is_loaded: diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index 55ffa56a79..a5bf766ae2 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -317,6 +317,7 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator): f"Mesh '{obj.data.name}' has loose geometry, loose geometry will be ignored to save mesh to IFC as a tessellation.", ) + representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context) element = core.assign_class( tool.Ifc, tool.Collector, @@ -326,13 +327,9 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator): predefined_type=predefined_type, should_add_representation=False, ) - representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context) ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation) bonsai.core.geometry.switch_representation( - tool.Ifc, - tool.Geometry, - obj=obj, - representation=representation, + tool.Ifc, tool.Geometry, obj=obj, representation=representation ) else: diff --git a/src/bonsai/bonsai/bim/module/search/data.py b/src/bonsai/bonsai/bim/module/search/data.py index 34f032c2b5..4bde0aa359 100644 --- a/src/bonsai/bonsai/bim/module/search/data.py +++ b/src/bonsai/bonsai/bim/module/search/data.py @@ -101,7 +101,7 @@ class ColourByPropertyData: elif pset.endswith("BaseQuantities"): keys.extend([f'/.*BaseQuantities/."{name}"' for name in properties.keys() if name != "id"]) else: - keys.extend([f"{pset}.{name}" for name in properties.keys() if name != "id"]) + keys.extend([f'"{pset}"."{name}"' for name in properties.keys() if name != "id"]) results = [(k, k, "") for k in keys] return default + results diff --git a/src/bonsai/bonsai/bim/module/sequence/__init__.py b/src/bonsai/bonsai/bim/module/sequence/__init__.py index 40a7e1fb3a..3262b64527 100644 --- a/src/bonsai/bonsai/bim/module/sequence/__init__.py +++ b/src/bonsai/bonsai/bim/module/sequence/__init__.py @@ -105,6 +105,7 @@ classes = ( operator.RemoveWorkTime, operator.ReorderTask, operator.SaveAnimationColorScheme, + operator.SelectTaskElements, operator.SelectTaskRelatedInputs, operator.SelectTaskRelatedProducts, operator.SelectUnassignedWorkScheduleProducts, diff --git a/src/bonsai/bonsai/bim/module/sequence/data.py b/src/bonsai/bonsai/bim/module/sequence/data.py index b011d48e15..94df71eacd 100644 --- a/src/bonsai/bonsai/bim/module/sequence/data.py +++ b/src/bonsai/bonsai/bim/module/sequence/data.py @@ -243,6 +243,9 @@ class SequenceData: data["NestingIndex"] = None for rel in task.Nests or []: data["NestingIndex"] = rel.RelatedObjects.index(task) + data["TotalInputs"] = len(tool.Sequence.get_task_inputs(task)) + data["TotalOutputs"] = len(tool.Sequence.get_task_outputs(task)) + data["TotalElements"] = data["TotalInputs"] + data["TotalOutputs"] cls.data["tasks"][task.id()] = data @classmethod diff --git a/src/bonsai/bonsai/bim/module/sequence/operator.py b/src/bonsai/bonsai/bim/module/sequence/operator.py index 1dacf6efb7..c07b9a0496 100644 --- a/src/bonsai/bonsai/bim/module/sequence/operator.py +++ b/src/bonsai/bonsai/bim/module/sequence/operator.py @@ -1281,6 +1281,17 @@ class DisableEditingSequence(bpy.types.Operator): return {"FINISHED"} +class SelectTaskElements(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.select_task_elements" + bl_label = "Select All Task Elements" + bl_options = {"REGISTER", "UNDO"} + task: bpy.props.IntProperty() + + def _execute(self, context): + core.select_task_inputs(tool.Sequence, tool.Spatial, task=tool.Ifc.get().by_id(self.task)) + core.select_task_outputs(tool.Sequence, tool.Spatial, task=tool.Ifc.get().by_id(self.task)) + + class SelectTaskRelatedProducts(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.select_task_related_products" bl_label = "Select All Output Products" diff --git a/src/bonsai/bonsai/bim/module/sequence/prop.py b/src/bonsai/bonsai/bim/module/sequence/prop.py index 86b5a1966d..1c35910e6c 100644 --- a/src/bonsai/bonsai/bim/module/sequence/prop.py +++ b/src/bonsai/bonsai/bim/module/sequence/prop.py @@ -75,6 +75,8 @@ def update_active_task_index(self, context): bonsai.bim.module.pset.data.refresh() if self.editing_task_type == "SEQUENCE": tool.Sequence.load_task_properties() + if self.should_auto_select: + tool.Sequence.select_active_task_elements(task) def update_active_task_outputs(self, context): @@ -485,6 +487,9 @@ class BIMWorkScheduleProperties(PropertyGroup): other_columns: EnumProperty( items=[ ("Controls.Calendar", "Calendar", ""), + ("Controls.TotalElements", "Total Elements", "The total number of assigned elements to this task"), + ("Controls.TotalInputs", "Total Inputs", "The total number of input elements to this task"), + ("Controls.TotalOutputs", "Total Outputs", "The total number of input elements to this task"), ], name="Special Columns", ) @@ -518,9 +523,13 @@ class BIMWorkScheduleProperties(PropertyGroup): active_task_input_index: IntProperty(name="Active Task Input Index") task_outputs: CollectionProperty(name="Task Outputs", type=TaskProduct) active_task_output_index: IntProperty(name="Active Task Output Index") - show_nested_outputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_outputs) - show_nested_resources: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_resources) - show_nested_inputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_inputs) + show_nested_outputs: BoolProperty( + name="Show Nested Task Elements", default=False, update=update_active_task_outputs + ) + show_nested_resources: BoolProperty( + name="Show Nested Task Elements", default=False, update=update_active_task_resources + ) + show_nested_inputs: BoolProperty(name="Show Nested Task Elements", default=False, update=update_active_task_inputs) product_input_tasks: CollectionProperty(name="Product Task Inputs", type=TaskProduct) product_output_tasks: CollectionProperty(name="Product Task Outputs", type=TaskProduct) active_product_output_task_index: IntProperty(name="Active Product Output Task Index") @@ -531,6 +540,9 @@ class BIMWorkScheduleProperties(PropertyGroup): filter_by_active_schedule: BoolProperty( name="Filter By Active Schedule", default=False, update=update_filter_by_active_schedule ) + should_auto_select: BoolProperty( + name="Auto Select", description="Auto select elements when task is selected", default=False + ) if TYPE_CHECKING: work_schedule_predefined_types: str diff --git a/src/bonsai/bonsai/bim/module/sequence/ui.py b/src/bonsai/bonsai/bim/module/sequence/ui.py index dc31471af3..9178268c3a 100644 --- a/src/bonsai/bonsai/bim/module/sequence/ui.py +++ b/src/bonsai/bonsai/bim/module/sequence/ui.py @@ -290,13 +290,21 @@ class BIM_PT_work_schedules(Panel): row2 = self.layout.row(align=True) row2.alignment = "RIGHT" - row2.prop(self.props, "enable_reorder", text="", icon="SORTALPHA") - row2.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING") - row2.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id - row2.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = ( + col = row2.column() + row_ = col.row(align=True) + op = row_.operator("bim.select_task_elements", icon="RESTRICT_SELECT_OFF", text="Select Elements") + op.task = task.ifc_definition_id + row_.prop(self.props, "should_auto_select", icon="OBJECT_HIDDEN", text="") + + col = row2.column() + row_ = col.row(align=True) + row_.prop(self.props, "enable_reorder", text="", icon="SORTALPHA") + row_.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING") + row_.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id + row_.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = ( ifc_definition_id ) - row2.operator("bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL").task = ( + row_.operator("bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL").task = ( ifc_definition_id ) row.operator("bim.add_task", text="Add", icon="ADD").task = ifc_definition_id @@ -716,11 +724,10 @@ class BIM_PT_task_icom(Panel): input_id = self.props.task_inputs[self.props.active_task_input_index].ifc_definition_id op.related_object = input_id - op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="Select") + op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="") op.task = task.ifc_definition_id + row2.prop(self.props, "show_nested_inputs", icon="OUTLINER", text="") - row2 = col.row() - row2.prop(self.props, "show_nested_inputs", text="Show Nested") row2 = col.row() row2.template_list("BIM_UL_task_inputs", "", self.props, "task_inputs", self.props, "active_task_input_index") @@ -744,8 +751,7 @@ class BIM_PT_task_icom(Panel): op.related_object_type = "RESOURCE" op.resource = self.props.task_resources[self.props.active_task_resource_index].ifc_definition_id - row2 = col.row() - row2.prop(self.props, "show_nested_resources", text="Show Nested") + row2.prop(self.props, "show_nested_resources", icon="OUTLINER", text="") row2 = col.row() row2.template_list( @@ -773,10 +779,10 @@ class BIM_PT_task_icom(Panel): output_id = self.props.task_outputs[self.props.active_task_output_index].ifc_definition_id op.relating_product = output_id - op = row2.operator("bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="Select") + op = row2.operator("bim.select_task_related_products", icon="RESTRICT_SELECT_OFF", text="") op.task = task.ifc_definition_id - row2 = col.row() - row2.prop(self.props, "show_nested_outputs", text="Show Nested") + row2.prop(self.props, "show_nested_outputs", icon="OUTLINER", text="") + row2 = col.row() row2.template_list( "BIM_UL_task_outputs", "", self.props, "task_outputs", self.props, "active_task_output_index" @@ -893,7 +899,7 @@ class BIM_UL_tasks(UIList): split2 = split1.split(factor=0.9 - min(0.5, 0.15 * len(self.props.columns))) split2.prop(item, "name", emboss=False, text="") - BIM_UL_tasks.draw_custom_columns(self.props, split2, item, task) + BIM_UL_tasks.draw_custom_columns(self.props, split2, item, task, item.ifc_definition_id) if self.props.active_task_id and self.props.editing_task_type == "ATTRIBUTES": row.prop( @@ -960,6 +966,7 @@ class BIM_UL_tasks(UIList): row: bpy.types.UILayout, item: Optional[bpy.types.PropertyGroup] = None, task: Optional[dict[str, Any]] = None, + ifc_definition_id: int = 0, *, header: bool = False, ) -> None: @@ -999,6 +1006,21 @@ class BIM_UL_tasks(UIList): row.label(text=item.derived_calendar + "*") else: row.label(text=item.calendar or "-") + elif column.name == "Controls.TotalInputs": + if header: + row.label(text="# Inputs") + else: + row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalInputs"])) + elif column.name == "Controls.TotalOutputs": + if header: + row.label(text="# Outputs") + else: + row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalOutputs"])) + elif column.name == "Controls.TotalElements": + if header: + row.label(text="# Elements") + else: + row.label(text=str(SequenceData.data["tasks"][ifc_definition_id]["TotalElements"])) else: ifc_class, name = column.name.split(".") if header: diff --git a/src/bonsai/bonsai/bim/module/spatial/decorator.py b/src/bonsai/bonsai/bim/module/spatial/decorator.py index b8781133b0..824fafec1f 100644 --- a/src/bonsai/bonsai/bim/module/spatial/decorator.py +++ b/src/bonsai/bonsai/bim/module/spatial/decorator.py @@ -70,7 +70,7 @@ class GridDecorator: grid_props = tool.Spatial.get_grid_props() for axis in grid_props.grid_axes: - if not (obj := axis.obj) or obj.hide_get() == True: + if not (obj := axis.obj) or obj.visible_get() is False: continue if obj.select_get() and context.mode != "OBJECT": continue @@ -125,21 +125,22 @@ class GridDecorator: unselected_edges = [] grid_props = tool.Spatial.get_grid_props() for axis in grid_props.grid_axes: - if (obj := axis.obj) and obj.hide_get() == False: - if obj.select_get(): - if context.mode != "OBJECT": - continue - edges = selected_edges - verts = selected_verts - else: - edges = unselected_edges - verts = unselected_verts - i = len(verts) - edges.append([i, i + 1]) - matrix_world = obj.matrix_world - v1 = matrix_world @ obj.data.vertices[0].co - v2 = matrix_world @ obj.data.vertices[1].co - verts.extend([v1, v2]) + if not (obj := axis.obj) or obj.visible_get() is False: + continue + if obj.select_get(): + if context.mode != "OBJECT": + continue + edges = selected_edges + verts = selected_verts + else: + edges = unselected_edges + verts = unselected_verts + i = len(verts) + edges.append([i, i + 1]) + matrix_world = obj.matrix_world + v1 = matrix_world @ obj.data.vertices[0].co + v2 = matrix_world @ obj.data.vertices[1].co + verts.extend([v1, v2]) if unselected_verts: self.draw_batch("LINES", unselected_verts, decorator_color_background, unselected_edges) diff --git a/src/bonsai/bonsai/bim/module/type/ui.py b/src/bonsai/bonsai/bim/module/type/ui.py index dd08278bc9..f749ceb07e 100644 --- a/src/bonsai/bonsai/bim/module/type/ui.py +++ b/src/bonsai/bonsai/bim/module/type/ui.py @@ -34,14 +34,11 @@ class BIM_PT_type(Panel): @classmethod def poll(cls, context): - if not context.active_object: - return False - element = tool.Ifc.get_entity(context.active_object) - if not element: - return False - if not element.is_a("IfcProduct") and not element.is_a("IfcTypeProduct"): - return True - return True + return ( + (obj := tool.Blender.get_active_object()) + and (element := tool.Ifc.get_entity(obj)) + and (element.is_a("IfcObject") or element.is_a("IfcTypeObject")) + ) def draw(self, context): if not TypeData.is_loaded: diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py index 0345f4019a..2e2d07c96c 100644 --- a/src/bonsai/bonsai/core/drawing.py +++ b/src/bonsai/bonsai/core/drawing.py @@ -108,10 +108,6 @@ def add_sheet(ifc: type[tool.Ifc], drawing: type[tool.Drawing], titleblock: ifco def regenerate_sheet( drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance ) -> Union[list[tool.Drawing.SheetWarningType], None]: - warnings = drawing.validate_sheet_files(sheet) - if warnings: - return warnings - titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK") assert titleblock_uri @@ -320,6 +316,7 @@ def duplicate_drawing( ) -> ifcopenshell.entity_instance: drawing_name = drawing_tool.ensure_unique_drawing_name(drawing_tool.get_name(drawing)) new_drawing = ifc.run("root.copy_class", product=drawing) + drawing_tool.clear_annotation_relationships(new_drawing) drawing_tool.copy_representation(drawing, new_drawing) drawing_tool.set_name(new_drawing, drawing_name) group = drawing_tool.get_drawing_group(new_drawing) @@ -475,46 +472,40 @@ def sync_references( if not drawing_tool.has_annotation(drawing): return - context = drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing)) - if not context: + if not (context := drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))): return group = drawing_tool.get_drawing_group(drawing) - for reference_element in drawing_tool.get_potential_reference_elements(drawing): + potential_reference_elements = drawing_tool.get_potential_reference_elements(drawing) + + for element in potential_reference_elements: + if (obj := ifc.get_object(element)) and ifc.is_moved(obj): + drawing_tool.sync_object_placement(obj) + + for element in drawing_tool.get_group_elements(group): + if not drawing_tool.is_auto_annotation(element): + 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) reference_obj = ifc.get_object(reference_element) - annotation = drawing_tool.get_drawing_reference_annotation(drawing, reference_element) + if reference_element not in potential_reference_elements: + # It was auto created, so it makes sense to auto delete + if obj := ifc.get_object(element): + drawing_tool.delete_object(obj) + ifc.run("root.remove_product", product=element) + elif not drawing_tool.regenerate_reference_annotation(drawing, element, reference_element, context): + if obj := ifc.get_object(element): + drawing_tool.delete_object(obj) + ifc.run("root.remove_product", product=element) - should_delete_existing_annotation = False - should_create_annotation = False - - # remove annotation only if the reference object was changed - # otherwise we rely on the existing annotation - if annotation: - if reference_obj and (ifc.is_moved(reference_obj) or ifc.is_edited(reference_obj)): - should_delete_existing_annotation = True - - if should_delete_existing_annotation or not annotation: - should_create_annotation = True - - if should_delete_existing_annotation: - annotation_obj = ifc.get_object(annotation) - if annotation_obj: - drawing_tool.delete_object(annotation_obj) - ifc.run("root.remove_product", product=annotation) - - if should_create_annotation: - annotation = drawing_tool.generate_reference_annotation(drawing, reference_element, context) - if annotation: + for reference_element in potential_reference_elements: + if not drawing_tool.get_drawing_reference_annotation(drawing, reference_element): + if annotation := drawing_tool.generate_reference_annotation(drawing, reference_element, context): ifc.run("drawing.assign_product", relating_product=reference_element, related_object=annotation) ifc.run("group.assign_group", group=group, products=[annotation]) collector.assign(ifc.get_object(annotation)) - if reference_obj and ifc.is_moved(reference_obj): - drawing_tool.sync_object_placement(reference_obj) - - if reference_obj and ifc.is_edited(reference_obj): - drawing_tool.sync_object_representation(reference_obj) - def select_assigned_product(drawing: type[tool.Drawing], context: bpy.types.Context) -> None: drawing.select_assigned_product(context) diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index b9b3ae0d49..ee54a2a7fc 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1506,28 +1506,21 @@ class Blender(bonsai.core.tool.Blender): return bpy.context.preferences.addons[blender_package_name].preferences @classmethod - def get_sun_position_addon(cls) -> Union[types.ModuleType, None]: - # Check if it's installed as legacy Blender addon. + def get_addon(cls, name: str) -> Union[types.ModuleType, None]: import importlib try: - sun_position = importlib.import_module("sun_position") + return importlib.import_module(name) # Legacy Blender addon except ImportError: - sun_position = None - - if sun_position: - return sun_position + pass for package_name in bpy.context.preferences.addons.keys(): - if package_name.endswith(".sun_position"): + if package_name.endswith(f".{name}"): try: - sun_position = importlib.import_module(package_name) - return sun_position + return importlib.import_module(package_name) except ModuleNotFoundError: pass - return sun_position - @classmethod def get_sun_props(cls) -> Union[SunPosProperties, None]: assert (scene := bpy.context.scene) @@ -1889,6 +1882,24 @@ class Blender(bonsai.core.tool.Blender): obj.select_set(True) bpy.context.view_layer.objects.active = previously_active + @classmethod + def sync_render_visibility(cls): + # Doing bpy.ops.object.hide_render_clear_all() or + # bpy.ops.object.isolate_type_render() is extremely slow. + # Hopefully this doesn't crash on Windows, it doesn't crash on Linux. + should_hides = [0 if obj.visible_get() else 1 for obj in bpy.data.objects] + should_hides = np.fromiter(should_hides, dtype=np.uint8, count=len(should_hides)) + bpy.data.objects.foreach_set("hide_render", should_hides) + return # Otherwise... + # for obj in bpy.data.objects: + # if not obj.data: + # continue + # # For speed, check equality prior to change to prevent needless updates + # if (is_visible := obj.visible_get()) and obj.hide_render is True: + # obj.hide_render = False + # elif not is_visible and obj.hide_render is False: + # obj.hide_render = True + @classmethod def hide_objects(cls, objs): previously_selected = {o.name for o in bpy.context.selected_objects} diff --git a/src/bonsai/bonsai/tool/collector.py b/src/bonsai/bonsai/tool/collector.py index 34f471ada4..65d5ae49bd 100644 --- a/src/bonsai/bonsai/tool/collector.py +++ b/src/bonsai/bonsai/tool/collector.py @@ -51,9 +51,13 @@ class Collector(bonsai.core.tool.Collector): if tool.Geometry.is_locked(element): tool.Geometry.lock_object(obj) element = (element.PartOfU or element.PartOfV or element.PartOfW)[0] + if not tool.Spatial.get_grid_props().is_visible: + obj.hide_viewport = True elif element.is_a("IfcGrid"): if tool.Geometry.is_locked(element): tool.Geometry.lock_object(obj) + if not tool.Spatial.get_grid_props().is_visible: + obj.hide_viewport = True if element.is_a("IfcProject"): if tool.Geometry.is_locked(element): @@ -69,7 +73,8 @@ class Collector(bonsai.core.tool.Collector): tool.Geometry.lock_object(obj) collection = cls._create_project_child_collection("IfcSpace") cls.link_collection_object_safe(collection, obj) - obj.hide_viewport = True + if not tool.Spatial.get_spatial_props().is_visible: + obj.hide_viewport = True elif element.is_a("IfcStructuralItem"): collection = cls._create_project_child_collection("IfcStructuralItem") cls.link_collection_object_safe(collection, obj) @@ -93,7 +98,8 @@ class Collector(bonsai.core.tool.Collector): cls.link_collection_object_safe(collection, obj) project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection) - obj.hide_viewport = True + if not tool.Spatial.get_spatial_props().is_visible: + obj.hide_viewport = True elif ( tool.Ifc.get_schema() != "IFC2X3" and element.is_a("IfcSpatialElement") @@ -105,7 +111,8 @@ class Collector(bonsai.core.tool.Collector): cls.link_collection_object_safe(collection, obj) project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection) - obj.hide_viewport = True + if not tool.Spatial.get_spatial_props().is_visible: + obj.hide_viewport = True elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING": if collection := cls._create_own_collection(obj): cls.link_collection_object_safe(collection, obj) diff --git a/src/bonsai/bonsai/tool/cost.py b/src/bonsai/bonsai/tool/cost.py index c65b2a29e2..d1ee2a841c 100644 --- a/src/bonsai/bonsai/tool/cost.py +++ b/src/bonsai/bonsai/tool/cost.py @@ -273,18 +273,16 @@ class Cost(bonsai.core.tool.Cost): return props = cls.get_cost_props() props.cost_item_type_products.clear() - # TODO implement process and resource types - # props.cost_item_processes.clear() - # props.cost_item_resources.clear() + props.cost_item_processes.clear() + props.cost_item_resources.clear() for rel in cost_item.Controls or []: for related_object in rel.RelatedObjects: if related_object.is_a("IfcTypeProduct"): new = props.cost_item_type_products.add() - # TODO implement process and resource types - # elif related_object.is_a("IfcProcess"): - # new = props.cost_item_processes.add() - # elif related_object.is_a("IfcResource"): - # new = props.cost_item_resources.add() + elif related_object.is_a("IfcProcess"): + new = props.cost_item_processes.add() + elif related_object.is_a("IfcResource"): + new = props.cost_item_resources.add() new.ifc_definition_id = related_object.id() new.name = related_object.Name or "Unnamed" diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py index 079892a9b5..35e877e734 100644 --- a/src/bonsai/bonsai/tool/debug.py +++ b/src/bonsai/bonsai/tool/debug.py @@ -18,6 +18,7 @@ from __future__ import annotations import os +import re import json import bmesh import bpy @@ -33,7 +34,7 @@ import bonsai.tool as tool from bonsai.bim.ifc import IfcStore from mathutils import Vector from collections import defaultdict -from typing import Literal, TYPE_CHECKING, assert_never +from typing import Literal, TYPE_CHECKING, assert_never, Union from collections.abc import Iterable if TYPE_CHECKING: @@ -136,12 +137,30 @@ class Debug(bonsai.core.tool.Debug): "PERSON", "PERSON_AND_ORGANIZATION", ], + by_name_or_identification_only: bool = False, ) -> dict[str, list[str]]: """Merge identical objects. Note that Styles UI (or other UI) should be updated manually after using this method. + + Args: + object_type: The type of object to merge + by_name_or_identification_only: If True, merge based only on Name attribute (or equivalent identifier). + Strips .XXX suffix patterns (e.g., 'foo.001' matches 'foo', 'foo.002'). + For PERSON, uses Identification. For APPLICATION, uses ApplicationFullName. + For PERSON_AND_ORGANIZATION, uses combination of person and organization identifiers. """ + def normalize_name(name: str) -> str: + """Remove .XXX suffix pattern from names (e.g., 'foo.001' -> 'foo')""" + if not name: + return "" + # Match pattern: name ending with .digits + match = re.match(r"^(.+)\.\d+$", name) + if match: + return match.group(1) + return name + def get_hash(element: ifcopenshell.entity_instance) -> int: data = element.get_info_2(include_identifier=False, recursive=True) if object_type == "APPLICATION": @@ -152,6 +171,49 @@ class Debug(bonsai.core.tool.Debug): data["TheOrganization"] = element.TheOrganization.id() return hash(json.dumps(data, sort_keys=True)) + def get_name_key(element: ifcopenshell.entity_instance) -> str: + """Get key based on name/identifier attribute for the given object type""" + if object_type == "STYLE": + name = element.Name if element.Name else "" + return normalize_name(name) + elif object_type == "MATERIAL": + name = element.Name if element.Name else "" + return normalize_name(name) + elif object_type == "ORGANIZATION": + name = element.Name if element.Name else "" + return normalize_name(name) + elif object_type == "APPLICATION": + name = element.ApplicationFullName if element.ApplicationFullName else "" + return normalize_name(name) + elif object_type == "PERSON": + ident = element.Identification if element.Identification else "" + return normalize_name(ident) + elif object_type == "PERSON_AND_ORGANIZATION": + person_id = element.ThePerson.Identification if element.ThePerson.Identification else "" + org_name = element.TheOrganization.Name if element.TheOrganization.Name else "" + return f"{normalize_name(person_id)}|{normalize_name(org_name)}" + else: + assert_never(object_type) + + def get_element_name(element: ifcopenshell.entity_instance) -> str: + """Get the actual name/identifier from element for sorting purposes""" + if object_type == "STYLE": + return element.Name if element.Name else "" + elif object_type == "MATERIAL": + return element.Name if element.Name else "" + elif object_type == "ORGANIZATION": + return element.Name if element.Name else "" + elif object_type == "APPLICATION": + return element.ApplicationFullName if element.ApplicationFullName else "" + elif object_type == "PERSON": + return element.Identification if element.Identification else "" + elif object_type == "PERSON_AND_ORGANIZATION": + person_id = element.ThePerson.Identification if element.ThePerson.Identification else "" + org_name = element.TheOrganization.Name if element.TheOrganization.Name else "" + return f"{person_id}|{org_name}" + else: + assert_never(object_type) + ifc_file = tool.Ifc.get() merged_element_types: dict[str, list[str]] = {} @@ -179,22 +241,47 @@ class Debug(bonsai.core.tool.Debug): for element_type in element_types: elements = ifc_file.by_type(element_type, include_subtypes=False) - # Calculate hashes. - hash_to_elements: defaultdict[int, list[ifcopenshell.entity_instance]] = defaultdict(list) - for element in elements: - # Except for styles, ignore unnamed elements as they may be not safe to merge - merge_optional_names = ("STYLE", "PERSON") - not_optional_name = ("APPLICATION", "ORGANIZATION") - has_no_name = ("PERSON_AND_ORGANIZATION",) - if ( - object_type not in merge_optional_names - and object_type not in not_optional_name - and object_type not in has_no_name - and not element.Name - ): - continue - element_hash = get_hash(element) - hash_to_elements[element_hash].append(element) + # Calculate hashes or name keys. + hash_to_elements: defaultdict[Union[int, str], list[ifcopenshell.entity_instance]] + + if by_name_or_identification_only: + # Group by name/identifier only (with .XXX suffix normalization) + hash_to_elements = defaultdict(list) + for element in elements: + name_key = get_name_key(element) + # Skip elements without a valid identifier + if not name_key: + continue + hash_to_elements[name_key].append(element) + + # Sort elements within each group to keep the one without suffix (or lowest suffix) + for name_key in hash_to_elements: + # Sort by: 1) prefer names without .XXX suffix, 2) then by original name + def sort_key(el): + name = get_element_name(el) + # Check if name has .XXX suffix + has_suffix = bool(re.match(r"^.+\.\d+$", name)) + # Return tuple: (has_suffix, name) - sort by no suffix first, then alphabetically + return (has_suffix, name) + + hash_to_elements[name_key].sort(key=sort_key) + else: + # Group by full hash + hash_to_elements = defaultdict(list) + for element in elements: + # Except for styles, ignore unnamed elements as they may be not safe to merge + merge_optional_names = ("STYLE", "PERSON") + not_optional_name = ("APPLICATION", "ORGANIZATION") + has_no_name = ("PERSON_AND_ORGANIZATION",) + if ( + object_type not in merge_optional_names + and object_type not in not_optional_name + and object_type not in has_no_name + and not element.Name + ): + continue + element_hash = get_hash(element) + hash_to_elements[element_hash].append(element) merged_elements_names: list[str] = [] # Merge elements. diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 336b3fd0dd..a53272b408 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -208,6 +208,27 @@ class Drawing(bonsai.core.tool.Drawing): return obj + @classmethod + def get_annotation_drawing(cls, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance | None: + for rel in element.HasAssignments: + if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING": + for e in rel.RelatedObjects: + if e.ObjectType == "DRAWING": + return e + + @classmethod + def exclude_annotation_from_drawing( + cls, element: ifcopenshell.entity_instance, drawing: ifcopenshell.entity_instance + ) -> None: + pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing") + if not pset: + pset = ifcopenshell.api.pset.add_pset(ifc_file, product=drawing, name="EPset_Drawing") + exclude = ifcopenshell.util.element.get_property_definition(pset, prop="Exclude") or "" + if exclude: + exclude += "+" + exclude += element.GlobalId + ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Exclude": exclude}) + @classmethod def ensure_annotation_in_drawing_plane( cls, obj: bpy.types.Object, camera: Optional[bpy.types.Object] = None @@ -218,11 +239,7 @@ class Drawing(bonsai.core.tool.Drawing): entity = tool.Ifc.get_entity(obj) if not entity: return - for rel in entity.HasAssignments: - if rel.is_a("IfcRelAssignsToGroup"): - for e in rel.RelatedObjects: - if e.ObjectType == "DRAWING": - return tool.Ifc.get_object(e) + return tool.Ifc.get_object(cls.get_annotation_drawing(entity)) if not camera: camera = get_camera_from_annotation_object(obj) or bpy.context.scene.camera @@ -236,7 +253,11 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def setup_annotation_object( - cls, obj: bpy.types.Object, object_type: str, related_object: Optional[bpy.types.Object] = None + cls, + obj: bpy.types.Object, + object_type: str, + related_object: Optional[bpy.types.Object] = None, + rotation_mode: str = "NONE", ) -> None: """Finish object's adjustments after both object and entity are created""" @@ -308,6 +329,55 @@ class Drawing(bonsai.core.tool.Drawing): ifc_file, relating_product=related_entity, related_object=obj_entity ) + if rotation_mode != "NONE" and related_object: + cls.apply_annotation_rotation(obj, related_object, rotation_mode) + + @classmethod + def apply_annotation_rotation( + cls, tag_obj: bpy.types.Object, related_object: bpy.types.Object, rotation_mode: str + ) -> None: + """Apply rotation to annotation based on the selected rotation mode""" + camera = bpy.context.scene.camera + camera_right = camera.matrix_world.to_3x3() @ mathutils.Vector((1, 0, 0)) + + if rotation_mode == "CAMERA_Horizontal": + location = tag_obj.location.copy() + camera_matrix = camera.matrix_world.copy() + camera_matrix.translation = location + tag_obj.matrix_world = camera_matrix + + elif rotation_mode == "CAMERA_Vertical": + location = tag_obj.location.copy() + camera_matrix = camera.matrix_world.copy() + camera_matrix.translation = location + rotation_90z = mathutils.Matrix.Rotation(math.pi / 2, 4, "Z") + camera_matrix = camera_matrix @ rotation_90z + tag_obj.matrix_world = camera_matrix + + elif rotation_mode == "LOCAL_X": + local_x = related_object.matrix_world.to_3x3() @ mathutils.Vector((1, 0, 0)) + local_x = local_x.normalized() + if local_x.dot(camera_right) < 0: + local_x = -local_x + + tag_obj.rotation_euler = local_x.to_track_quat("X", "Z").to_euler() + + elif rotation_mode == "LOCAL_Y": + local_y = related_object.matrix_world.to_3x3() @ mathutils.Vector((0, 1, 0)) + local_y = local_y.normalized() + if local_y.dot(camera_right) < 0: + local_y = -local_y + + tag_obj.rotation_euler = local_y.to_track_quat("X", "Z").to_euler() + + elif rotation_mode == "LOCAL_Z": + local_z = related_object.matrix_world.to_3x3() @ mathutils.Vector((0, 0, 1)) + local_z = local_z.normalized() + if local_z.dot(camera_right) < 0: + local_z = -local_z + + tag_obj.rotation_euler = local_z.to_track_quat("X", "Z").to_euler() + @classmethod def is_annotation_object_type( cls, element: ifcopenshell.entity_instance, object_types: Union[str, Sequence[str]] @@ -815,7 +885,13 @@ class Drawing(bonsai.core.tool.Drawing): def get_assigned_product(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: for rel in element.HasAssignments: if rel.is_a("IfcRelAssignsToProduct"): - return rel.RelatingProduct + product = rel.RelatingProduct + if product.is_a("IfcGrid") and rel.Name: + for attribute in ("UAxes", "VAxes", "WAxes"): + for axis in getattr(product, attribute) or []: + if axis.AxisTag == rel.Name: + return axis + return product @classmethod def import_annotations_in_group(cls, group: ifcopenshell.entity_instance) -> None: @@ -1401,7 +1477,10 @@ class Drawing(bonsai.core.tool.Drawing): cls, drawing: ifcopenshell.entity_instance ) -> list[ifcopenshell.entity_instance]: elements = [] - existing_references = cls.get_group_elements(cls.get_drawing_group(drawing)) + existing_references = set(cls.get_group_elements(cls.get_drawing_group(drawing))) + if exclude := ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "Exclude"): + existing_references.update(ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), exclude)) + for element in tool.Ifc.get().by_type("IfcAnnotation"): if element in existing_references or element == drawing: continue @@ -1411,14 +1490,25 @@ class Drawing(bonsai.core.tool.Drawing): "GlobalReferencing", False ): elements.append(element) - for element in tool.Ifc.get().by_type("IfcGridAxis"): - elements.append(element) + for element in tool.Ifc.get().by_type("IfcGrid"): + if element in existing_references: + continue + for axis in element.UAxes + element.VAxes + (element.WAxes or tuple()): + if axis in existing_references: + continue + elements.append(axis) target_view = tool.Drawing.get_drawing_target_view(drawing) if target_view in ("SECTION_VIEW", "ELEVATION_VIEW"): for element in tool.Ifc.get().by_type("IfcBuildingStorey"): + if element in existing_references: + continue elements.append(element) return elements + @classmethod + def is_auto_annotation(cls, element: ifcopenshell.entity_instance): + return element.is_a("IfcAnnotation") and element.ObjectType in ("GRID", "SECTION", "ELEVATION", "SECTION_LEVEL") + @classmethod def get_drawing_reference_annotation( cls, drawing: ifcopenshell.entity_instance, reference_element: ifcopenshell.entity_instance @@ -1430,7 +1520,7 @@ class Drawing(bonsai.core.tool.Drawing): # IfcRelAssignsToProduct.RelatingProduct = IfcGrid # IfcRelAssignsToProduct.Name = IfcGridAxis.AxisTag grid = None - for attribute in ("PartOfW", "PartOfV", "PartOfU"): + for attribute in ("PartOfU", "PartOfV", "PartOfW"): if getattr(reference_element, attribute, None): grid = getattr(reference_element, attribute)[0] break @@ -1452,6 +1542,26 @@ class Drawing(bonsai.core.tool.Drawing): if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct == reference_element: return element + @classmethod + def regenerate_reference_annotation( + cls, + drawing: ifcopenshell.entity_instance, + annotation: ifcopenshell.entity_instance, + reference_element: ifcopenshell.entity_instance, + context: ifcopenshell.entity_instance, + ) -> ifcopenshell.entity_instance: + if reference_element.is_a("IfcGridAxis"): + return cls.regenerate_grid_axis_reference_annotation(drawing, annotation, reference_element, context) + elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING": + target_view = ifcopenshell.util.element.get_pset(reference_element, "EPset_Drawing", "TargetView") + if target_view == "ELEVATION_VIEW": + return cls.regenerate_elevation_reference_annotation(drawing, annotation, reference_element, context) + elif target_view == "SECTION_VIEW": + return cls.regenerate_section_reference_annotation(drawing, annotation, reference_element, context) + elif reference_element.is_a("IfcBuildingStorey"): + return cls.regenerate_storey_annotation(drawing, annotation, reference_element, context) + return annotation + @classmethod def generate_reference_annotation( cls, @@ -1461,333 +1571,410 @@ class Drawing(bonsai.core.tool.Drawing): ) -> ifcopenshell.entity_instance: if reference_element.is_a("IfcGridAxis"): return cls.generate_grid_axis_reference_annotation(drawing, reference_element, context) - elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING": - - def ensure_referenced_drawing_obj_exists(drawing: ifcopenshell.entity_instance): - obj = tool.Ifc.get_object(drawing) - if obj is None: - # Annotations in that drawing are lazy loaded as needed - obj = cls.import_drawing(drawing) - tool.Blender.get_layer_collection(obj.users_collection[0]).hide_viewport = True - target_view = ifcopenshell.util.element.get_pset(reference_element, "EPset_Drawing", "TargetView") if target_view == "ELEVATION_VIEW": - ensure_referenced_drawing_obj_exists(reference_element) return cls.generate_elevation_reference_annotation(drawing, reference_element, context) elif target_view == "SECTION_VIEW": - ensure_referenced_drawing_obj_exists(reference_element) return cls.generate_section_reference_annotation(drawing, reference_element, context) - elif reference_element.is_a("IfcBuildingStorey"): return cls.generate_storey_annotation(drawing, reference_element, context) + @classmethod + def generate_storey_points( + cls, drawing: ifcopenshell.entity_instance, storey: ifcopenshell.entity_instance + ) -> list | None: + import bonsai.bim.module.drawing.helper as helper + + camera = tool.Ifc.get_object(drawing) + if camera.data.type != "ORTHO": + return + if not cls.is_matrix_perpendicular(camera.matrix_world, Matrix()): + return + + xmin, xmax, ymin, ymax = helper.ortho_view_frame(camera.data)[:4] + rl = ifcopenshell.util.placement.get_local_placement(storey.ObjectPlacement)[2][3] + y = (camera.matrix_world.inverted() @ Vector((0.0, 0.0, rl))).y + if y < ymin or y > ymax: + return + + return (Vector((xmax, y, 0.0)), Vector((xmin, y, 0.0))) + @classmethod def generate_storey_annotation( cls, drawing: ifcopenshell.entity_instance, - reference_element: ifcopenshell.entity_instance, + storey: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance, ) -> ifcopenshell.entity_instance: + if not (points := cls.generate_storey_points(drawing, storey)): + return + + camera = tool.Ifc.get_object(drawing) + mesh = bpy.data.meshes.new("Mesh") + obj = bpy.data.objects.new(storey.Name or "Unnamed", mesh) + obj.matrix_world = cls.get_default_annotation_matrix(camera) + element = cls.run_root_assign_class( + obj=obj, ifc_class="IfcAnnotation", predefined_type="SECTION_LEVEL", should_add_representation=False + ) + tool.Geometry.run_edit_object_placement(obj) + element.Name = storey.Name or "Unnamed" + builder = ShapeBuilder(tool.Ifc.get()) + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + points = [p / unit_scale for p in points] + representation = builder.get_representation(context, [builder.polyline(points)]) + ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation) + bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation) + return element + + @classmethod + def regenerate_storey_annotation( + cls, + drawing: ifcopenshell.entity_instance, + annotation: ifcopenshell.entity_instance, + storey: ifcopenshell.entity_instance, + context: ifcopenshell.entity_instance, + ) -> ifcopenshell.entity_instance: + if not (points := cls.generate_storey_points(drawing, storey)): + return + + camera = tool.Ifc.get_object(drawing) + settings = ifcopenshell.geom.settings() + settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) + shape = ifcopenshell.geom.create_shape(settings, annotation) + m = ifcopenshell.util.shape.get_shape_matrix(shape) + mw = cls.get_default_annotation_matrix(camera) + existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)] + + new_points = None + if not np.allclose(m, np.array(mw), atol=1e-4): + new_points = points + elif len(existing_verts) != 2: + new_points = points + else: + existing_verts = sorted(existing_verts, key=lambda v: v.x) + xmin, xmax = [v.x for v in existing_verts] + y = points[0].y + if not tool.Cad.is_x(y, existing_verts[0].y) or not tool.Cad.is_x(y, existing_verts[1].y): + new_points = (Vector((xmax, y, 0.0)), Vector((xmin, y, 0.0))) + + if new_points: + if representation := ifcopenshell.util.representation.get_representation(annotation, context): + ifcopenshell.api.geometry.unassign_representation( + tool.Ifc.get(), product=annotation, representation=representation + ) + builder = ShapeBuilder(tool.Ifc.get()) + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + new_points = [p / unit_scale for p in new_points] + representation = builder.get_representation(context, [builder.polyline(new_points)]) + ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation) + + if obj := tool.Ifc.get_object(annotation): + obj.matrix_world = mw + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + bonsai.core.geometry.switch_representation( + tool.Ifc, tool.Geometry, obj=obj, representation=representation + ) + else: + ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw)) + + annotation.Name = storey.Name or "Unnamed" + return annotation + + @classmethod + def generate_section_reference_points( + cls, drawing: ifcopenshell.entity_instance, section: ifcopenshell.entity_instance + ) -> list | None: import bonsai.bim.module.drawing.helper as helper camera = tool.Ifc.get_object(drawing) - assert isinstance(camera, bpy.types.Object) - assert isinstance((camera_data := camera.data), bpy.types.Camera) - props = tool.Drawing.get_camera_props(camera_data) + if camera.data.type != "ORTHO": + return + settings = ifcopenshell.geom.settings() + shape = ifcopenshell.geom.create_shape(settings, section) + m = ifcopenshell.util.shape.get_shape_matrix(shape) + if not cls.is_matrix_perpendicular(camera.matrix_world, Matrix(m)): + return + if not cls.does_shape_intersect_camera(shape, camera): + return - bounds = helper.ortho_view_frame(camera_data) if camera_data.type == "ORTHO" else None - reference_obj = tool.Ifc.get_object(reference_element) - assert isinstance(reference_obj, bpy.types.Object) + # Get cutting plane as a line + verts = ifcopenshell.util.shape.get_vertices(shape.geometry) + cutting_plane_verts = sorted(verts, key=lambda v: v[2])[-4:] + v1, *_, v2 = sorted(cutting_plane_verts, key=lambda v: v[0]) # Cut is in +X direction + im = camera.matrix_world.inverted() + v1, v2 = [im @ Vector((m @ np.append(v, 1.0))[:3]) for v in [v1, v2]] - def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix: - mat = reference_obj.matrix_world.copy() - xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation - xyz[2] = 0 - xyz = camera.matrix_world @ xyz - mat.translation = xyz - annotation_offset = mathutils.Vector((0, 0, -camera_data.clip_start - 0.05)) - annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset - mat.translation += annotation_offset - return mat - - def project_point_onto_camera(point: Vector, camera: bpy.types.Object) -> Vector: - projection = camera.matrix_world.to_quaternion() @ mathutils.Vector((0, 0, -1)) - return camera.matrix_world.inverted() @ mathutils.geometry.intersect_line_plane( - point.xyz, point.xyz - projection, camera.location, projection - ) - - obj_matrix = to_camera_coords(camera, reference_obj) - - if props.raster_x > props.raster_y: - width = camera_data.ortho_scale - height = width / props.raster_x * props.raster_y - else: - height = camera_data.ortho_scale - width = height / props.raster_y * props.raster_x - - projection = project_point_onto_camera(reference_obj.location, camera) - co1 = camera.matrix_world @ mathutils.Vector((width / 2, projection[1], -1)) - co2 = camera.matrix_world @ mathutils.Vector((-(width / 2), projection[1], -1)) - co1 = obj_matrix.inverted() @ co1 - co2 = obj_matrix.inverted() @ co2 - - data = bpy.data.curves.new("Annotation", type="CURVE") - data.dimensions = "3D" - data.resolution_u = 2 - - polyline = data.splines.new("POLY") - polyline.points.add(1) - polyline.points[-2].co = list(co1) + [1] - polyline.points[-1].co = list(co2) + [1] - - obj = bpy.data.objects.new(reference_obj.name, data) - obj.matrix_world = obj_matrix - - element = cls.run_root_assign_class( - obj=obj, - ifc_class="IfcAnnotation", - predefined_type="SECTION_LEVEL", - should_add_representation=True, - context=context, - ifc_representation_class=None, - ) - if representation := ifcopenshell.util.representation.get_representation(element, context): - cls.reload_representation(obj=obj, representation=representation) - bpy.data.curves.remove(data) - - return element + bounds = helper.ortho_view_frame(camera.data) + if not (points := helper.clip_segment(bounds, [v1, v2])): + return + for v in points: + v.z = 0 + return points @classmethod def generate_section_reference_annotation( cls, drawing: ifcopenshell.entity_instance, - reference_element: ifcopenshell.entity_instance, + section: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance, ) -> ifcopenshell.entity_instance: - import bonsai.bim.module.drawing.helper as helper + if not (points := cls.generate_section_reference_points(drawing, section)): + return - reference_obj = tool.Ifc.get_object(reference_element) - reference_obj.matrix_world camera = tool.Ifc.get_object(drawing) - bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None + mesh = bpy.data.meshes.new("Mesh") + obj = bpy.data.objects.new(section.Name or "Unnamed", mesh) + obj.matrix_world = cls.get_default_annotation_matrix(camera) + element = cls.run_root_assign_class( + obj=obj, ifc_class="IfcAnnotation", predefined_type="SECTION", should_add_representation=False + ) + element.Name = section.Name or "Unnamed" + builder = ShapeBuilder(tool.Ifc.get()) + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + points = [p / unit_scale for p in points] + representation = builder.get_representation(context, [builder.polyline(points)]) + ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation) + bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation) + return element - def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix: - mat = reference_obj.matrix_world.copy() - xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation - xyz[2] = 0 - xyz = camera.matrix_world @ xyz - mat.translation = xyz - annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05)) - annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset - mat.translation += annotation_offset - return mat + @classmethod + def regenerate_section_reference_annotation( + cls, + drawing: ifcopenshell.entity_instance, + annotation: ifcopenshell.entity_instance, + section: ifcopenshell.entity_instance, + context: ifcopenshell.entity_instance, + ) -> ifcopenshell.entity_instance: + if not (points := cls.generate_section_reference_points(drawing, section)): + return - def clip_to_camera_boundary( - mesh: bpy.types.Mesh, bounds: tuple[float, float, float, float, float, float] - ) -> Union[bpy.types.Mesh, None]: - mesh.verts.ensure_lookup_table() - points = [v.co for v in mesh.verts[0:2]] - points = helper.clip_segment(bounds, points) - if points is None: - return None - mesh.verts[0].co = points[0] - mesh.verts[1].co = points[1] - return mesh + camera = tool.Ifc.get_object(drawing) + settings = ifcopenshell.geom.settings() + settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) + shape = ifcopenshell.geom.create_shape(settings, annotation) + m = ifcopenshell.util.shape.get_shape_matrix(shape) + mw = cls.get_default_annotation_matrix(camera) + existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)] - if cls.is_perpendicular(camera, reference_obj) and cls.is_intersecting(camera, reference_obj): - reference_mesh = cls.get_camera_block(reference_obj) - obj_matrix = to_camera_coords(camera, reference_obj) + new_points = None + if not np.allclose(m, np.array(mw), atol=1e-4): + new_points = points + elif len(existing_verts) != 2: + new_points = points + else: + if not tool.Cad.are_edges_collinear(existing_verts, points): + # Attempt to update the section line by projecting existing verts onto the new line + v1 = tool.Cad.point_on_edge(existing_verts[0], points) + v2 = tool.Cad.point_on_edge(existing_verts[1], points) + existing_length = (existing_verts[0] - existing_verts[1]).length + new_length = (v2 - v1).length + if abs((existing_length - new_length) / existing_length) <= 0.10: + # If the projected line is within 10% of the previous length ... + new_points = (v1, v2) + else: + new_points = points - # The reference mesh vertices represent a view cube. To convert - # this into a section line we: - # 1. Select the 4 +Z vertices local to the reference element. This - # is the cutting plane. - verts_local_to_reference = [reference_obj.matrix_world.inverted() @ v for v in reference_mesh["verts"]] - cutting_plane_verts = sorted(verts_local_to_reference, key=lambda x: x.z)[-4:] - global_cutting_plane_verts = [reference_obj.matrix_world @ v for v in cutting_plane_verts] - # 2. Project the cutting plane onto our viewing camera. - verts_local_to_camera = [camera.matrix_world.inverted() @ v for v in global_cutting_plane_verts] - # 3. Collapse verts with the same XY coords, and set Z to be just - # below the clip_start so it's visible - collapsed_verts = [] - for vert in verts_local_to_camera: - if not [True for v in collapsed_verts if (vert.xy - v.xy).length < 1e-2]: - collapsed_verts.append(mathutils.Vector((vert.x, vert.y, -camera.data.clip_start - 0.05))) - # 4. The first two vertices is the section line - section_line = collapsed_verts[0:2] - # 5. Sort the vertices in the +X direction so that the vertices are - # ordered to "point" in the direction of the section cut. - section_line = sorted( - section_line, key=lambda co: (reference_obj.matrix_world.inverted() @ camera.matrix_world @ co).x - ) - global_section_line = [camera.matrix_world @ v for v in section_line] - local_section_line = [obj_matrix.inverted() @ v for v in global_section_line] - - mesh = bpy.data.meshes.new(name="Annotation") - mesh.from_pydata(local_section_line, [(0, 1)], []) - bm = bmesh.new() - bm.from_mesh(mesh) - bm = clip_to_camera_boundary(bm, bounds) - bm.to_mesh(mesh) - bm.free() - - obj = bpy.data.objects.new(reference_obj.name, mesh) - obj.matrix_world = obj_matrix - - element = cls.run_root_assign_class( - obj=obj, - ifc_class="IfcAnnotation", - predefined_type="SECTION", - should_add_representation=True, - context=context, - ifc_representation_class=None, - ) - if representation := ifcopenshell.util.representation.get_representation(element, context): - cls.reload_representation(obj=obj, representation=representation) - return element + if new_points: + if representation := ifcopenshell.util.representation.get_representation(annotation, context): + ifcopenshell.api.geometry.unassign_representation( + tool.Ifc.get(), product=annotation, representation=representation + ) + builder = ShapeBuilder(tool.Ifc.get()) + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + new_points = [p / unit_scale for p in new_points] + representation = builder.get_representation(context, [builder.polyline(new_points)]) + ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation) + if obj := tool.Ifc.get_object(annotation): + obj.matrix_world = mw + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + bonsai.core.geometry.switch_representation( + tool.Ifc, tool.Geometry, obj=obj, representation=representation + ) + else: + ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw)) + annotation.Name = section.Name or "Unnamed" + return annotation @classmethod def generate_elevation_reference_annotation( cls, drawing: ifcopenshell.entity_instance, - reference_element: ifcopenshell.entity_instance, + elevation: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance, ) -> ifcopenshell.entity_instance: - reference_obj = tool.Ifc.get_object(reference_element) - reference_obj.matrix_world camera = tool.Ifc.get_object(drawing) - - def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix: - mat = reference_obj.matrix_world.copy() - xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation - xyz[2] = 0 - xyz = camera.matrix_world @ xyz - mat.translation = xyz - annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05)) - annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset - mat.translation += annotation_offset - return mat - - if cls.is_perpendicular(camera, reference_obj) and cls.is_intersecting(camera, reference_obj): - obj = bpy.data.objects.new(reference_obj.name, None) + if camera.data.type != "ORTHO": + return + settings = ifcopenshell.geom.settings() + shape = ifcopenshell.geom.create_shape(settings, elevation) + m = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape)) + if cls.is_matrix_perpendicular(camera.matrix_world, m) and cls.does_shape_intersect_camera(shape, camera): + obj = bpy.data.objects.new(elevation.Name or "Unnamed", None) obj.empty_display_size = 0.1 - obj.matrix_world = to_camera_coords(camera, reference_obj) - + obj.matrix_world = cls.get_default_annotation_matrix(camera, matrix_world=m) element = cls.run_root_assign_class( - obj=obj, - ifc_class="IfcAnnotation", - predefined_type="ELEVATION", - should_add_representation=False, - context=context, - ifc_representation_class=None, + obj=obj, ifc_class="IfcAnnotation", predefined_type="ELEVATION", should_add_representation=False ) - if representation := ifcopenshell.util.representation.get_representation(element, context): - cls.reload_representation(obj=obj, representation=representation) + element.Name = elevation.Name or "Unnamed" return element + @classmethod + def regenerate_elevation_reference_annotation( + cls, + drawing: ifcopenshell.entity_instance, + annotation: ifcopenshell.entity_instance, + elevation: ifcopenshell.entity_instance, + context: ifcopenshell.entity_instance, + ) -> ifcopenshell.entity_instance: + camera = tool.Ifc.get_object(drawing) + if camera.data.type != "ORTHO": + return + settings = ifcopenshell.geom.settings() + shape = ifcopenshell.geom.create_shape(settings, elevation) + m = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape)) + if cls.is_matrix_perpendicular(camera.matrix_world, m) and cls.does_shape_intersect_camera(shape, camera): + existing_matrix = Matrix(ifcopenshell.util.placement.get_local_placement(annotation.ObjectPlacement)) + # The user is allowed to shift the elevation, but not rotate it + if not np.allclose(np.array(m.to_3x3()), np.array(existing_matrix.to_3x3()), atol=1e-4): + mw = cls.get_default_annotation_matrix(camera, matrix_world=m) + if obj := tool.Ifc.get_object(annotation): + obj.matrix_world = mw + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + else: + ifcopenshell.api.geometry.edit_object_placement( + tool.Ifc.get(), product=annotation, matrix=np.array(mw) + ) + annotation.Name = elevation.Name or "Unnamed" + return annotation + + @classmethod + def generate_grid_axis_reference_points( + cls, drawing: ifcopenshell.entity_instance, axis: ifcopenshell.entity_instance + ) -> list | None: + import bonsai.bim.module.drawing.helper as helper + + camera = tool.Ifc.get_object(drawing) + if camera.data.type != "ORTHO": + return + + settings = ifcopenshell.geom.settings() + settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) + geometry = ifcopenshell.geom.create_shape(settings, axis.AxisCurve) + verts = ifcopenshell.util.shape.get_vertices(geometry) + grid = (axis.PartOfU or axis.PartOfV or axis.PartOfW)[0] + m = ifcopenshell.util.placement.get_local_placement(grid.ObjectPlacement) + im = camera.matrix_world.inverted() + v1, v2 = [im @ Vector((m @ np.append(v, 1.0))[:3]) for v in verts[:2]] + + target_view = tool.Drawing.get_drawing_target_view(drawing) + if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW"): + bounds = helper.ortho_view_frame(camera.data) + if not (points := helper.clip_segment(bounds, [v1, v2])): + return + elif target_view in ("ELEVATION_VIEW", "SECTION_VIEW"): + bounds = helper.ortho_view_frame(camera.data) + if not (points := helper.elevate_segment(bounds, [v1, v2])): + return + else: + return + for v in points: + v.z = 0 + return points + @classmethod def generate_grid_axis_reference_annotation( cls, drawing: ifcopenshell.entity_instance, - reference_element: ifcopenshell.entity_instance, + axis: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance, ) -> Union[ifcopenshell.entity_instance, None]: - import bonsai.bim.module.drawing.helper as helper - - target_view = tool.Drawing.get_drawing_target_view(drawing) + if not (points := cls.generate_grid_axis_reference_points(drawing, axis)): + return camera = tool.Ifc.get_object(drawing) - assert isinstance(camera, bpy.types.Object) - assert isinstance(camera_data := camera.data, bpy.types.Camera) - - is_ortho = camera_data.type == "ORTHO" - bounds = helper.ortho_view_frame(camera_data) if is_ortho else None - clipping = is_ortho and target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW") - elevating = is_ortho and target_view in ("ELEVATION_VIEW", "SECTION_VIEW") - - def clone(src: bpy.types.Object) -> bpy.types.Object: - dst = src.copy() - assert isinstance(dst.data, bpy.types.Mesh) - dst.data = dst.data.copy() - dst.name = dst.name.replace("IfcGridAxis/", "") - tool.Blender.get_object_bim_props(dst).ifc_definition_id = 0 - tool.Geometry.get_mesh_props(dst.data).ifc_definition_id = 0 - return dst - - def disassemble(obj: bpy.types.Object) -> tuple[bpy.types.Object, bmesh.types.BMesh]: - assert isinstance(obj.data, bpy.types.Mesh) - mesh = bmesh.new() - mesh.verts.ensure_lookup_table() - mesh.from_mesh(obj.data) - return obj, mesh - - def assemble(obj: bpy.types.Object, mesh: bmesh.types.BMesh) -> bpy.types.Object: - assert isinstance(obj.data, bpy.types.Mesh) - mesh.to_mesh(obj.data) - return obj - - def to_camera_coords( - obj: bpy.types.Object, mesh: bmesh.types.BMesh - ) -> tuple[bpy.types.Object, bmesh.types.BMesh]: - mesh.transform(camera.matrix_world.inverted() @ obj.matrix_world) - obj.matrix_world = camera.matrix_world - annotation_offset = mathutils.Vector((0, 0, -camera_data.clip_start - 0.05)) - annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset - obj.matrix_world.translation += annotation_offset - return obj, mesh - - def clip_to_camera_boundary(mesh: bmesh.types.BMesh) -> bmesh.types.BMesh: - mesh.verts.ensure_lookup_table() - points = [v.co for v in mesh.verts[0:2]] - points = helper.clip_segment(bounds, points) - if points is None: - return None - mesh.verts[0].co = points[0] - mesh.verts[1].co = points[1] - return mesh - - def draw_grids_vertically(mesh: bmesh.types.BMesh) -> bmesh.types.BMesh: - mesh.verts.ensure_lookup_table() - points = [v.co for v in mesh.verts[0:2]] - points = helper.elevate_segment(bounds, points) - if points is None: - return None - points = helper.clip_segment(bounds, points) - if points is None: - return None - mesh.verts[0].co = points[0] - mesh.verts[1].co = points[1] - return mesh - - obj = tool.Ifc.get_object(reference_element) - if not obj: - return - assert isinstance(obj, bpy.types.Object) - obj, mesh = to_camera_coords(*disassemble(clone(obj))) - - if clipping: - mesh = clip_to_camera_boundary(mesh) - elif elevating: - mesh = draw_grids_vertically(mesh) - - if mesh is None: - return - - assemble(obj, mesh) - + mesh = bpy.data.meshes.new("Mesh") + obj = bpy.data.objects.new(axis.AxisTag or "-", mesh) + obj.matrix_world = cls.get_default_annotation_matrix(camera) element = cls.run_root_assign_class( - obj=obj, - ifc_class="IfcAnnotation", - predefined_type="GRID", - should_add_representation=True, - context=context, - ifc_representation_class=None, + obj=obj, ifc_class="IfcAnnotation", predefined_type="GRID", should_add_representation=False ) - if representation := ifcopenshell.util.representation.get_representation(element, context): - cls.reload_representation(obj=obj, representation=representation) + element.Name = axis.AxisTag or "-" + builder = ShapeBuilder(tool.Ifc.get()) + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + points = [p / unit_scale for p in points] + representation = builder.get_representation(context, [builder.polyline(points)]) + ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation) + bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation) return element + @classmethod + def regenerate_grid_axis_reference_annotation( + cls, + drawing: ifcopenshell.entity_instance, + annotation: ifcopenshell.entity_instance, + axis: ifcopenshell.entity_instance, + context: ifcopenshell.entity_instance, + ) -> Union[ifcopenshell.entity_instance, None]: + if not (points := cls.generate_grid_axis_reference_points(drawing, axis)): + return + + camera = tool.Ifc.get_object(drawing) + settings = ifcopenshell.geom.settings() + settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) + shape = ifcopenshell.geom.create_shape(settings, annotation) + m = ifcopenshell.util.shape.get_shape_matrix(shape) + mw = cls.get_default_annotation_matrix(camera) + existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)] + + new_points = None + if not np.allclose(m, np.array(mw), atol=1e-4): + new_points = points + elif len(existing_verts) != 2: + new_points = points + else: + if not tool.Cad.are_edges_collinear(existing_verts, points): + # Attempt to update the section line by projecting existing verts onto the new line + v1 = tool.Cad.point_on_edge(existing_verts[0], points) + v2 = tool.Cad.point_on_edge(existing_verts[1], points) + existing_length = (existing_verts[0] - existing_verts[1]).length + new_length = (v2 - v1).length + if abs((existing_length - new_length) / existing_length) <= 0.10: + # If the projected line is within 10% of the previous length ... + new_points = (v1, v2) + else: + new_points = points + + if new_points: + if representation := ifcopenshell.util.representation.get_representation(annotation, context): + ifcopenshell.api.geometry.unassign_representation( + tool.Ifc.get(), product=annotation, representation=representation + ) + builder = ShapeBuilder(tool.Ifc.get()) + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + new_points = [p / unit_scale for p in new_points] + representation = builder.get_representation(context, [builder.polyline(new_points)]) + ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation) + if obj := tool.Ifc.get_object(annotation): + obj.matrix_world = mw + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + bonsai.core.geometry.switch_representation( + tool.Ifc, tool.Geometry, obj=obj, representation=representation + ) + else: + ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw)) + annotation.Name = axis.AxisTag or "-" + return annotation + + @classmethod + def get_default_annotation_matrix(cls, camera, matrix_world=None): + if matrix_world is None: + matrix_world = camera.matrix_world.copy() + annotation_offset = Vector((0, 0, -camera.data.clip_start - 0.05)) + annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset + matrix_world.translation += annotation_offset + return matrix_world + @classmethod def is_perpendicular(cls, a: bpy.types.Object, b: bpy.types.Object) -> bool: axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))] @@ -1798,6 +1985,16 @@ class Drawing(bonsai.core.tool.Drawing): return True return False + @classmethod + def is_matrix_perpendicular(cls, a: Matrix, b: Matrix) -> bool: + axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))] + a_quaternion = a.to_quaternion() + b_quaternion = b.to_quaternion() + for axis in axes: + if abs((a_quaternion @ axis).angle(b_quaternion @ axis) - (math.pi / 2)) < 1e-5: + return True + return False + @classmethod def get_camera_block(cls, obj: bpy.types.Object) -> dict: assert isinstance(camera := obj.data, bpy.types.Camera) @@ -1841,6 +2038,16 @@ class Drawing(bonsai.core.tool.Drawing): b_tree = mathutils.bvhtree.BVHTree.FromPolygons(b_block["verts"], b_block["faces"]) return bool(a_tree.overlap(b_tree)) + @classmethod + def does_shape_intersect_camera(cls, shape, camera) -> bool: + a_block = cls.get_camera_block(camera) + a_tree = mathutils.bvhtree.BVHTree.FromPolygons(a_block["verts"], a_block["faces"]) + m = ifcopenshell.util.shape.get_shape_matrix(shape) + verts = [(m @ np.append(v, 1.0))[:3] for v in ifcopenshell.util.shape.get_vertices(shape.geometry)] + faces = ifcopenshell.util.shape.get_faces(shape.geometry) + b_tree = mathutils.bvhtree.BVHTree.FromPolygons(verts, faces) + return bool(a_tree.overlap(b_tree)) + @classmethod def replace_text_literal_variables( cls, @@ -2568,3 +2775,14 @@ class Drawing(bonsai.core.tool.Drawing): ifcopenshell.api.document.remove_reference(tool.Ifc.get(), reference=reference) tool.Drawing.import_sheets() + + @classmethod + def hide_all_drawing_collections(cls) -> None: + for element in tool.Ifc.get().by_type("IfcAnnotation"): + if element.ObjectType == "DRAWING" and (obj := tool.Ifc.get_object(element)): + tool.Blender.get_layer_collection(obj.users_collection[0]).hide_viewport = True + + @classmethod + def clear_annotation_relationships(cls, drawing: ifcopenshell.entity_instance) -> None: + for rel in drawing.ReferencedBy: + tool.Ifc.get().remove(rel) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 8db4264783..6df540ef13 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -233,20 +233,20 @@ class Geometry(bonsai.core.tool.Geometry): element = tool.Ifc.get_entity(obj) if not element: return - elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING": - return bonsai.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element) + elif element.is_a("IfcAnnotation"): + if element.ObjectType == "DRAWING": + return bonsai.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element) + elif tool.Drawing.is_auto_annotation(element): + return # For now, these are special referenced objects and cannot be deleted. Exclude instead. elif element.is_a("IfcRelSpaceBoundary"): ifcopenshell.api.boundary.remove_boundary(ifc_file, boundary=element) tool.Boundary.undecorate_boundary(obj) return bpy.data.objects.remove(obj) elif element.is_a("IfcGridAxis"): - is_last_axis = False # Deleting the last W axis is OK if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or ( (grid := element.PartOfV) and len(grid[0].VAxes) == 1 ): - is_last_axis = True - if is_last_axis: return ifcopenshell.api.grid.remove_grid_axis(ifc_file, axis=element) return bpy.data.objects.remove(obj) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 10181c5d93..543106f937 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -1399,7 +1399,6 @@ class Model(bonsai.core.tool.Model): number_of_risers = number_of_treads + 1 tread_rise = height / number_of_risers - custom_tread_run = any(run != 0 for run in custom_first_last_tread_run) nosing_overlap = max(nosing_length, 0) nosing_tread_gap = -min(nosing_length, 0) nosing_overlap_offset = -V_(nosing_overlap, 0) @@ -1430,19 +1429,26 @@ class Model(bonsai.core.tool.Model): default_tread_offset = Vector([tread_run, tread_rise]) def get_tread_data(i): - if custom_tread_run: - current_tread_run = None - if i == 0: - current_tread_run = custom_first_last_tread_run[0] - elif i == number_of_risers - 1: - current_tread_run = custom_first_last_tread_run[1] + # Check if this is first or last tread with custom run + current_tread_run = None + if i == 0 and custom_first_last_tread_run[0] is not None: + current_tread_run = custom_first_last_tread_run[0] + elif i == number_of_risers - 1 and custom_first_last_tread_run[1] is not None: + current_tread_run = custom_first_last_tread_run[1] + + if current_tread_run is not None: + tread_offset = default_tread_offset.copy() + tread_offset.x = current_tread_run + + # Handle zero-width treads + if current_tread_run == 0: + # For zero width, just return vertical offset with no horizontal tread + return tread_offset, () + + tread_verts = deepcopy(default_tread_verts) + tread_verts[-1].x = current_tread_run + return tread_offset, tread_verts - if current_tread_run: - tread_offset = default_tread_offset.copy() - tread_offset.x = current_tread_run - tread_verts = deepcopy(default_tread_verts) - tread_verts[-1].x = current_tread_run - return tread_offset, tread_verts return default_tread_offset, default_tread_verts # treads @@ -1450,9 +1456,13 @@ class Model(bonsai.core.tool.Model): for i in range(number_of_risers): last_vert_i = len(vertices) - 1 tread_offset, tread_verts = get_tread_data(i) - current_tread_verts = [v + current_offset for v in tread_verts] - edges.extend(default_tread_edges + last_vert_i) - vertices.extend(current_tread_verts) + + # Skip adding vertices/edges for zero-width treads + if tread_verts: + current_tread_verts = [v + current_offset for v in tread_verts] + edges.extend(default_tread_edges + last_vert_i) + vertices.extend(current_tread_verts) + current_offset += tread_offset if stair_type == "WOOD/STEEL": @@ -1467,35 +1477,47 @@ class Model(bonsai.core.tool.Model): default_tread_offset = V_(tread_run + nosing_tread_gap, tread_rise) def get_tread_data(i): - if custom_tread_run: - current_tread_run = None - if i == 0 and custom_first_last_tread_run[0] != 0: - current_tread_run = custom_first_last_tread_run[0] - elif i == number_of_risers - 1 and custom_first_last_tread_run[1] != 0: - current_tread_run = custom_first_last_tread_run[1] + # Check if this is first or last tread with custom run + current_tread_run = None + if i == 0 and custom_first_last_tread_run[0] is not None: + current_tread_run = custom_first_last_tread_run[0] + elif i == number_of_risers - 1 and custom_first_last_tread_run[1] is not None: + current_tread_run = custom_first_last_tread_run[1] + + if current_tread_run is not None: + tread_offset = default_tread_offset.copy() + tread_offset.x = current_tread_run + nosing_tread_gap + + # Handle zero-width treads + if current_tread_run == 0: + return tread_offset, () + + tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth)) + return tread_offset, tread_verts - if current_tread_run: - tread_offset = default_tread_offset.copy() - tread_offset.x = current_tread_run + nosing_tread_gap - tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth)) - return tread_offset, tread_verts return default_tread_offset, default_tread_verts # each tread is a separate shape cur_offset = V_(0, 0) + tread_index = 0 for i in range(number_of_risers): tread_offset, tread_verts = get_tread_data(i) - cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts] - vertices.extend(cur_trade_shape) - cur_vertex = i * 4 - verts_to_add = ( - (cur_vertex, cur_vertex + 1), - (cur_vertex + 1, cur_vertex + 2), - (cur_vertex + 2, cur_vertex + 3), - (cur_vertex + 3, cur_vertex), - ) - edges.extend(verts_to_add) + # Skip adding vertices/edges for zero-width treads + if tread_verts: + cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts] + vertices.extend(cur_trade_shape) + + cur_vertex = tread_index * 4 + verts_to_add = ( + (cur_vertex, cur_vertex + 1), + (cur_vertex + 1, cur_vertex + 2), + (cur_vertex + 2, cur_vertex + 3), + (cur_vertex + 3, cur_vertex), + ) + edges.extend(verts_to_add) + tread_index += 1 + cur_offset += tread_offset elif stair_type == "GENERIC": diff --git a/src/bonsai/bonsai/tool/sequence.py b/src/bonsai/bonsai/tool/sequence.py index 96fbb487a5..14c88dd251 100644 --- a/src/bonsai/bonsai/tool/sequence.py +++ b/src/bonsai/bonsai/tool/sequence.py @@ -512,14 +512,12 @@ class Sequence(bonsai.core.tool.Sequence): @classmethod def get_task_inputs(cls, task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: props = cls.get_work_schedule_props() - is_deep = props.show_nested_inputs - return ifcopenshell.util.sequence.get_task_inputs(task, is_deep) + return ifcopenshell.util.sequence.get_task_inputs(task, is_recursive=props.show_nested_inputs) @classmethod def get_task_outputs(cls, task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: props = cls.get_work_schedule_props() - is_deep = props.show_nested_outputs - return ifcopenshell.util.sequence.get_task_outputs(task, is_deep) + return ifcopenshell.util.sequence.get_task_outputs(task, is_recursive=props.show_nested_outputs) @classmethod def are_entities_same_class(cls, entities: list[ifcopenshell.entity_instance]) -> bool: @@ -540,8 +538,7 @@ class Sequence(bonsai.core.tool.Sequence): if not task: return props = cls.get_work_schedule_props() - is_deep = props.show_nested_resources - return ifcopenshell.util.sequence.get_task_resources(task, is_deep) + return ifcopenshell.util.sequence.get_task_resources(task, props.show_nested_resources) @classmethod def load_task_inputs(cls, inputs: list[ifcopenshell.entity_instance]) -> None: @@ -1582,7 +1579,7 @@ class Sequence(bonsai.core.tool.Sequence): @classmethod def create_new_task_json(cls, task, json, type_map=None, baseline_schedule=None): task_time = task.TaskTime - resources = ifcopenshell.util.sequence.get_task_resources(task, is_deep=False) + resources = ifcopenshell.util.sequence.get_task_resources(task, is_recursive=False) string_resources = "" resources_usage = "" @@ -1689,8 +1686,8 @@ class Sequence(bonsai.core.tool.Sequence): ) -> list[ifcopenshell.entity_instance]: products = [] for task in ifcopenshell.util.sequence.get_root_tasks(work_schedule): - products.extend(ifcopenshell.util.sequence.get_task_inputs(task, is_deep=True)) - products.extend(ifcopenshell.util.sequence.get_task_outputs(task, is_deep=True)) + products.extend(ifcopenshell.util.sequence.get_task_inputs(task, is_recursive=True)) + products.extend(ifcopenshell.util.sequence.get_task_outputs(task, is_recursive=True)) return products @classmethod @@ -1789,6 +1786,16 @@ class Sequence(bonsai.core.tool.Sequence): cls.load_task_outputs(outputs) cls.load_task_resources(task) + @classmethod + def select_active_task_elements(cls, task): + if not task: + return + bpy.ops.object.select_all(action="DESELECT") + props = cls.get_work_schedule_props() + for element in cls.get_task_inputs(task) | cls.get_task_outputs(task): + if obj := tool.Ifc.get_object(element): + obj.select_set(True) + @classmethod def refresh_task_resources(cls): task = cls.get_highlighted_task() diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index a9d7c170ef..ec0835b0a9 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -175,7 +175,6 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def select_products(cls, products: Iterable[ifcopenshell.entity_instance], unhide: bool = False) -> None: - bpy.ops.object.select_all(action="DESELECT") for product in products: obj = tool.Ifc.get_object(product) if obj and bpy.context.view_layer.objects.get(obj.name): diff --git a/src/bonsai/test/tool/test_model.py b/src/bonsai/test/tool/test_model.py index 35d5c6bc57..04fdd881a6 100644 --- a/src/bonsai/test/tool/test_model.py +++ b/src/bonsai/test/tool/test_model.py @@ -176,6 +176,27 @@ class TestStairCalculatedParams(NewFile): calculated_data["Length"] += -0.2 + 0.1 self.compare_data(pset_data, calculated_data) + # zero-width first tread + pset_data = pset_data_base.copy() + calculated_data = calculated_data_base.copy() + pset_data["custom_first_last_tread_run"] = (0.0, 0.0) + calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each + self.compare_data(pset_data, calculated_data) + + # zero-width last tread + pset_data = pset_data_base.copy() + calculated_data = calculated_data_base.copy() + pset_data["custom_first_last_tread_run"] = (0.3, 0.0) + calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each + self.compare_data(pset_data, calculated_data) + + # both first and last treads zero-width + pset_data = pset_data_base.copy() + calculated_data = calculated_data_base.copy() + pset_data["custom_first_last_tread_run"] = (0.0, 0.0) + calculated_data["Length"] = 0.6 # Only 2 middle treads at 0.3 each + self.compare_data(pset_data, calculated_data) + # overlap affects stair length only by first tread pset_data = pset_data_base.copy() calculated_data = calculated_data_base.copy() @@ -296,6 +317,94 @@ class TestGenerateStair2DProfile(NewFile): generated_profile = subject.generate_stair_2d_profile(**kwargs) self.compare_data(generated_profile, expected_profile) + def test_create_concrete_stair_zero_width_first_tread(self): + """Test concrete stair with zero-width first tread""" + kwargs = { + "base_slab_depth": 0.25, + "has_top_nib": False, + "height": 1.0, + "number_of_treads": 3, + "stair_type": "CONCRETE", + "top_slab_depth": 0.25, + "tread_depth": 0.25, + "tread_run": 0.3, + "width": 1.2, + "custom_first_last_tread_run": (0.0, 0.0), + } + verts_data = ( + V(0.0, 0, 0.0), + # First tread skipped - goes straight to second tread + V(0.0, 0, 0.5), + V(0.3, 0, 0.5), + V(0.3, 0, 0.75), + V(0.6, 0, 0.75), + V(0.6, 0, 1.0), + V(0.9, 0, 1.0), + V(0.9, 0, 0.67457), + V(0.0, 0, -0.25), + ) + edges_data = ( + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 5), + (5, 6), + (6, 7), + (8, 0), + (7, 8), + ) + edges_data = [e[::-1] for e in edges_data] + faces_data = () + expected_profile = (verts_data, edges_data, faces_data) + generated_profile = subject.generate_stair_2d_profile(**kwargs) + self.compare_data(generated_profile, expected_profile) + + def test_create_concrete_stair_zero_width_last_tread(self): + """Test concrete stair with zero-width last tread""" + kwargs = { + "base_slab_depth": 0.25, + "has_top_nib": False, + "height": 1.0, + "number_of_treads": 3, + "stair_type": "CONCRETE", + "top_slab_depth": 0.25, + "tread_depth": 0.25, + "tread_run": 0.3, + "width": 1.2, + "custom_first_last_tread_run": (0.0, 0.0), + } + verts_data = ( + V(0.0, 0, 0.0), + V(0.0, 0, 0.25), + V(0.3, 0, 0.25), + V(0.3, 0, 0.5), + V(0.6, 0, 0.5), + V(0.6, 0, 0.75), + V(0.9, 0, 0.75), + # Last tread skipped + V(0.9, 0, 0.67457), + V(0.1, 0, -0.25), + V(0.0, 0, -0.25), + ) + edges_data = ( + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 5), + (5, 6), + (6, 7), + (9, 0), + (8, 9), + (7, 8), + ) + edges_data = [e[::-1] for e in edges_data] + faces_data = () + expected_profile = (verts_data, edges_data, faces_data) + generated_profile = subject.generate_stair_2d_profile(**kwargs) + self.compare_data(generated_profile, expected_profile) + def test_create_wood_steel_stair(self): kwargs = { "height": 1.0, @@ -348,6 +457,100 @@ class TestGenerateStair2DProfile(NewFile): generated_profile = subject.generate_stair_2d_profile(**kwargs) self.compare_data(generated_profile, expected_profile) + def test_create_wood_steel_stair_zero_width_first_tread(self): + """Test wood/steel stair with zero-width first tread""" + kwargs = { + "height": 1.0, + "number_of_treads": 3, + "stair_type": "WOOD/STEEL", + "tread_depth": 0.25, + "tread_run": 0.3, + "width": 1.2, + "custom_first_last_tread_run": (0.0, 0.0), + } + verts_data = ( + # First tread skipped - start at second tread + V(0.0, 0, 0.25), + V(0.3, 0, 0.25), + V(0.3, 0, 0.5), + V(0.0, 0, 0.5), + V(0.3, 0, 0.5), + V(0.6, 0, 0.5), + V(0.6, 0, 0.75), + V(0.3, 0, 0.75), + V(0.6, 0, 0.75), + V(0.9, 0, 0.75), + V(0.9, 0, 1.0), + V(0.6, 0, 1.0), + ) + edges_data = ( + (0, 1), + (1, 2), + (2, 3), + (3, 0), + (4, 5), + (5, 6), + (6, 7), + (7, 4), + (8, 9), + (9, 10), + (10, 11), + (11, 8), + ) + + faces_data = () + + expected_profile = (verts_data, edges_data, faces_data) + generated_profile = subject.generate_stair_2d_profile(**kwargs) + self.compare_data(generated_profile, expected_profile) + + def test_create_wood_steel_stair_zero_width_last_tread(self): + """Test wood/steel stair with zero-width last tread""" + kwargs = { + "height": 1.0, + "number_of_treads": 3, + "stair_type": "WOOD/STEEL", + "tread_depth": 0.25, + "tread_run": 0.3, + "width": 1.2, + "custom_first_last_tread_run": (0.0, 0.0), + } + verts_data = ( + V(0.0, 0, 0.0), + V(0.3, 0, 0.0), + V(0.3, 0, 0.25), + V(0.0, 0, 0.25), + V(0.3, 0, 0.25), + V(0.6, 0, 0.25), + V(0.6, 0, 0.5), + V(0.3, 0, 0.5), + V(0.6, 0, 0.5), + V(0.9, 0, 0.5), + V(0.9, 0, 0.75), + V(0.6, 0, 0.75), + # Last tread skipped + ) + edges_data = ( + (0, 1), + (1, 2), + (2, 3), + (3, 0), + (4, 5), + (5, 6), + (6, 7), + (7, 4), + (8, 9), + (9, 10), + (10, 11), + (11, 8), + ) + + faces_data = () + + expected_profile = (verts_data, edges_data, faces_data) + generated_profile = subject.generate_stair_2d_profile(**kwargs) + self.compare_data(generated_profile, expected_profile) + def test_create_generic_stair(self): kwargs = {"height": 1.0, "number_of_treads": 3, "stair_type": "GENERIC", "tread_run": 0.3, "width": 1.2} verts_data = ( @@ -381,6 +584,41 @@ class TestGenerateStair2DProfile(NewFile): generated_profile = subject.generate_stair_2d_profile(**kwargs) self.compare_data(generated_profile, expected_profile) + def test_create_generic_stair_zero_width_treads(self): + """Test generic stair with zero-width first and last treads""" + kwargs = { + "height": 1.0, + "number_of_treads": 3, + "stair_type": "GENERIC", + "tread_run": 0.3, + "width": 1.2, + "custom_first_last_tread_run": (0.0, 0.0), + } + verts_data = ( + V(0.0, 0, 0.0), + # First tread skipped + V(0.0, 0, 0.5), + V(0.3, 0, 0.5), + V(0.3, 0, 0.75), + V(0.6, 0, 0.75), + # Last tread skipped + V(0.6, 0, 0.0), + ) + edges_data = ( + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 5), + (5, 0), + ) + edges_data = [e[::-1] for e in edges_data] + + faces_data = () + expected_profile = (verts_data, edges_data, faces_data) + generated_profile = subject.generate_stair_2d_profile(**kwargs) + self.compare_data(generated_profile, expected_profile) + class TestUsingArrays(NewFile): def setup_array(self, add_second_layer=False, sync_children=False): diff --git a/src/ifc5d/README.md b/src/ifc5d/README.md index bcf72eefbc..63e133463e 100644 --- a/src/ifc5d/README.md +++ b/src/ifc5d/README.md @@ -96,11 +96,11 @@ writer.write() ### CLI app for converting IFC files to CSV, ODS or XLSX format. Usage: - python ifc5Dspreadsheet.py input_file output_file [-l log_file] [-f format_type] + python ifc5Dspreadsheet.py input_file output_dir [-l log_file] [-f format_type] Arguments: input_file (str): The path to the input IFC file to process. - output_file (str): The output directory for CSV or filename for other formats. + output_dir (str): The output directory. Options: -l, --log log_file (str): The path to the file where errors should be logged. Default is process.log. diff --git a/src/ifc5d/ifc5d/ifc5Dspreadsheet.py b/src/ifc5d/ifc5d/ifc5Dspreadsheet.py index d5f3bba22c..369232c401 100644 --- a/src/ifc5d/ifc5d/ifc5Dspreadsheet.py +++ b/src/ifc5d/ifc5d/ifc5Dspreadsheet.py @@ -319,8 +319,8 @@ class Ifc5Dwriter: cost_schedule: Optional[ifcopenshell.entity_instance] = None, ): """ - :param file: IFC file to exprot cost schedules from. - :param output: Output directory for csv files. + :param file: IFC file to export cost schedules from. + :param output: Output directory. :param cost_schedule: exported cost schedule. If not provided, will export all available schedules. """ self.output = output @@ -404,6 +404,7 @@ class Ifc5DCsvWriter(Ifc5Dwriter): import csv super().write() + os.makedirs(self.output, exist_ok=True) for sheet, data in self.sheet_data.items(): with open( os.path.join(self.output, "{}.csv".format(data["Name"])), "w", newline="", encoding="utf-8" @@ -436,6 +437,7 @@ class Ifc5DOdsWriter(Ifc5Dwriter): ns1.addElement(Number(decimalplaces="2", minintegerdigits="1", grouping="true")) self.doc.styles.addElement(ns1) + os.makedirs(self.output, exist_ok=True) file_name = "" for cost_schedule in self.cost_schedules: if file_name: @@ -537,6 +539,7 @@ class Ifc5DXlsxWriter(Ifc5Dwriter): import xlsxwriter super().write() + os.makedirs(self.output, exist_ok=True) file_name = "" for cost_schedule in self.cost_schedules: if file_name: @@ -683,7 +686,7 @@ class Ifc5DPdfWriter(Ifc5Dwriter): if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("input", type=str, help="Specify an IFC file to process") - parser.add_argument("output", help="The output directory for CSV or filename for other formats") + parser.add_argument("output", help="The output directory") parser.add_argument("-l", "--log", type=str, help="Specify where errors should be logged", default="process.log") parser.add_argument( "-f", "--format", type=str, help="Choose which format to export in (CSV/ODS/XLSX)", default="CSV" diff --git a/src/ifcconvert/IfcConvert.cpp b/src/ifcconvert/IfcConvert.cpp index 376373006f..c41c1eab80 100644 --- a/src/ifcconvert/IfcConvert.cpp +++ b/src/ifcconvert/IfcConvert.cpp @@ -207,7 +207,7 @@ size_t read_filters_from_file(const std::string&, inclusion_filter&, inclusion_t void parse_filter(geom_filter &, const std::vector&); std::vector setup_filters(const std::vector&, const std::string&); -bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, bool no_progress, bool mmap); +bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, bool no_progress, bool mmap, bool bypass_properties=false); // from https://stackoverflow.com/questions/31696328/boost-program-options-using-zero-parameter-options-multiple-times struct verbosity_counter { @@ -965,7 +965,10 @@ int main(int argc, char** argv) { time_t start,end; time(&start); - if (!init_input_file(IfcUtil::path::to_utf8(input_filename), ifc_file, no_progress || quiet, mmap)) { + // @nb last argument true -> bypass_properties which are not read by any of the geometry serializers + // XML, RocksDB, IFC are already special-cased above + // SVG requires properties for IfcAnnotation/DRAWING properties + if (!init_input_file(IfcUtil::path::to_utf8(input_filename), ifc_file, no_progress || quiet, mmap, output_extension != SVG)) { write_log(!quiet); serializer.reset(); IfcUtil::path::delete_file(IfcUtil::path::to_utf8(output_temp_filename)); /**< @todo Windows Unicode support */ @@ -1336,7 +1339,7 @@ void write_log(bool header) { #include -bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, bool no_progress, bool mmap) { +bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, bool no_progress, bool mmap, bool bypass_properties) { time_t start, end; // Prevent IfcFile::Init() prints by setting output to null temporarily @@ -1344,20 +1347,36 @@ bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file, time(&start); + bool requires_init = false; + #ifdef WITH_IFCXML if (boost::ends_with(boost::to_lower_copy(filename), ".ifcxml")) { ifc_file = IfcParse::parse_ifcxml(filename); - } else + } else #endif + { + ifc_file = new IfcParse::IfcFile(IfcParse::uninitialized_tag{}); + requires_init = true; + } - { + ifc_file->bypass_type("IfcRelDefinesByProperties"); + ifc_file->bypass_type("IfcPropertySetDefinition"); + ifc_file->bypass_type("IfcProperty"); + ifc_file->bypass_type("IfcMaterialProperties"); + ifc_file->bypass_type("IfcProfileProperties"); + ifc_file->bypass_type("IfcPhysicalQuantity"); + #ifdef USE_MMAP - ifc_file = new IfcParse::IfcFile(filename, mmap); + if (mmap) { + ifc_file->initialize(filename, mmap); + requires_init = false; + } #else - (void)mmap; - ifc_file = new IfcParse::IfcFile(filename); + (void)mmap; #endif - } + if (requires_init) { + ifc_file->initialize(filename); + } if (!ifc_file || !ifc_file->good()) { Logger::Error("Unable to parse input file '" + filename + "'"); diff --git a/src/ifcgeom/infra_sweep_helper.cpp b/src/ifcgeom/infra_sweep_helper.cpp index f3f79d4c18..f9d50f5b00 100644 --- a/src/ifcgeom/infra_sweep_helper.cpp +++ b/src/ifcgeom/infra_sweep_helper.cpp @@ -172,6 +172,7 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_, } auto interpolated_loop = polygon_from_points(points); + interpolated_loop->external = w1->external; if (interpolated->kind() == taxonomy::FACE) { std::static_pointer_cast(interpolated)->children.push_back(interpolated_loop); } else { diff --git a/src/ifcgeom/kernels/opencascade/loft.cpp b/src/ifcgeom/kernels/opencascade/loft.cpp index 0900717bc8..188150b614 100644 --- a/src/ifcgeom/kernels/opencascade/loft.cpp +++ b/src/ifcgeom/kernels/opencascade/loft.cpp @@ -118,7 +118,8 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re auto jt = it + 1; std::array fa = { *it, *jt }; std::array shps; - std::array ws; + std::vector> ws; + ws.emplace_back(); for (int i = 0; i < 2; ++i) { if (fa[i]->kind() == taxonomy::FACE) { if (!convert(std::static_pointer_cast(fa[i]), shps[i])) { @@ -135,11 +136,20 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re if (shps[i].ShapeType() != TopAbs_FACE && shps[i].ShapeType() != TopAbs_WIRE) { return false; } - // @todo this is only outer wire + if (shps[i].ShapeType() == TopAbs_FACE) { - ws[i] = BRepTools::OuterWire(TopoDS::Face(shps[i])); + ws[0][i] = BRepTools::OuterWire(TopoDS::Face(shps[i])); + size_t j = 1; + for (TopExp_Explorer exp(shps[i], TopAbs_WIRE); exp.More(); exp.Next()) { + if (exp.Current() != ws[0][i]) { + while (ws.size() <= j) { + ws.emplace_back(); + } + ws[j++][i] = TopoDS::Wire(exp.Current()); + } + } } else { - ws[i] = TopoDS::Wire(shps[i]); + ws[0][i] = TopoDS::Wire(shps[i]); } } if (shps[0].ShapeType() == TopAbs_FACE) { @@ -154,35 +164,38 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re BB.Add(comp, shps[1]); } } - BRepTools_WireExplorer a(ws[0]); - BRepTools_WireExplorer b(ws[1]); - for (; a.More() && b.More(); a.Next(), b.Next()) { - auto& e1 = a.Current(); - // auto e3 = TopoDS::Edge(b.Current().Reversed()); - auto& e3 = b.Current(); - // Documentation says unconnected edges are automatically connected, but this is not the case - TopoDS_Vertex e1a, e1b, e3a, e3b; - TopExp::Vertices(e1, e1a, e1b, true); - TopExp::Vertices(e3, e3a, e3b, true); - auto e2 = BRepBuilderAPI_MakeEdge(e1b, e3a).Edge(); - auto e4 = BRepBuilderAPI_MakeEdge(e3b, e1a).Edge(); - - /* - BRepFill_Filling fill; - fill.Add(e1, GeomAbs_C0); - fill.Add(e2, GeomAbs_C0); - fill.Add(e3, GeomAbs_C0); - fill.Add(e4, GeomAbs_C0); - fill.Build(); - // faces.Append(fill.Face()); - BB.Add(comp, fill.Face()); - */ + for (auto& wp : ws) { + BRepTools_WireExplorer a(wp[0]); + BRepTools_WireExplorer b(wp[1]); + for (; a.More() && b.More(); a.Next(), b.Next()) { + auto& e1 = a.Current(); + // auto e3 = TopoDS::Edge(b.Current().Reversed()); + auto& e3 = b.Current(); - auto f = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e1a, e1b, e3b, true).Wire()).Face(); - BB.Add(comp, f); - auto g = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e3b, e3a, e1a, true).Wire()).Face(); - BB.Add(comp, g); + // Documentation says unconnected edges are automatically connected, but this is not the case + TopoDS_Vertex e1a, e1b, e3a, e3b; + TopExp::Vertices(e1, e1a, e1b, true); + TopExp::Vertices(e3, e3a, e3b, true); + auto e2 = BRepBuilderAPI_MakeEdge(e1b, e3a).Edge(); + auto e4 = BRepBuilderAPI_MakeEdge(e3b, e1a).Edge(); + + /* + BRepFill_Filling fill; + fill.Add(e1, GeomAbs_C0); + fill.Add(e2, GeomAbs_C0); + fill.Add(e3, GeomAbs_C0); + fill.Add(e4, GeomAbs_C0); + fill.Build(); + // faces.Append(fill.Face()); + BB.Add(comp, fill.Face()); + */ + + auto f = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e1a, e1b, e3b, true).Wire()).Face(); + BB.Add(comp, f); + auto g = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e3b, e3a, e1a, true).Wire()).Face(); + BB.Add(comp, g); + } } } diff --git a/src/ifcgeom/mapping/IfcAxis2PlacementLinear.cpp b/src/ifcgeom/mapping/IfcAxis2PlacementLinear.cpp index 8e50f24c5c..b11b61b46b 100644 --- a/src/ifcgeom/mapping/IfcAxis2PlacementLinear.cpp +++ b/src/ifcgeom/mapping/IfcAxis2PlacementLinear.cpp @@ -25,62 +25,58 @@ using namespace ifcopenshell::geometry; taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAxis2PlacementLinear* inst) { - if (!inst->Location()->as()) - Logger::Error(std::runtime_error("Location must be IfcPointByDistanceExpression for IfcAxis2PlacementLinear")); + if (!inst->Location()->as()) { + Logger::Error(std::runtime_error("Location must be IfcPointByDistanceExpression for IfcAxis2PlacementLinear")); + } - Eigen::Vector3d o, axis(0, 0, 1), refDirection; + Eigen::Vector3d o, axis(0, 0, 1), refDirection; - taxonomy::matrix4::ptr m = taxonomy::cast(map(inst->Location())); - o = m->components().col(3).head<3>(); + taxonomy::matrix4::ptr m = taxonomy::cast(map(inst->Location())); + o = m->components().col(3).head<3>(); - // From 8.9.3.4 IfcAxis2PlacementLinear there are 4 cases that need to be considered - // 1) Axis is given but not RefDirection - // 2) RefDirection is given but not Axis - // 3) Neither Axis or RefDirection are provided - // 4) Both Axis and RefDirection are provided + // From 8.9.3.4 IfcAxis2PlacementLinear there are 4 cases that need to be considered + // 1) Axis is given but not RefDirection + // 2) RefDirection is given but not Axis + // 3) Neither Axis or RefDirection are provided + // 4) Both Axis and RefDirection are provided - const bool hasAxis = inst->Axis() != nullptr; - const bool hasRef = inst->RefDirection() != nullptr; + const bool hasAxis = inst->Axis() != nullptr; + const bool hasRef = inst->RefDirection() != nullptr; - if (hasAxis != hasRef) { + /* + if (hasAxis != hasRef) { Logger::Warning("Axis and RefDirection should be specified together", inst); } + */ - if (hasAxis && !hasRef) - { - taxonomy::direction3::ptr a = taxonomy::cast(map(inst->Axis())); - axis = *a->components_; + if (hasAxis && !hasRef) { + taxonomy::direction3::ptr a = taxonomy::cast(map(inst->Axis())); + axis = *a->components_; - refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted - // refDirection is not necessarily orthogonal to axis. - // axis.cross(refDirection) gives y. y.cross(axis) gives x=refDirection - refDirection = axis.cross(refDirection).cross(axis); - } - else if (!hasAxis && hasRef) - { - taxonomy::direction3::ptr r = taxonomy::cast(map(inst->RefDirection())); - refDirection = *r->components_; - Eigen::Vector3d up(0, 0, 1); - axis = refDirection.cross(up.cross(refDirection)); - } - else if (!hasAxis && !hasRef) - { - refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted - Eigen::Vector3d up(0, 0, 1); - axis = refDirection.cross(up.cross(refDirection)); - } - else - { - taxonomy::direction3::ptr a = taxonomy::cast(map(inst->Axis())); - axis = *a->components_; + refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted + // refDirection is not necessarily orthogonal to axis. + // axis.cross(refDirection) gives y. y.cross(axis) gives x=refDirection + refDirection = axis.cross(refDirection).cross(axis); + } else if (!hasAxis && hasRef) { + taxonomy::direction3::ptr r = taxonomy::cast(map(inst->RefDirection())); + refDirection = *r->components_; + Eigen::Vector3d up(0, 0, 1); + axis = refDirection.cross(up.cross(refDirection)); + } else if (!hasAxis && !hasRef) { + refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted + Eigen::Vector3d up(0, 0, 1); + axis = refDirection.cross(up.cross(refDirection)); + } else { + taxonomy::direction3::ptr a = taxonomy::cast(map(inst->Axis())); + axis = *a->components_; - taxonomy::direction3::ptr r = taxonomy::cast(map(inst->RefDirection())); - refDirection = *r->components_; - refDirection = axis.cross(refDirection).cross(axis); // refDirection needs to be orthogonal to axis - } + taxonomy::direction3::ptr r = taxonomy::cast(map(inst->RefDirection())); + refDirection = *r->components_; + refDirection = axis.cross(refDirection).cross(axis); // refDirection needs to be orthogonal to axis + } - // axis and refDirection need to be orthogonal - return taxonomy::make(o, axis, refDirection); + // axis and refDirection need to be orthogonal + return taxonomy::make(o, axis, refDirection); } #endif diff --git a/src/ifcgeom/mapping/IfcBoundingBox.cpp b/src/ifcgeom/mapping/IfcBoundingBox.cpp index 21085fb2bb..0de182a648 100644 --- a/src/ifcgeom/mapping/IfcBoundingBox.cpp +++ b/src/ifcgeom/mapping/IfcBoundingBox.cpp @@ -24,7 +24,7 @@ using namespace ifcopenshell::geometry; taxonomy::ptr mapping::map_impl(const IfcSchema::IfcBoundingBox* inst) { if (!settings_.get().get()) { - // @todo make sure it doesn't log. + failed_on_purpose_.insert(inst); return nullptr; } diff --git a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp index 82eea17648..a2bc648f08 100644 --- a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp +++ b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp @@ -81,7 +81,13 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in rot = taxonomy::matrix4( Eigen::Vector3d(0, 0, 0), taxonomy::cast(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0); - } + } else if (csp->RefDirection()) { + rot = taxonomy::matrix4( + Eigen::Vector3d(0, 0, 0), + Eigen::Vector3d(0, 0, 1), + taxonomy::cast(map(csp->RefDirection()))->ccomponents() + ).ccomponents().block<3, 3>(0, 0); + } profile_rotations.push_back(rot); } if (faces.size() != profile_offsets.size()) { diff --git a/src/ifcgeom/mapping/IfcSectionedSurface.cpp b/src/ifcgeom/mapping/IfcSectionedSurface.cpp index 1111d4dac5..c6b700f5e5 100644 --- a/src/ifcgeom/mapping/IfcSectionedSurface.cpp +++ b/src/ifcgeom/mapping/IfcSectionedSurface.cpp @@ -82,7 +82,14 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) { rot = taxonomy::matrix4( Eigen::Vector3d(0, 0, 0), taxonomy::cast(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0); - } + } else if (csp->RefDirection()) { + rot = taxonomy::matrix4( + Eigen::Vector3d(0, 0, 0), + Eigen::Vector3d(0, 0, 1), + taxonomy::cast(map(csp->RefDirection()))->ccomponents()) + .ccomponents() + .block<3, 3>(0, 0); + } profile_rotations.push_back(rot); } #else diff --git a/src/ifcgeom/mapping/mapping.cpp b/src/ifcgeom/mapping/mapping.cpp index b4685a7a86..004a41821c 100644 --- a/src/ifcgeom/mapping/mapping.cpp +++ b/src/ifcgeom/mapping/mapping.cpp @@ -372,6 +372,18 @@ const IfcUtil::IfcBaseEntity* mapping::get_single_material_association(const Ifc } } #endif + +#ifdef SCHEMA_HAS_IfcMaterialConstituentSet + if (associated_material->as()) { + IfcSchema::IfcMaterialConstituentSet* constituentset = associated_material->as(); + if (settings_.get().value ? constituentset->MaterialConstituents()->get()->size() >= 1 : constituentset->MaterialConstituents()->get()->size() == 1) { + IfcSchema::IfcMaterialConstituent* constituent = (*constituentset->MaterialConstituents()->get()->begin()); + if (auto* m_ = constituent->Material()) { + single_material = m_; + } + } + } +#endif } } } diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile index aa660d9a9e..c0ecb4ff1f 100644 --- a/src/ifcopenshell-python/Makefile +++ b/src/ifcopenshell-python/Makefile @@ -79,6 +79,11 @@ build-urls: test: pytest -p no:pytest-blender test --ignore=test/util/test_shape_builder.py +.PHONY: test-parallel +test-parallel: + @NPROCS=$$(nproc 2>/dev/null || sysctl -n hw.ncpu); \ + pytest -p no:pytest-blender -n $$NPROCS test --ignore=test/util/test_shape_builder.py + .PHONY: test-mathutils test-mathutils: pytest -p no:pytest-blender test/util/test_shape_builder.py diff --git a/src/ifcopenshell-python/ifcopenshell/__init__.py b/src/ifcopenshell-python/ifcopenshell/__init__.py index 74cee2d701..c12d619b58 100644 --- a/src/ifcopenshell-python/ifcopenshell/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/__init__.py @@ -60,6 +60,7 @@ import zipfile import tempfile from pathlib import Path from typing import Optional, Union, TYPE_CHECKING, Any, overload, Literal +from collections.abc import Sequence if TYPE_CHECKING: import ifcopenshell.express.schema_class @@ -138,7 +139,12 @@ def open( path: Union[os.PathLike, str], format: Optional[str] = None, *, should_stream: bool = False, readonly: bool = False ) -> Union[_file, sqlite, _stream]: ... def open( - path: Union[os.PathLike, str], format: Optional[str] = None, should_stream: bool = False, readonly: bool = False + path: Union[os.PathLike, str], + format: Optional[str] = None, + should_stream: bool = False, + readonly: bool = False, + mmap: bool = False, + bypass_types: Optional[Sequence[str]] = None, ) -> Union[_file, sqlite, _stream]: """Loads an IFC dataset from a filepath @@ -186,7 +192,17 @@ def open( if should_stream: return stream(path) if readonly: # Temporary conditional see #7131. Remove once newer builds don't segfault on Linux. - f = ifcopenshell_wrapper.open(str(path.absolute()), readonly) + f = ifcopenshell_wrapper.open(str(path.absolute()), readonly=readonly) + elif bypass_types: + f = ifcopenshell_wrapper.file(ifcopenshell_wrapper.uninitialized_tag()) + for ty in bypass_types: + f.bypass_type(ty) + if mmap: + f.initialize(str(path.absolute()), mmap=mmap) + else: + f.initialize(str(path.absolute())) + elif mmap: + f = ifcopenshell_wrapper.open(str(path.absolute()), mmap=mmap) else: f = ifcopenshell_wrapper.open(str(path.absolute())) return file(f) @@ -299,20 +315,44 @@ def guess_format(path: Path) -> Literal[".ifc", ".ifcZIP", ".ifcXML", ".ifcJSON" return None -def stream2(path: Union[Path, str]): +def stream2(path: Union[Path, str], mmap: bool = False, page_size: int = 0): """Streams the content of a file path from disk, yielding each instance as a dictionary. Args: path (Union[Path, str]): input file path + mmap (bool): open the file contents using memory mapping + page_size (int): open file in python and feed chunks to the parser Yields: dict: entity instance dictionaries """ - streamer = ifcopenshell_wrapper.InstanceStreamer(str(path)) - while streamer: - if inst := streamer.read_instance_py(): - yield inst + if page_size: + import builtins + + f = builtins.open(path, encoding="ascii") + strm = ifcopenshell_wrapper.InstanceStreamer() + strm.pushPage(f.read(page_size)) + finished = False + while True: + while strm.hasSemicolon(): + if inst := strm.readInstancePy(): + yield inst + else: + finished = True + break + if finished: + break + else: + if data := f.read(page_size): + strm.pushPage(data) + else: + break + else: + streamer = ifcopenshell_wrapper.InstanceStreamer(str(path), mmap) + while streamer: + if inst := streamer.readInstancePy(): + yield inst def stream2_from_string(data: str): diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py index 119ab82a14..ffde70580f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py @@ -143,7 +143,7 @@ class Usecase: return next(e for e in self.file.get_inverse(self.task_time) if e.is_a("IfcTask")) def handle_resource_calculation(self): - resources = ifcopenshell.util.sequence.get_task_resources(self.task, is_deep=False) + resources = ifcopenshell.util.sequence.get_task_resources(self.task, is_recursive=False) for resource in resources: if ifcopenshell.util.constraint.is_attribute_locked(resource, "Usage.ScheduleWork"): ifcopenshell.api.resource.calculate_resource_usage(self.file, resource=resource) diff --git a/src/ifcopenshell-python/ifcopenshell/express/cat.py b/src/ifcopenshell-python/ifcopenshell/express/cat.py new file mode 100644 index 0000000000..d6e7b2c880 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/cat.py @@ -0,0 +1,15 @@ +import sys, fileinput + +if sys.platform == "win32" and not hasattr(sys.stdout, 'buffer'): + import os, msvcrt + msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) + +files = sys.argv[1:] +if files[0] == '-o': + b = open(files[1], 'wb') + files = files[2:] +else: + b = getattr(sys.stdout, 'buffer', sys.stdout) + +for line in fileinput.input(files=files, mode='rb'): + b.write(line) diff --git a/src/ifcopenshell-python/ifcopenshell/express/run.bat b/src/ifcopenshell-python/ifcopenshell/express/run.bat new file mode 100644 index 0000000000..96fd58973d --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/run.bat @@ -0,0 +1,185 @@ +@ECHO OFF + +:: python bootstrap.py express.bnf > express_parser.py + +IF EXIST IFC2X3_TC1.exp ( + python express_parser.py IFC2X3_TC1.exp header implementation schema_class definitions + + IF EXIST Ifc2x3-schema.cpp ( + :: v0.6.0 + python cat.py -o ..\..\..\ifcparse\Ifc2x3.cpp txt/header_ifc2x3.txt Ifc2x3.cpp + python cat.py -o ..\..\..\ifcparse\Ifc2x3.h txt/header_ifc2x3.txt Ifc2x3.h + python cat.py -o ..\..\..\ifcparse\Ifc2x3-schema.cpp txt/header_ifc2x3.txt Ifc2x3-schema.cpp + python cat.py -o ..\..\..\ifcparse\Ifc2x3-definitions.h txt/header_ifc2x3.txt Ifc2x3-definitions.h + ) ELSE ( + :: v0.5.0 + python cat.py -o ..\..\..\ifcparse\Ifc2x3.cpp txt/header_ifc2x3.txt txt/ifndef_ifc4.txt Ifc2x3.cpp txt/endif.txt + python cat.py -o ..\..\..\ifcparse\Ifc2x3.h txt/header_ifc2x3.txt Ifc2x3.h + python cat.py -o ..\..\..\ifcparse\Ifc2x3enum.h txt/header_ifc2x3.txt Ifc2x3enum.h + python cat.py -o ..\..\..\ifcparse\Ifc2x3-latebound.cpp txt/header_ifc2x3.txt txt/ifndef_ifc4.txt Ifc2x3-latebound.cpp txt/endif.txt + python cat.py -o ..\..\..\ifcparse\Ifc2x3-latebound.h txt/header_ifc2x3.txt Ifc2x3-latebound.h + ) + + del *.cpp *.h +) + +IF EXIST IFC4_ADD2TC1.exp ( + python express_parser.py IFC4_ADD2TC1.exp header implementation schema_class definitions + + IF EXIST Ifc4-schema.cpp ( + :: v0.6.0 + python cat.py -o ..\..\..\ifcparse\Ifc4.cpp txt/header_ifc4.txt Ifc4.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4.h txt/header_ifc4.txt Ifc4.h + python cat.py -o ..\..\..\ifcparse\Ifc4-schema.cpp txt/header_ifc4.txt Ifc4-schema.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4-definitions.h txt/header_ifc4.txt Ifc4-definitions.h + ) ELSE ( + :: v0.5.0 + python cat.py -o ..\..\..\ifcparse\Ifc4.cpp txt/header_ifc4.txt txt/ifdef_ifc4.txt Ifc4.cpp txt/endif.txt + python cat.py -o ..\..\..\ifcparse\Ifc4.h txt/header_ifc4.txt Ifc4.h + python cat.py -o ..\..\..\ifcparse\Ifc4enum.h txt/header_ifc4.txt Ifc4enum.h + python cat.py -o ..\..\..\ifcparse\Ifc4-latebound.cpp txt/header_ifc4.txt txt/ifdef_ifc4.txt Ifc4-latebound.cpp txt/endif.txt + python cat.py -o ..\..\..\ifcparse\Ifc4-latebound.h txt/header_ifc4.txt Ifc4-latebound.h + ) + + del *.cpp *.h +) + +IF EXIST IFC4x1.exp ( + python express_parser.py IFC4x1.exp header implementation schema_class definitions + + IF EXIST Ifc4x1-schema.cpp ( + :: v0.6.0 + python cat.py -o ..\..\..\ifcparse\Ifc4x1.cpp txt/header_ifc4x1.txt Ifc4x1.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x1.h txt/header_ifc4x1.txt Ifc4x1.h + python cat.py -o ..\..\..\ifcparse\Ifc4x1-schema.cpp txt/header_ifc4x1.txt Ifc4x1-schema.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x1-definitions.h txt/header_ifc4x1.txt Ifc4x1-definitions.h + ) + + del *.cpp *.h +) + +IF EXIST IFC4x2.exp ( + python express_parser.py IFC4x2.exp header implementation schema_class definitions + + IF EXIST Ifc4x2-schema.cpp ( + :: v0.6.0 + python cat.py -o ..\..\..\ifcparse\Ifc4x2.cpp txt/header_ifc4x2.txt Ifc4x2.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x2.h txt/header_ifc4x2.txt Ifc4x2.h + python cat.py -o ..\..\..\ifcparse\Ifc4x2-schema.cpp txt/header_ifc4x2.txt Ifc4x2-schema.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x2-definitions.h txt/header_ifc4x2.txt Ifc4x2-definitions.h + ) + + del *.cpp *.h +) + +IF EXIST IFC4x3_RC1.exp ( + python express_parser.py IFC4x3_RC1.exp header implementation schema_class definitions + + IF EXIST Ifc4x3_rc1-schema.cpp ( + :: v0.6.0 + python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc1.cpp txt/header_ifc4x3_rc1.txt Ifc4x3_rc1.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc1.h txt/header_ifc4x3_rc1.txt Ifc4x3_rc1.h + python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc1-schema.cpp txt/header_ifc4x3_rc1.txt Ifc4x3_rc1-schema.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc1-definitions.h txt/header_ifc4x3_rc1.txt Ifc4x3_rc1-definitions.h + ) + + del *.cpp *.h +) + +IF EXIST IFC4x3_RC2.exp ( + python express_parser.py IFC4x3_RC2.exp header implementation schema_class definitions + + IF EXIST Ifc4x3_rc2-schema.cpp ( + :: v0.6.0 + python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc2.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc2.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc2.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc2.h + python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc2-schema.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc2-schema.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc2-definitions.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc2-definitions.h + ) + + del *.cpp *.h +) + +IF EXIST IFC4x3_RC3.exp ( + python express_parser.py IFC4x3_RC3.exp header implementation schema_class definitions + + IF EXIST Ifc4x3_rc3-schema.cpp ( + :: v0.6.0 + python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc3.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc3.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc3.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc3.h + python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc3-schema.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc3-schema.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc3-definitions.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc3-definitions.h + ) + + del *.cpp *.h +) + +IF EXIST IFC4x3_RC4.exp ( + python express_parser.py IFC4x3_RC4.exp header implementation schema_class definitions + + IF EXIST Ifc4x3_rc4-schema.cpp ( + :: v0.6.0 + python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc4.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc4.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc4.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc4.h + python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc4-schema.cpp txt/header_ifc4x3_rc2.txt Ifc4x3_rc4-schema.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x3_rc4-definitions.h txt/header_ifc4x3_rc2.txt Ifc4x3_rc4-definitions.h + ) + + del *.cpp *.h +) + +IF EXIST IFC4X3.exp ( + python express_parser.py IFC4X3.exp header implementation schema_class definitions + + IF EXIST Ifc4x3-schema.cpp ( + :: v0.6.0 + python cat.py -o ..\..\..\ifcparse\Ifc4x3.cpp txt/header_ifc4x3_rc2.txt Ifc4x3.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x3.h txt/header_ifc4x3_rc2.txt Ifc4x3.h + python cat.py -o ..\..\..\ifcparse\Ifc4x3-schema.cpp txt/header_ifc4x3_rc2.txt Ifc4x3-schema.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x3-definitions.h txt/header_ifc4x3_rc2.txt Ifc4x3-definitions.h + ) + + del *.cpp *.h +) + +IF EXIST IFC4X3_TC1.exp ( + python express_parser.py IFC4X3_TC1.exp header implementation schema_class definitions + + IF EXIST Ifc4x3_tc1-schema.cpp ( + :: v0.6.0 + python cat.py -o ..\..\..\ifcparse\Ifc4x3_tc1.cpp txt/header_ifc4x3_tc1.txt Ifc4x3_tc1.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x3_tc1.h txt/header_ifc4x3_tc1.txt Ifc4x3_tc1.h + python cat.py -o ..\..\..\ifcparse\Ifc4x3_tc1-schema.cpp txt/header_ifc4x3_tc1.txt Ifc4x3_tc1-schema.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x3_tc1-definitions.h txt/header_ifc4x3_tc1.txt Ifc4x3_tc1-definitions.h + ) + + del *.cpp *.h +) + +IF EXIST IFC4X3_ADD1.exp ( + python express_parser.py IFC4X3_ADD1.exp header implementation schema_class definitions + + IF EXIST Ifc4x3_add1-schema.cpp ( + :: v0.6.0 + python cat.py -o ..\..\..\ifcparse\Ifc4x3_add1.cpp txt/header_ifc4x3_add1.txt Ifc4x3_add1.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x3_add1.h txt/header_ifc4x3_add1.txt Ifc4x3_add1.h + python cat.py -o ..\..\..\ifcparse\Ifc4x3_add1-schema.cpp txt/header_ifc4x3_add1.txt Ifc4x3_add1-schema.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x3_add1-definitions.h txt/header_ifc4x3_add1.txt Ifc4x3_add1-definitions.h + ) + + del *.cpp *.h +) + +IF EXIST IFC4X3_ADD2.exp ( + python express_parser.py IFC4X3_ADD2.exp header implementation schema_class definitions + + IF EXIST Ifc4x3_add2-schema.cpp ( + :: v0.6.0 + python cat.py -o ..\..\..\ifcparse\Ifc4x3_add2.cpp txt/header_ifc4x3_add2.txt Ifc4x3_add2.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x3_add2.h txt/header_ifc4x3_add2.txt Ifc4x3_add2.h + python cat.py -o ..\..\..\ifcparse\Ifc4x3_add2-schema.cpp txt/header_ifc4x3_add2.txt Ifc4x3_add2-schema.cpp + python cat.py -o ..\..\..\ifcparse\Ifc4x3_add2-definitions.h txt/header_ifc4x3_add2.txt Ifc4x3_add2-definitions.h + ) + + del *.cpp *.h +) diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/endif.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/endif.txt new file mode 100644 index 0000000000..69331c3ca9 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/txt/endif.txt @@ -0,0 +1 @@ +#endif diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc2x3.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc2x3.txt new file mode 100644 index 0000000000..2487658eb8 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc2x3.txt @@ -0,0 +1,25 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 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 * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +/******************************************************************************** + * * + * This file has been generated from IFC2X3_TC1.exp. Do not make modifications * + * but instead modify the python script that has been used to generate this. * + * * + ********************************************************************************/ diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4.txt new file mode 100644 index 0000000000..52da1accf8 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4.txt @@ -0,0 +1,25 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 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 * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +/******************************************************************************** + * * + * This file has been generated from IFC4.exp. Do not make modifications * + * but instead modify the python script that has been used to generate this. * + * * + ********************************************************************************/ diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x1.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x1.txt new file mode 100644 index 0000000000..dfacb0e04b --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x1.txt @@ -0,0 +1,25 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 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 * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +/******************************************************************************** + * * + * This file has been generated from IFC4x1.exp. Do not make modifications * + * but instead modify the python script that has been used to generate this. * + * * + ********************************************************************************/ diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x2.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x2.txt new file mode 100644 index 0000000000..5f6f1f75a7 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x2.txt @@ -0,0 +1,25 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 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 * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +/******************************************************************************** + * * + * This file has been generated from IFC4x2.exp. Do not make modifications * + * but instead modify the python script that has been used to generate this. * + * * + ********************************************************************************/ diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_add1.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_add1.txt new file mode 100644 index 0000000000..85aac44883 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_add1.txt @@ -0,0 +1,25 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 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 * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +/******************************************************************************** + * * + * This file has been generated from IFC4X3_ADD1.exp. Do not make modifications * + * but instead modify the python script that has been used to generate this. * + * * + ********************************************************************************/ diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_add2.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_add2.txt new file mode 100644 index 0000000000..6616f73b69 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_add2.txt @@ -0,0 +1,25 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 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 * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +/******************************************************************************** + * * + * This file has been generated from IFC4X3_ADD2.exp. Do not make modifications * + * but instead modify the python script that has been used to generate this. * + * * + ********************************************************************************/ diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_rc1.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_rc1.txt new file mode 100644 index 0000000000..208e687c24 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_rc1.txt @@ -0,0 +1,25 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 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 * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +/******************************************************************************** + * * + * This file has been generated from IFC4x3_RC1.exp. Do not make modifications * + * but instead modify the python script that has been used to generate this. * + * * + ********************************************************************************/ diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_rc2.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_rc2.txt new file mode 100644 index 0000000000..5d228771fd --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_rc2.txt @@ -0,0 +1,25 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 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 * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +/******************************************************************************** + * * + * This file has been generated from IFC4x3_RC2.exp. Do not make modifications * + * but instead modify the python script that has been used to generate this. * + * * + ********************************************************************************/ diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_tc1.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_tc1.txt new file mode 100644 index 0000000000..cb46c3274f --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/txt/header_ifc4x3_tc1.txt @@ -0,0 +1,25 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 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 * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +/******************************************************************************** + * * + * This file has been generated from IFC4X3_TC1.exp. Do not make modifications * + * but instead modify the python script that has been used to generate this. * + * * + ********************************************************************************/ diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/ifdef_ifc4.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/ifdef_ifc4.txt new file mode 100644 index 0000000000..cd56d7026e --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/txt/ifdef_ifc4.txt @@ -0,0 +1,2 @@ + +#ifdef USE_IFC4 diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/ifdef_ifc4x1.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/ifdef_ifc4x1.txt new file mode 100644 index 0000000000..1f3194a48d --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/txt/ifdef_ifc4x1.txt @@ -0,0 +1,2 @@ + +#ifdef USE_IFC4X1 diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4.txt new file mode 100644 index 0000000000..f662cbf4cf --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4.txt @@ -0,0 +1,2 @@ + +#ifndef USE_IFC4 diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4x1 - Copy.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4x1 - Copy.txt new file mode 100644 index 0000000000..a5826d1e34 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4x1 - Copy.txt @@ -0,0 +1,2 @@ + +#ifndef USE_IFC4X1 diff --git a/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4x1.txt b/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4x1.txt new file mode 100644 index 0000000000..a5826d1e34 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/express/txt/ifndef_ifc4x1.txt @@ -0,0 +1,2 @@ + +#ifndef USE_IFC4X1 diff --git a/src/ifcopenshell-python/ifcopenshell/file.py b/src/ifcopenshell-python/ifcopenshell/file.py index a93c9b5dea..c7350a13da 100644 --- a/src/ifcopenshell-python/ifcopenshell/file.py +++ b/src/ifcopenshell-python/ifcopenshell/file.py @@ -248,6 +248,10 @@ READ_ERROR = ifcopenshell_wrapper.file_open_status.READ_ERROR NO_HEADER = ifcopenshell_wrapper.file_open_status.NO_HEADER UNSUPPORTED_SCHEMA = ifcopenshell_wrapper.file_open_status.UNSUPPORTED_SCHEMA INVALID_SYNTAX = ifcopenshell_wrapper.file_open_status.INVALID_SYNTAX +try: + UNKNOWN = ifcopenshell_wrapper.file_open_status.UNKNOWN +except: + UNKNOWN = 5 # Workaround import struct @@ -496,6 +500,24 @@ class rocksdb_file_storage: return "".join("".join(map(str, t)) if t[1] else "" for t in zip(prefixes, version_tuple[0:2])) +class file_header: + def __init__(self, file, header_data): + self.file = file + self.header_data = header_data + + @property + def file_description(self) -> entity_instance: + return entity_instance.wrap_value(self.header_data.file_description_py(), file=self.file) + + @property + def file_name(self) -> entity_instance: + return entity_instance.wrap_value(self.header_data.file_name_py(), file=self.file) + + @property + def file_schema(self) -> entity_instance: + return entity_instance.wrap_value(self.header_data.file_schema_py(), file=self.file) + + class file: """Base class for containing IFC files. @@ -513,7 +535,7 @@ class file: """ wrapped_data: ifcopenshell_wrapper.file - header: ifcopenshell_wrapper.IfcSpfHeader + header: file_header units: dict[str, entity_instance] = {} history_size: int = 64 history: list[Transaction] @@ -586,8 +608,11 @@ class file: "Unsupported schema: %s" % ",".join(self.header.file_schema.schema_identifiers), ), INVALID_SYNTAX: lambda: (Error, "Syntax error during parse, check logs"), + # This is the case when passing uninitialized_tag + UNKNOWN: lambda: (None, None), }[f.good().value()]() - raise exc(msg) + if exc is not None: + raise exc(msg) else: args = filter(None, [schema]) args = map(ifcopenshell_wrapper.schema_by_name, args) @@ -1025,12 +1050,10 @@ class file: @property def header(self): - try: # Temporary workaround until new builds are ready. See #7131. - h = self.wrapped_data.header() + try: + return file_header(self, self.wrapped_data.header()) except: return self.wrapped_data.header - object.__setattr__(h, "file_ref", lambda inst: entity_instance.wrap_value(inst, file=self)) - return h @property def storage(self) -> Optional[rocksdb_file_storage]: diff --git a/src/ifcopenshell-python/ifcopenshell/geom/stats.py b/src/ifcopenshell-python/ifcopenshell/geom/stats.py new file mode 100644 index 0000000000..364083acd7 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/geom/stats.py @@ -0,0 +1,246 @@ +import sys +from typing import Optional, IO +from itertools import accumulate + +from . import ifcopenshell_wrapper + + +class TransformDefaultDict: + def __init__(self, keyfunc=lambda x: x, default_factory=None): + """ + Similar to collections.defaultdict, but allows for storing + non-hashable keys by means of a transform function + """ + self.keyfunc = keyfunc + self.default_factory = default_factory + self._data = {} + self._original_keys = {} + + def __setitem__(self, key, value): + k = self.keyfunc(key) + self._data[k] = value + self._original_keys[k] = key + + def __getitem__(self, key): + k = self.keyfunc(key) + if k in self._data: + return self._data[k] + if self.default_factory is not None: + value = self.default_factory() + self._data[k] = value + self._original_keys[k] = key + return value + raise KeyError(key) + + def __contains__(self, key): + return self.keyfunc(key) in self._data + + def __delitem__(self, key): + k = self.keyfunc(key) + del self._data[k] + del self._original_keys[k] + + def __iter__(self): + return iter(self._original_keys.values()) + + def __len__(self): + return len(self._data) + + def items(self): + return ((self._original_keys[k], v) for k, v in self._data.items()) + + def keys(self): + return self._original_keys.values() + + def values(self): + return self._data.values() + + def get(self, key, default=None): + return self._data.get(self.keyfunc(key), default) + + def setdefault(self, key, default=None): + k = self.keyfunc(key) + if k not in self._data: + self._data[k] = default + self._original_keys[k] = key + return self._data[k] + + def update(self, other): + for k, v in other.items(): + self[k] = v + + def clear(self): + self._data.clear() + self._original_keys.clear() + + def __repr__(self): + items = ", ".join(f"{k!r}: {v!r}" for k, v in self.items()) + return f"{self.__class__.__name__}({{{items}}})" + + +class ProductCounter: + def __init__(self, only_with_representation=True): + self.total = 0 + transform_func = lambda decl: decl.name() if hasattr(decl, "name") else decl + self.counts = TransformDefaultDict(keyfunc=transform_func, default_factory=int) + self.counts_excl = TransformDefaultDict(keyfunc=transform_func, default_factory=int) + self.only_with_representation = only_with_representation + + def count(self, decl, values): + if decl._is("IfcProduct"): + if self.only_with_representation and values.get("Representation") is None: + return + self.counts_excl[decl] += 1 + while decl.name() != "IfcProduct": + self.counts[decl] += 1 + decl = decl.supertype() + self.total += 1 + + def print(self, out=sys.stdout): + if self.counts: + results = [] + + def process(ent, level=-1): + if ent in self.counts: + results.append((level, ent.name(), self.counts[ent])) + for subtype in ent.subtypes(): + process(subtype, level=level + 1) + + prod = next(iter(self.counts.keys())).schema().declaration_by_name("ifcproduct") + process(prod) + max_width = max(level * 2 + len(name) for level, name, _ in results) + for level, name, count in results: + print(" " * (level * 2), f"{name:<{max_width - level * 2}}", ": ", count, file=out, sep="") + + +class BooleanResultCounter: + def __init__(self, exclude_union=True): + self.total = 0 + self.exclude_union = exclude_union + + def count(self, decl, values): + if decl._is("IfcBooleanResult"): + if self.exclude_union and values.get("Operator") == "UNION": + return + self.total += 1 + + def print(self, out=sys.stdout): + print("IfcBooleanResult:", self.total, file=out) + + +class StatsCollector: + streamer: ifcopenshell_wrapper.InstanceStreamer + page_size: Optional[int] + file_stream: Optional[IO[str]] + + finalized: bool = False + needs_data: bool = False + + counters: list + + num_semis: int = 0 + + def __init__(self): + self.streamer = ifcopenshell_wrapper.InstanceStreamer() + self.needs_data = True + self.counters = [ProductCounter(), BooleanResultCounter()] + + def feedFromFile(self, f: Optional[IO[str]] = None): + if f: + self.file_stream = f + self.feed(self.file_stream.read(self.page_size)) + + def feed(self, data: str): + self.streamer.pushPage(data) + self.needs_data = False + self.num_semis = self.streamer.semicolonCount() + + @staticmethod + def fromFilePath(fn, page_size: int = 102400): + collector = StatsCollector() + collector.page_size = page_size + collector.feedFromFile(open(str(fn), encoding="ascii")) + return collector + + def next(self): + if self.num_semis > 0: + if inst := self.streamer.readInstancePy(True): + self.num_semis -= 1 + return inst["type"], dict(list(inst.items())[2:]) + else: + self.finalized = True + return None + elif self.file_stream: + self.feedFromFile() + return self.next() + else: + self.needs_data = True + return None + + def process(self): + if n := self.next(): + decl, values = n + else: + return + if decl.schema().name() == "HEADER_SECTION_SCHEMA": + return + for cnt in self.counters: + cnt.count(decl, values) + + def print(self, out=sys.stdout): + for cnt in self.counters: + print(type(cnt).__name__, file=out) + print("=" * len(type(cnt).__name__), file=out) + cnt.print(out) + + def includeElementTypesBasedOnBudget(self, priorities: dict, budget): + def specificity(decl, target): + if decl._is(target): + i = 0 + while decl.name() != target: + decl = decl.supertype() + i += 1 + return i + return None + + def calc_prio(ty): + # find most specific priority directive for the given type in ty + return min(((specificity(ty, k), v) for k, v in priorities.items() if ty._is(k)), default=(0, 0))[1] + + sorted_types = sorted(self.counters[0].counts_excl.keys(), key=calc_prio, reverse=True) + sorted_types = [s for s in sorted_types if not s._is("IfcFeatureElement")] + counts = [self.counters[0].counts_excl[v] for v in sorted_types] + + ccounts = list(accumulate(counts)) + + if ccounts[0] >= budget: + # At the very least include one type + return sorted_types[0:1] + + for i, s in enumerate(ccounts): + if s >= budget: + over = s - budget + under = budget - ccounts[i - 1] + k = i if over <= under else i - 1 + return sorted_types[0 : k + 1] + + # We're lucky we can render everything in budget + return sorted_types[:] + + +if __name__ == "__main__": + collector = StatsCollector.fromFilePath(sys.argv[1]) + while not collector.finalized: + collector.process() + collector.print() + print( + *collector.includeElementTypesBasedOnBudget( + { + "IfcWall": 3, + "IfcSlab": 3, + "IfcWindow": 2, + "IfcDoor": 2, + "IfcBuildingElement": 1, + } + ) + ) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 48d65ae7ae..bf870cb1ed 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -117,6 +117,7 @@ def get_pset( if ( psets_only and not pset.is_a("IfcPropertySet") + and not pset.is_a("IfcPreDefinedPropertySet") and not (is_ifc2x3 and pset.is_a("IfcExtendedMaterialProperties")) ): pset = None @@ -126,7 +127,11 @@ def get_pset( if type_pset is not None and not prop: if psets_only or qtos_only: type_pset_element = element.file.by_id(type_pset["id"]) - if psets_only and not type_pset_element.is_a("IfcPropertySet"): + if ( + psets_only + and not type_pset_element.is_a("IfcPropertySet") + and not type_pset_element.is_a("IfcPreDefinedPropertySet") + ): type_pset = None elif qtos_only and not type_pset_element.is_a("IfcElementQuantity"): type_pset = None @@ -177,7 +182,7 @@ def get_psets( psets = {} if element.is_a("IfcTypeObject"): for definition in element.HasPropertySets or []: - if psets_only and not definition.is_a("IfcPropertySet"): + if psets_only and not definition.is_a("IfcPropertySet") and not definition.is_a("IfcPreDefinedPropertySet"): continue if qtos_only and not definition.is_a("IfcElementQuantity"): continue @@ -213,7 +218,11 @@ def get_psets( for relationship in is_defined_by: if relationship.is_a("IfcRelDefinesByProperties"): definition = relationship.RelatingPropertyDefinition - if psets_only and not definition.is_a("IfcPropertySet"): + if ( + psets_only + and not definition.is_a("IfcPropertySet") + and not definition.is_a("IfcPreDefinedPropertySet") + ): continue if qtos_only and not definition.is_a("IfcElementQuantity"): continue @@ -565,6 +574,40 @@ def get_predefined_type(element: ifcopenshell.entity_instance) -> Union[str, Non return predefined_type +def is_userdefined_type(element: ifcopenshell.entity_instance) -> bool: + """Checks if the predefined type is userdefined + + :param element: The IFC Element entity + :return: True if userdefined + + Example: + + .. code:: python + + element = ifcopenshell.by_type("IfcWall")[0] + is_userdefined_type = ifcopenshell.util.element.is_userdefined_type(element) + """ + if element_type := get_type(element): + predefined_type = getattr(element_type, "PredefinedType", None) + if predefined_type == "USERDEFINED": + return True + elif not predefined_type: + predefined_type = getattr(element_type, "ElementType", ...) + if predefined_type == ...: + predefined_type = getattr(element_type, "ProcessType", None) + if predefined_type: + return True + if predefined_type and predefined_type != "NOTDEFINED": + return False + + predefined_type = getattr(element, "PredefinedType", None) + if predefined_type == "USERDEFINED": + return True + elif not predefined_type: + return bool(getattr(element, "ObjectType", None)) + return False + + def get_type(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: """Retrieves the construction type element of an element occurrence. diff --git a/src/ifcopenshell-python/ifcopenshell/util/sequence.py b/src/ifcopenshell-python/ifcopenshell/util/sequence.py index 930184ecdb..1452aa011c 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/util/sequence.py @@ -329,61 +329,28 @@ def guess_date_range(work_schedule: ifcopenshell.entity_instance): return earliest, latest -def get_direct_task_outputs(task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: - return [rel.RelatingProduct for rel in task.HasAssignments if rel.is_a("IfcRelAssignsToProduct")] +def get_task_outputs( + task: ifcopenshell.entity_instance, is_recursive: bool = False +) -> set[ifcopenshell.entity_instance]: + if is_recursive: + return {o for subtask in [task] + list(get_all_nested_tasks(task)) for o in get_task_outputs(subtask)} + return {rel.RelatingProduct for rel in task.HasAssignments if rel.is_a("IfcRelAssignsToProduct")} -def get_task_outputs(task: ifcopenshell.entity_instance, is_deep: bool = False) -> list[ifcopenshell.entity_instance]: - if not is_deep: - return get_direct_task_outputs(task) - else: - return [output for nested_task in get_all_nested_tasks(task) for output in get_direct_task_outputs(nested_task)] +def get_task_inputs( + task: ifcopenshell.entity_instance, is_recursive: bool = False +) -> set[ifcopenshell.entity_instance]: + if is_recursive: + return {o for subtask in [task] + list(get_all_nested_tasks(task)) for o in get_task_inputs(subtask)} + return {o for rel in task.OperatesOn for o in rel.RelatedObjects if o.is_a("IfcProduct")} -def get_task_inputs(task: ifcopenshell.entity_instance, is_deep: bool = False) -> list[ifcopenshell.entity_instance]: - if not is_deep: - return [ - object - for rel in task.OperatesOn - if rel.is_a("IfcRelAssignsToProcess") - for object in rel.RelatedObjects - if object.is_a("IfcProduct") - ] - else: - return [ - output - for nested_task in get_all_nested_tasks(task) - for output in [ - object - for rel in nested_task.OperatesOn - if rel.is_a("IfcRelAssignsToProcess") - for object in rel.RelatedObjects - if object.is_a("IfcProduct") - ] - ] - - -def get_task_resources(task: ifcopenshell.entity_instance, is_deep: bool = False) -> list[ifcopenshell.entity_instance]: - if not is_deep: - return [ - object - for rel in task.OperatesOn - if rel.is_a("IfcRelAssignsToProcess") - for object in rel.RelatedObjects - if object.is_a("IfcResource") - ] - else: - return [ - resource - for nested_task in get_all_nested_tasks(task) - for resource in [ - object - for rel in nested_task.OperatesOn - if rel.is_a("IfcRelAssignsToProcess") - for object in rel.RelatedObjects - if object.is_a("IfcResource") - ] - ] +def get_task_resources( + task: ifcopenshell.entity_instance, is_recursive: bool = False +) -> set[ifcopenshell.entity_instance]: + if is_recursive: + return {r for subtask in [task] + list(get_all_nested_tasks(task)) for r in get_task_resources(subtask)} + return {o for rel in task.OperatesOn for o in rel.RelatedObjects if o.is_a("IfcResource")} def has_task_outputs(task: ifcopenshell.entity_instance) -> bool: diff --git a/src/ifcopenshell-python/ifcopenshell/validate.py b/src/ifcopenshell-python/ifcopenshell/validate.py index 6bb00992c0..25f9b3a685 100644 --- a/src/ifcopenshell-python/ifcopenshell/validate.py +++ b/src/ifcopenshell-python/ifcopenshell/validate.py @@ -320,14 +320,16 @@ def log_internal_cpp_errors( chr_offset_re = re.compile(r"at offset (\d+)\s*") for_instance_re = re.compile(r"\s*for instance #(\d+)\s*") + for_header_ent_re = re.compile(r"\s*for header entity (\w+)") if log_content is None: log_content = ifcopenshell.get_log() msgs = list(map(json.loads, filter(None, log_content.split("\n")))) chr_offsets = [chr_offset_re.findall(m["message"]) for m in msgs] instance_messages = [for_instance_re.findall(m["message"]) for m in msgs] + header_messages = [for_header_ent_re.findall(m["message"]) for m in msgs] - if chr_offsets or (instance_messages and f is None): + if any(chr_offsets) or (any(instance_messages) and f is None): # The file is opened in binary mode, in order # to correspond with the offsets reported by # IfcOpenShell C++ @@ -348,7 +350,7 @@ def log_internal_cpp_errors( else: logger.error("For instance:\n %s\n%s", line, m) - if instance_messages: + if any(instance_messages): for instid, msg in zip(instance_messages, msgs): if instid: m = for_instance_re.sub("", msg["message"]) @@ -376,6 +378,12 @@ def log_internal_cpp_errors( else: logger.error(m) + if any(header_messages): + for hent, msg in zip(header_messages, msgs): + if hent: + m = msg["message"] + logger.error(m) + entity_attribute_map: dict[tuple[str, str], tuple[entity_type, tuple[attribute, ...]]] = {} diff --git a/src/ifcopenshell-python/test/fixtures/rules/pass-rigid-op-IfcPlaneAngleMeasure-IfcPlaneAngleMeasure-ifc4x3_add2.ifc b/src/ifcopenshell-python/test/fixtures/rules/pass-rigid-op-IfcPlaneAngleMeasure-IfcPlaneAngleMeasure-ifc4x3_add2.ifc index 9ac949de12..8946080689 100644 --- a/src/ifcopenshell-python/test/fixtures/rules/pass-rigid-op-IfcPlaneAngleMeasure-IfcPlaneAngleMeasure-ifc4x3_add2.ifc +++ b/src/ifcopenshell-python/test/fixtures/rules/pass-rigid-op-IfcPlaneAngleMeasure-IfcPlaneAngleMeasure-ifc4x3_add2.ifc @@ -1,12 +1,12 @@ ISO-10303-21; HEADER; FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); -FILE_NAME('','2023-01-11T09:47:45',(),(),'IfcOpenShell v0.7.0-8a108f37','IfcOpenShell v0.7.0-8a108f37',''); +FILE_NAME('','2023-01-11T09:47:45',(''),(''),'IfcOpenShell v0.7.0-8a108f37','IfcOpenShell v0.7.0-8a108f37',''); FILE_SCHEMA(('IFC4X3_ADD2')); ENDSEC; DATA; -#1=IFCGEOGRAPHICCRS('EPSG:4326',$,$,$,$); -#2=IFCGEOGRAPHICCRS('EPSG:3857',$,$,$,$); +#1=IFCGEOGRAPHICCRS('EPSG:4326',$,$,$,$,$); +#2=IFCGEOGRAPHICCRS('EPSG:3857',$,$,$,$,$); #3=IFCRIGIDOPERATION(#1,#2,IFCPLANEANGLEMEASURE(0.),IFCPLANEANGLEMEASURE(0.),0.); ENDSEC; END-ISO-10303-21; diff --git a/src/ifcopenshell-python/test/geom/stats.py b/src/ifcopenshell-python/test/geom/stats.py new file mode 100644 index 0000000000..fd0cd8e337 --- /dev/null +++ b/src/ifcopenshell-python/test/geom/stats.py @@ -0,0 +1,26 @@ +import os +import ifcopenshell +from ifcopenshell.geom.stats import StatsCollector +import pytest + + +@pytest.mark.parametrize( + "file", + [os.path.join(os.path.dirname(__file__), "../../../../test/input/Allplan 2017 Test Bauteil-Oberflächen.ifc")], +) +def test_stats(file): + collector = StatsCollector.fromFilePath(file) + while not collector.finalized: + collector.process() + collector.print() + prio = { + "IfcWall": 2, + "IfcBuildingElement": 1, + "IfcBuildingElementProxy": -1, + } + ents = [a.name() for a in collector.includeElementTypesBasedOnBudget(prio, 1)] + assert ents == ["IfcWall"] or ents == ["IfcWallStandardCase"] + ents = [a.name() for a in collector.includeElementTypesBasedOnBudget(prio, 100)] + assert "IfcBuildingElementProxy" in ents + ents = [a.name() for a in collector.includeElementTypesBasedOnBudget(prio, 50)] + assert "IfcBuildingElementProxy" not in ents diff --git a/src/ifcopenshell-python/test/test_streaming_rocksdb_and_simpletyperefs.py b/src/ifcopenshell-python/test/test_streaming_rocksdb_and_simpletyperefs.py index 5cb3fccf85..b0bfb2925a 100644 --- a/src/ifcopenshell-python/test/test_streaming_rocksdb_and_simpletyperefs.py +++ b/src/ifcopenshell-python/test/test_streaming_rocksdb_and_simpletyperefs.py @@ -23,6 +23,11 @@ import pytest import ifcopenshell import tempfile +try: + import psutil +except ImportError: + psutil = None + fn = os.path.join(os.path.dirname(__file__), "fixtures/ColumnPSetsOfSets.ifc") @@ -33,12 +38,50 @@ def test_stream(): } +def test_chunked_stream(): + assert list(ifcopenshell.stream2(fn)) == list(ifcopenshell.stream2(fn, page_size=1024)) + + +def test_mmaped_stream(): + assert list(ifcopenshell.stream2(fn)) == list(ifcopenshell.stream2(fn, mmap=True)) + + def test_file(): f = ifcopenshell.open(fn) assert f[139].RelatingPropertyDefinition.is_a("IfcPropertySetDefinitionSet") assert {x.id() for x in f[139].RelatingPropertyDefinition[0]} == {136, 138} +def test_partial_open(): + f = ifcopenshell.open(fn) + assert len(f.by_type("ifccartesianpoint")) + f = ifcopenshell.open(fn, bypass_types=("IfcRepresentationItem",)) + assert len(f.by_type("ifccartesianpoint")) == 0 + + +def test_opening_unicode(): + import ifcopenshell.template + + with tempfile.TemporaryDirectory() as d: + fn = os.path.join(d, "ხყჯ𐰢ᨕதకᎣᚱᾗ.ifc") + f = ifcopenshell.template.create() + f.write(fn) + g = ifcopenshell.open(fn) + assert g.by_type("ifcproject") + + +@pytest.mark.skipif(psutil is None, reason="psutil not installed") +def test_memusage_partial_open(): + m0 = psutil.Process().memory_info().rss + f = ifcopenshell.open(fn) + m1 = psutil.Process().memory_info().rss + g = ifcopenshell.open(fn, bypass_types=("IfcRepresentationItem",)) + m2 = psutil.Process().memory_info().rss + # arbitrary... + expected_ratio = 0.75 + assert (m2 - m1) < (m1 - m0) * expected_ratio + + def test_rocks(): with tempfile.TemporaryDirectory() as d: rfn = os.path.join(d, os.path.basename(fn)) diff --git a/src/ifcopenshell-python/test/test_write.py b/src/ifcopenshell-python/test/test_write.py index 352aea2538..e7a1f76b7c 100644 --- a/src/ifcopenshell-python/test/test_write.py +++ b/src/ifcopenshell-python/test/test_write.py @@ -40,9 +40,11 @@ class TestWrite: def test_write_ifcspf(self): self.assert_model_is_written("model.ifc") + @pytest.mark.skip(reason="IFCXML writing not supported") def test_write_ifcxml(self): self.assert_model_is_written("model.ifcXML") + @pytest.mark.skip(reason="IFCXML writing not supported") def test_write_ifc_zip_ifcxml_format(self): self.assert_model_is_written("model.ifcZIP", format=".ifcXML", zipped=True) @@ -58,9 +60,11 @@ class TestWrite: def test_write_anyextension_ifczip_ifcspf_format(self): self.assert_model_is_written("model.anyextension", format=".ifc", zipped=True) + @pytest.mark.skip(reason="IFCXML writing not supported") def test_write_anyextension_ifcxml_format(self): self.assert_model_is_written("model.anyextension", format=".ifcXML") + @pytest.mark.skip(reason="IFCXML writing not supported") def test_write_anyextension_ifczip_ifcxml_format(self): self.assert_model_is_written("model.anyextension", format=".ifcXML", zipped=True) diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index 1a1e28d58b..2c62c6d7c5 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -43,7 +43,7 @@ import ifcopenshell.util.element as subject from ifcopenshell.util.shape_builder import ShapeBuilder -class TestIFC2X3MaterialProfilePsts(test.bootstrap.IFC2X3): +class TestIFC2X3MaterialProfilePsets(test.bootstrap.IFC2X3): def test_get_profile_pset(self): profile = ifcopenshell.api.profile.add_parameterized_profile(self.file, "IfcRectangleProfileDef") pset = ifcopenshell.api.pset.add_pset(self.file, profile, "") @@ -159,6 +159,15 @@ class TestGetPsetIFC4(test.bootstrap.IFC4): assert subject.get_pset(self.file.create_entity("IfcPerson"), "name") is None assert subject.get_pset(self.file.create_entity("IfcPerson"), "name", "a") is None + def test_getting_predefined_psets(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcDoorType") + pset = self.file.create_entity("IfcDoorLiningProperties", ifcopenshell.guid.new()) + pset.Name = "My Lining" + pset.LiningDepth = 42 + element.HasPropertySets = [pset] + assert subject.get_pset(element, "My Lining") == {"LiningDepth": 42, "id": pset.id()} + assert subject.get_pset(element, "My Lining", psets_only=True) == {"LiningDepth": 42, "id": pset.id()} + class TestGetPsetsIFC4(test.bootstrap.IFC4): def test_getting_the_psets_of_a_product_as_a_dictionary(self): @@ -222,6 +231,15 @@ class TestGetPsetsIFC4(test.bootstrap.IFC4): ifcopenshell.api.pset.edit_qto(self.file, qto=qto, properties={"x": 42}) assert subject.get_psets(element, qtos_only=True) == {"qto": {"x": 42, "id": qto.id()}} + def test_getting_predefined_psets(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcDoorType") + pset = self.file.create_entity("IfcDoorLiningProperties", ifcopenshell.guid.new()) + pset.Name = "My Lining" + pset.LiningDepth = 42 + element.HasPropertySets = [pset] + assert subject.get_psets(element) == {"My Lining": {"LiningDepth": 42, "id": pset.id()}} + assert subject.get_psets(element, psets_only=True) == {"My Lining": {"LiningDepth": 42, "id": pset.id()}} + class TestGetPropertyDefinitionIFC4(test.bootstrap.IFC4): def test_getting_the_properties_of_a_pset(self): @@ -369,6 +387,65 @@ class TestGetPredefinedTypeIFC4(test.bootstrap.IFC4): assert subject.get_predefined_type(element_type) == "NOTDEFINED" +class TestIsUserdefinedTypeIFC4(test.bootstrap.IFC4): + def test_getting_a_predefined_element(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + element.PredefinedType = "PARTITIONING" + assert not subject.is_userdefined_type(element) + + def test_getting_an_element_userdefined_type(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + element.PredefinedType = "USERDEFINED" + element.ObjectType = "FOOBAR" + assert subject.is_userdefined_type(element) + + def test_getting_an_element_type_without_a_predefined_type_attribute(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcAnnotation") + element.ObjectType = "FOOBAR" + assert subject.is_userdefined_type(element) + + def test_getting_an_inherited_predefined_type(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") + ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type) + element_type.PredefinedType = "PARTITIONING" + assert not subject.is_userdefined_type(element) + + def test_getting_an_inherited_userdefined_type_for_an_element_type(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") + ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type) + element_type.PredefinedType = "USERDEFINED" + element_type.ElementType = "FOOBAR" + assert subject.is_userdefined_type(element) + + def test_getting_an_overriden_predefined_type(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") + ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type) + element_type.PredefinedType = "NOTDEFINED" + element.PredefinedType = "PARTITIONING" + assert not subject.is_userdefined_type(element) + + def test_getting_an_inherited_userdefined_type_for_a_process_type(self): + element = ifcopenshell.api.sequence.add_task(self.file) + element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcTaskType") + ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type) + element_type.PredefinedType = "USERDEFINED" + element_type.ProcessType = "FOOBAR" + assert subject.is_userdefined_type(element) + + def test_getting_an_element_type_predefined_type(self): + element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") + element_type.PredefinedType = "PARTITIONING" + assert not subject.is_userdefined_type(element_type) + + def test_getting_an_element_type_null_predefined_type(self): + element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") + element_type.PredefinedType = "NOTDEFINED" + assert not subject.is_userdefined_type(element_type) + + class TestGetTypeIFC4(test.bootstrap.IFC4): def test_getting_the_type_of_a_product(self): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") diff --git a/src/ifcparse/FileReader.cpp b/src/ifcparse/FileReader.cpp new file mode 100644 index 0000000000..e16d26e641 --- /dev/null +++ b/src/ifcparse/FileReader.cpp @@ -0,0 +1,326 @@ +#include "FileReader.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef USE_MMAP +#include +#include +#endif + +#include "utils.h" + +namespace { + +#if defined(_WIN32) + inline void file_seek_abs(FILE* f, std::uint64_t off) { + if (_fseeki64(f, static_cast(off), SEEK_SET) != 0) + throw std::runtime_error("fseek failed"); + } +#else + inline void file_seek_abs(FILE* f, std::uint64_t off) { + if (fseeko(f, static_cast(off), SEEK_SET) != 0) + throw std::runtime_error("fseeko failed"); + } +#endif + +} // namespace + +using namespace IfcParse; + +struct FullBufferImpl final : FileReader::Impl { + std::vector buf_; + explicit FullBufferImpl(const std::string& fn) { +#ifdef _MSC_VER + std::wstring fn_ws = IfcUtil::path::from_utf8(fn); + const wchar_t* fn_wide = fn_ws.c_str(); + auto stream = _wfopen(fn_wide, L"rb"); +#else + auto stream = fopen(fn.c_str(), "rb"); +#endif + fseek(stream, 0, SEEK_END); + buf_.resize((size_t)ftell(stream)); + rewind(stream); + buf_.resize((size_t) fread(buf_.data(), 1, buf_.capacity(), stream)); + fclose(stream); + } + size_t size() const override { return buf_.size(); } + char get(size_t pos) const override { + if (pos >= buf_.size()) throw std::out_of_range("get out of range"); + return buf_[pos]; + } +}; + +struct PagedFileImpl final : FileReader::Impl { + std::string fn_; + FILE* fp_ = nullptr; + size_t file_size_ = 0; + size_t page_size_ = 4096; + + // LRU cache + size_t capacity_ = 8; + mutable std::list lru_; + struct Entry { + FileReader::Page page; + std::list::iterator it; + }; + mutable std::unordered_map map_; + + PagedFileImpl(const std::string& fn, size_t page_size, size_t cap) + : fn_(fn), page_size_(std::max(512, page_size)), capacity_(std::max(2, cap)) + { +#ifdef _MSC_VER + std::wstring fn_ws = IfcUtil::path::from_utf8(fn); + const wchar_t* fn_wide = fn_ws.c_str(); + fp_ = _wfopen(fn_wide, L"rb"); +#else + fp_ = fopen(fn.c_str(), "rb"); +#endif + fseek(fp_, 0, SEEK_END); + file_size_ = (size_t)ftell(fp_); + rewind(fp_); + } + + ~PagedFileImpl() override { + if (fp_) std::fclose(fp_); + fp_ = nullptr; + } + + size_t size() const override { return file_size_; } + + char get(size_t pos) const override { + if (pos >= file_size_) throw std::out_of_range("get out of range"); + const size_t pidx = pos / page_size_; + const FileReader::Page& p = fetchPage_(pidx); + const size_t off = pos % page_size_; + if (off >= p.data.size()) throw std::out_of_range("offset beyond valid page bytes"); + return p.data[off]; + } + +private: + const FileReader::Page& fetchPage_(size_t idx) const { + auto it = map_.find(idx); + if (it != map_.end()) { + touch_(it); + return it->second.page; + } + // Load page from disk using persistent FILE* + FileReader::Page pg; + pg.data.resize(page_size_); + const size_t begin = idx * page_size_; + const size_t avail = std::min(page_size_, file_size_ - begin); + + file_seek_abs(fp_, begin); + if (avail > 0) { + const size_t nread = std::fread(pg.data.data(), 1, avail, fp_); + if (nread != avail) throw std::runtime_error("Short fread on page"); + } + // trim to actual size + pg.data.resize(avail); + + // Insert into LRU + if (map_.size() >= capacity_) evict_(); + lru_.push_front(idx); + auto lit = lru_.begin(); + auto [emplaced_it, ok] = map_.emplace(idx, Entry{ std::move(pg), lit }); + (void)ok; + return emplaced_it->second.page; + } + + void touch_(typename std::unordered_map::iterator it) const { + lru_.erase(it->second.it); + lru_.push_front(it->first); + it->second.it = lru_.begin(); + } + + void evict_() const { + if (lru_.empty()) return; + const size_t victim = lru_.back(); + lru_.pop_back(); + map_.erase(victim); + } +}; + +#ifdef USE_MMAP +struct MMapImpl final : FileReader::Impl { + boost::iostreams::mapped_file_source map_; + size_t size_ = 0; + + explicit MMapImpl(const std::string& fn) { + map_.open(boost::filesystem::path(IfcUtil::path::from_utf8(fn))); + if (!map_.is_open()) throw std::runtime_error("Failed to open mapped_file_source"); + size_ = static_cast(map_.size()); + } + + size_t size() const override { return size_; } + + char get(size_t pos) const override { + if (pos >= size_) throw std::out_of_range("get out of range"); + return map_.data()[pos]; + } +}; +#endif + +/// User-pushed sequential backend with an arbitrary-length queue of future pages. +/// We keep a deque of pages; when reads move forward, we drop fully-consumed +/// pages from the front to release memory. +struct PushedSequentialImpl final : std::enable_shared_from_this, FileReader::Impl { + // Deque of pages, front is earliest in file. + std::deque pages_; + // total bytes in dropped pages + size_t discarded_page_bytes_ = 0; + + size_t size() const override { + size_t n = discarded_page_bytes_; + for (auto& pg : pages_) n += pg.data.size(); + return n; + } + + // Drop fully-consumed pages so pos is guaranteed to be within the first page + void dropPages(size_t pos) override { + while (!pages_.empty()) { + if (pos - discarded_page_bytes_ >= pages_.front().data.size()) { + discarded_page_bytes_ += pages_.front().data.size(); + pages_.pop_front(); + } else { + break; + } + } + } + + char get(size_t pos) const override { + /* + auto self = const_cast(this); + // We do not do this automatically because all variable width tokens: + // ENUM/STRING/BINARY/KEYWORD are stored as file offsets until a full + // entity instance is finalized. + if (this->shared_from_this().use_count() == 2) { + // only drop pages when there is only one active client. + // NB this->shared_from_this() increases count by 1 + self->drop_consumed_up_to(pos); + } + */ + + const size_t avail_end = size(); + if (pos >= avail_end) throw std::out_of_range("pushed backend: position not committed yet"); + + pos -= discarded_page_bytes_; + + size_t page_start = 0; + for (const auto& pg : pages_) { + if (pos < page_start + pg.data.size()) { + const size_t off = pos - page_start; + return pg.data[off]; + } else { + page_start += pg.data.size(); + } + } + + throw std::out_of_range("pushed backend: internal inconsistency"); + } + + void pushNextPage(const std::string& data) override { + FileReader::Page p; p.data.assign(data.data(), data.data() + data.size()); + pages_.push_back(std::move(p)); + } +}; + +IfcParse::FileReader::FileReader(const std::string& fn) + : cursor_(0) +{ + impl_ = std::make_shared(fn); +} + +IfcParse::FileReader::FileReader(const std::string& fn, const mmap_tag&) + : cursor_(0) +{ +#ifdef USE_MMAP + impl_ = std::make_shared(fn); +#else + (void)fn; + throw std::runtime_error("IfcParse::FileReader: mmap_tag specified but library not compiled with USE_MMAP"); +#endif +} + +IfcParse::FileReader::FileReader(const caller_fed_tag&) + : cursor_(0) +{ + impl_ = std::make_shared(); +} + +IfcParse::FileReader::FileReader(const std::string& content, const caller_fed_tag&) +{ + impl_ = std::make_shared(); + impl_->pushNextPage(content); +} + +IfcParse::FileReader::FileReader(const std::string& fn, size_t page_size, size_t page_capacity) + : cursor_(0) +{ + impl_ = std::make_shared(fn, page_size, page_capacity); +} + +FileReader FileReader::clone() const { + FileReader c(*this); + c.cursor_ = this->cursor_; + return c; +} + +void FileReader::seek(size_t pos) { + if (pos > impl_->size()) throw std::out_of_range("seek out of range"); + cursor_ = pos; +} + +size_t FileReader::tell() const { return cursor_; } + +size_t FileReader::size() const { return impl_->size(); } + +char FileReader::peek() const { + if (cursor_ >= impl_->size()) throw std::out_of_range("peek at EOF"); + return impl_->get(cursor_); +} + +void FileReader::increment(size_t n) { + if (cursor_ + n > impl_->size()) throw std::out_of_range("increment past EOF"); + cursor_ += n; +} + +void IfcParse::FileReader::pushNextPage(const std::string& data) +{ + impl_->pushNextPage(data); +} + +void IfcParse::FileReader::dropPages() +{ + impl_->dropPages(0); +} + +void IfcParse::FileReader::dropPages(size_t up_to_pos) +{ + impl_->dropPages(up_to_pos); +} + +bool IfcParse::FileReader::eof() const +{ + return cursor_ >= impl_->size(); +} + +char IfcParse::FileReader::read() +{ + auto c = peek(); + increment(1); + return c; +} + +char IfcParse::FileReader::get(size_t offset) const +{ + return impl_->get(offset); +} diff --git a/src/ifcparse/FileReader.h b/src/ifcparse/FileReader.h new file mode 100644 index 0000000000..164c5826a6 --- /dev/null +++ b/src/ifcparse/FileReader.h @@ -0,0 +1,133 @@ +/******************************************************************************** + * * + * This file is part of IfcOpenShell. * + * * + * IfcOpenShell is free software: you can redistribute it and/or modify * + * it under the terms of the Lesser GNU General Public License as published by * + * the Free Software Foundation, either version 3.0 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 * + * Lesser GNU General Public License for more details. * + * * + * You should have received a copy of the Lesser GNU General Public License * + * along with this program. If not, see . * + * * + ********************************************************************************/ + +/********************************************************************************* + * * + * Reads a file and provides functions to access its * + * contents randomly and character by character * + * * + ********************************************************************************/ + +#ifndef IFCSPFSTREAM_H +#define IFCSPFSTREAM_H + +#include "ifc_parse_api.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace IfcParse { + +/// \brief Read-only file accessor that supports four backends: +/// - full-buffer in RAM +/// - paged (LRU-cached with capacity) +/// - memory-mapped via boost::iostreams +/// - user-pushed sequential pages (caller feeds pages intended for streaming reads in WASM) +class IFC_PARSE_API FileReader { +public: + struct Page { std::vector data; }; + + /// \brief Tag to choose memory-mapped backend via boost::iostreams::mapped_file_source. + struct mmap_tag {}; + /// \brief Tag to choose user-pushed sequential pages. + struct caller_fed_tag {}; + + /// \brief Construct a FileReader. + /// \param fn File path. + /// \param maybe_mmaped_or_chunked + /// - std::nullopt: full-buffer mode (entire file loaded into memory) + /// - size_t: paged mode with this page size in bytes (e.g., 4096) + /// - mmap_tag: memory-mapped mode using boost::iostreams::mapped_file_source + /// \param page_cache_capacity Number of pages to keep in the LRU when in paged mode. + FileReader(const std::string& fn); + + FileReader(const std::string& fn, const mmap_tag&); + + FileReader(const caller_fed_tag&); + FileReader(const std::string& content, const caller_fed_tag&); + + FileReader(const std::string& fn, size_t page_size, size_t page_capacity); + + /// \brief Copy-construct a new reader sharing the underlying storage but with its own cursor. + FileReader clone() const; + + /// \brief Seek to an absolute byte position. + /// \throws std::out_of_range if pos > size(). + void seek(size_t pos); + + /// \brief Return the current cursor position. + size_t tell() const; + + /// \brief Total file size in bytes. + size_t size() const; + + /// \brief Peek the byte at the current cursor. + /// \throws std::out_of_range at EOF. + char peek() const; + + /// \brief Advance the cursor by n bytes (default 1). + /// \throws std::out_of_range if advancing crosses EOF. + void increment(size_t n = 1); + + /// \brief Push the next sequential page (pushed backend only). + /// \param data Contents of the page. + /// \throws std::logic_error if the current backend is not in pushed mode + void pushNextPage(const std::string& data); + + /// \brief Drops pages up to cursor position or provided offset. Does nothing when current backend is not in pushed mode + /// \param up_to_pos Pages with an end offset before up_to_pos are dropped from memory + void dropPages(); + void dropPages(size_t up_to_pos); + + /// \brief Returns true if the cursor is at or beyond the end of available data. + /// For the pushed backend, EOF means all pushed bytes have been consumed. + bool eof() const; + + /// \brief Equivalent of peek() followed by increment(1) + char read(); + + /// \brief Equivalent of peek() followed by increment(1) + char get(size_t offset) const; + + struct Impl { + virtual ~Impl() = default; + virtual size_t size() const = 0; + virtual char get(size_t pos) const = 0; + /// \brief Backend may support pushing pages; default throws. + virtual void pushNextPage(const std::string&) { + throw std::logic_error("push_next_page: backend does not support pushed mode"); + } + virtual void dropPages(size_t) { + // empty on purpose + } + }; + +private: + std::shared_ptr impl_; + size_t cursor_ = 0; +}; + +} // namespace IfcParse + +#endif diff --git a/src/ifcparse/IfcCharacterDecoder.cpp b/src/ifcparse/IfcCharacterDecoder.cpp index 7e62910808..71685dcedc 100644 --- a/src/ifcparse/IfcCharacterDecoder.cpp +++ b/src/ifcparse/IfcCharacterDecoder.cpp @@ -29,7 +29,7 @@ #include "IfcCharacterDecoder.h" #include "IfcException.h" -#include "IfcSpfStream.h" +#include "FileReader.h" #include "IfcLogger.h" #include @@ -77,7 +77,7 @@ using namespace IfcParse; -IfcCharacterDecoder::IfcCharacterDecoder(IfcParse::IfcSpfStream* stream) { +IfcCharacterDecoder::IfcCharacterDecoder(IfcParse::FileReader* stream) { stream_ = stream; codepage_ = 0; } @@ -86,49 +86,9 @@ IfcCharacterDecoder::~IfcCharacterDecoder() { } namespace { -unsigned int reference_helper = 0; + std::string read_string(IfcParse::FileReader& stream_, IfcParse::IfcCharacterDecoder::ConversionMode mode, char substitution_character) { + std::u32string builder_; -class pure_impure_helper { - private: - bool pure_; - IfcParse::IfcSpfStream* stream_; - unsigned int& pointer_; - std::u32string builder_; - - char peek() { - if (pure_) { - return stream_->peek_at(pointer_); - } - return stream_->Peek(); - } - - unsigned int tell() { - if (pure_) { - return pointer_; - } - return stream_->Tell(); - } - - void increment() { - if (pure_) { - stream_->increment_at(pointer_); - } else { - stream_->Inc(); - } - } - - public: - pure_impure_helper(IfcParse::IfcSpfStream* stream) - : pure_(false), - stream_(stream), - pointer_(reference_helper) {} - - pure_impure_helper(IfcParse::IfcSpfStream* stream, unsigned int& pointer) - : pure_(true), - stream_(stream), - pointer_(pointer) {} - - std::string get(IfcParse::IfcCharacterDecoder::ConversionMode mode, char substitution_character) { unsigned int parse_state = 0; builder_.clear(); builder_.push_back('\''); @@ -137,7 +97,7 @@ class pure_impure_helper { unsigned int hex = 0; unsigned int hex_count = 0; - while ((current_char = peek()) != 0) { + while ((current_char = stream_.peek()) != 0) { if (EXPECTS_CHARACTER(parse_state)) { builder_.push_back(IfcUtil::convert_codepage(codepage, current_char + 0x80)); parse_state = 0; @@ -178,7 +138,7 @@ class pure_impure_helper { } else if (IS_HEXADECIMAL(current_char) && EXPECTS_HEX(parse_state)) { if (IS_LOWERCASE_HEX(current_char)) { Logger::Warning("Lowercase hexadecimal character '" + std::string(1, current_char) + - "' found at offset " + std::to_string(pointer_) + + "' found at offset " + std::to_string(stream_.tell()) + ". It is recommended to use uppercase for hexadecimal."); } hex <<= 4; @@ -202,12 +162,12 @@ class pure_impure_helper { if (parse_state == APOSTROPHE && current_char != '\'') { break; } - throw IfcInvalidTokenException(tell(), current_char); + throw IfcInvalidTokenException(stream_.tell(), current_char); } else { parse_state = hex = hex_count = 0; builder_.push_back(current_char); } - increment(); + stream_.increment(); } builder_.push_back('\''); @@ -248,22 +208,25 @@ class pure_impure_helper { } throw IfcParse::IfcException("Invalid conversion mode"); } -}; } // namespace IfcCharacterDecoder::operator std::string() { - return pure_impure_helper(stream_).get(mode, substitution_character); + return read_string(*stream_, mode, substitution_character); } -std::string IfcCharacterDecoder::get(unsigned int& ptr) { - return pure_impure_helper(stream_, ptr).get(mode, substitution_character); +std::string IfcCharacterDecoder::get(size_t& ptr) { + auto local_stream = *stream_; + local_stream.seek(ptr); + auto s = read_string(local_stream, mode, substitution_character); + ptr = local_stream.tell(); + return s; } void IfcCharacterDecoder::skip() { unsigned int parse_state = 0; char current_char; unsigned int hex_count = 0; - while ((current_char = stream_->Peek()) != 0) { + while ((current_char = stream_->peek()) != 0) { if (EXPECTS_CHARACTER(parse_state)) { parse_state = 0; } else if (current_char == '\'' && (parse_state == 0U)) { @@ -318,11 +281,11 @@ void IfcCharacterDecoder::skip() { if (parse_state == APOSTROPHE && current_char != '\'') { break; } - throw IfcInvalidTokenException(stream_->Tell(), current_char); + throw IfcInvalidTokenException(stream_->tell(), current_char); } else { parse_state = hex_count = 0; } - stream_->Inc(); + stream_->increment(); } } diff --git a/src/ifcparse/IfcCharacterDecoder.h b/src/ifcparse/IfcCharacterDecoder.h index 6a6d318aa1..e978a5b4c2 100644 --- a/src/ifcparse/IfcCharacterDecoder.h +++ b/src/ifcparse/IfcCharacterDecoder.h @@ -27,7 +27,7 @@ #ifndef IFCCHARACTERDECODER_H #define IFCCHARACTERDECODER_H -#include "IfcSpfStream.h" +#include "FileReader.h" #include @@ -41,7 +41,7 @@ namespace IfcParse { class IFC_PARSE_API IfcCharacterDecoder { private: - IfcParse::IfcSpfStream* stream_; + IfcParse::FileReader* stream_; int codepage_; public: @@ -52,7 +52,7 @@ class IFC_PARSE_API IfcCharacterDecoder { }; static ConversionMode mode; static char substitution_character; - IfcCharacterDecoder(IfcParse::IfcSpfStream* stream); + IfcCharacterDecoder(IfcParse::FileReader* stream); ~IfcCharacterDecoder(); // Only advances the underlying token stream read pointer // to the next token. @@ -62,7 +62,7 @@ class IFC_PARSE_API IfcCharacterDecoder { operator std::string(); // Gets a decoded string representation at the offset provided, // does not mutate the underlying token stream read pointer. - std::string get(unsigned int&); + std::string get(size_t&); }; } // namespace IfcParse diff --git a/src/ifcparse/IfcEntityInstanceData.cpp b/src/ifcparse/IfcEntityInstanceData.cpp index ca2f73b4ef..e87c429545 100644 --- a/src/ifcparse/IfcEntityInstanceData.cpp +++ b/src/ifcparse/IfcEntityInstanceData.cpp @@ -38,7 +38,7 @@ namespace { } #ifdef IFOPSH_WITH_ROCKSDB else { - T val; + T val = T{}; const bool is_header = entity_or_type->schema() == &Header_section_schema::get_schema(); if constexpr ( // the following types cannot be directly deserialized from rocksdb, but need to be constructed @@ -51,6 +51,12 @@ namespace { (is_header ? entity_or_type->name() : std::to_string(instance_name_)) + "|" + std::to_string(index_), &str); impl::deserialize(array_.db_ptr, str, val); + } else { + static_assert( + std::is_same_v || + std::is_same_v>, IfcUtil::IfcBaseClass>, + "RocksDB deserialization must be specialized for this EnumerationReference and IfcBaseClass*" + ); } return val; } @@ -194,13 +200,15 @@ AttributeValue::operator IfcUtil::IfcBaseClass* () const std::to_string(index_), &str); size_t v; memcpy(&v, str.data() + 2, sizeof(size_t)); - if (str[1] == 'i') { + if (str.size() > 1 && str[1] == 'i') { // entity reference, by #Name return array_.db_ptr->assert_existance(v, IfcParse::impl::rocks_db_file_storage::entityinstance_ref); - } else if (str[1] == 't') { + } else if (str.size() > 1 && str[1] == 't') { // type reference by Identity return array_.db_ptr->assert_existance(v, IfcParse::impl::rocks_db_file_storage::typedecl_ref); - } + } else { + throw std::runtime_error("Invalid data encountered"); + } } #endif } @@ -339,28 +347,28 @@ bool impl::serialize(std::string& val, const aggregate_of_aggregate_of_instance: return true; } -bool impl::serialize(std::string& val, const Blank& t) +bool impl::serialize(std::string& val, const Blank&) { val.resize(1); val[0] = TypeEncoder::encode_type(); return true; } -bool impl::serialize(std::string& val, const Derived& t) +bool impl::serialize(std::string& val, const Derived&) { val.resize(1); val[0] = TypeEncoder::encode_type(); return true; } -bool impl::serialize(std::string& val, const empty_aggregate_t& t) +bool impl::serialize(std::string& val, const empty_aggregate_t&) { val.resize(1); val[0] = TypeEncoder::encode_type(); return true; } -bool impl::serialize(std::string& val, const empty_aggregate_of_aggregate_t& t) +bool impl::serialize(std::string& val, const empty_aggregate_of_aggregate_t&) { val.resize(1); val[0] = TypeEncoder::encode_type(); @@ -397,6 +405,7 @@ bool impl::deserialize(IfcParse::impl::rocks_db_file_storage*, const std::string } else { return false; } + return true; } bool impl::deserialize(IfcParse::impl::rocks_db_file_storage*, const std::string& val, boost::dynamic_bitset<>& t) { diff --git a/src/ifcparse/IfcEntityInstanceData.h b/src/ifcparse/IfcEntityInstanceData.h index df2edddb94..04e7dcb0ad 100644 --- a/src/ifcparse/IfcEntityInstanceData.h +++ b/src/ifcparse/IfcEntityInstanceData.h @@ -155,7 +155,7 @@ struct TypeEncoder_t> { using TypeEncoder = TypeEncoder_t; struct IFC_PARSE_API MutableAttributeValue { - int name_; + uint32_t name_; uint8_t index_; }; @@ -165,7 +165,7 @@ namespace IfcParse { } } -#if IFOPSH_WITH_ROCKSDB +#ifdef IFOPSH_WITH_ROCKSDB namespace impl { @@ -266,7 +266,6 @@ namespace impl { if (val[0] != TypeEncoder::encode_type()) { return false; } - auto s = (val.size() - 1) / sizeof(T); memcpy(&t, val.data() + 1, sizeof(T)); return true; } @@ -404,7 +403,7 @@ public: template auto apply_visitor(void* storage, const IfcParse::declaration* decl, std::size_t identity, std::size_t index, Visitor&& visitor) const { // @todo do we need visitation on all data/storage/attribute levels? - AttributeValue((IfcParse::impl::rocks_db_file_storage*)storage, identity, decl, index).apply_visitor(std::forward(visitor)); + AttributeValue((IfcParse::impl::rocks_db_file_storage*)storage, identity, decl, (uint8_t) index).apply_visitor(std::forward(visitor)); } #endif }; diff --git a/src/ifcparse/IfcException.h b/src/ifcparse/IfcException.h index 6689ebb7d4..f30a5e9f3f 100644 --- a/src/ifcparse/IfcException.h +++ b/src/ifcparse/IfcException.h @@ -57,7 +57,7 @@ class IFC_PARSE_API IfcAttributeOutOfRangeException : public IfcException { class IFC_PARSE_API IfcInvalidTokenException : public IfcException { public: IfcInvalidTokenException( - int token_start, + size_t token_start, const std::string& token_string, const std::string& expected_type) : IfcException( @@ -65,7 +65,7 @@ class IFC_PARSE_API IfcInvalidTokenException : public IfcException { boost::lexical_cast(token_start) + " invalid " + expected_type) {} IfcInvalidTokenException( - int token_start, + size_t token_start, char character) : IfcException( std::string("Unexpected '") + std::string(1, character) + "' at offset " + diff --git a/src/ifcparse/IfcFile.cpp b/src/ifcparse/IfcFile.cpp index 25cd414c4c..d0b2dd030e 100644 --- a/src/ifcparse/IfcFile.cpp +++ b/src/ifcparse/IfcFile.cpp @@ -56,7 +56,7 @@ namespace { constexpr bool is_type_in_variant_v = is_type_in_variant::value; template - void dispatch_token(int instance_id, int attribute_id, IfcParse::Token t, IfcParse::declaration* decl, Fn fn) { + void dispatch_token(boost::optional instance_id, int attribute_id, IfcParse::Token t, IfcParse::declaration* decl, Fn fn) { if (t.type == IfcParse::Token_BINARY) { fn(IfcParse::TokenFunc::asBinary(t)); } else if (IfcParse::TokenFunc::isBool(t)) { @@ -89,7 +89,7 @@ namespace { } template - void construct_(int instance_id, int attribute_id, IfcParse::parse_context& p, const IfcParse::aggregation_type* aggr, Fn fn) { + void construct_(boost::optional instance_id, int attribute_id, IfcParse::parse_context& p, const IfcParse::aggregation_type* aggr, Fn fn) { if (p.tokens_.empty()) { // @todo instead of ugly if-else we could also default initialize the respective // variant types below. @@ -234,7 +234,7 @@ namespace { } } -IfcEntityInstanceData IfcParse::parse_context::construct(int name, unresolved_references& references_to_resolve, const IfcParse::declaration* decl, boost::optional expected_size, int resolve_reference_index, bool coerce_attribute_count) { +IfcEntityInstanceData IfcParse::parse_context::construct(boost::optional name, unresolved_references& references_to_resolve, const IfcParse::declaration* decl, boost::optional expected_size, int resolve_reference_index, bool coerce_attribute_count) { std::vector parameter_types; std::unique_ptr transient_named_type; @@ -259,7 +259,11 @@ IfcEntityInstanceData IfcParse::parse_context::construct(int name, unresolved_re expected_size && *expected_size != tokens_.size()) { size_t expected = expected_size ? *expected_size : parameter_types.size(); - Logger::Warning("Expected " + std::to_string(expected) + " attribute values, found " + std::to_string(tokens_.size()) + " for instance #" + std::to_string(name > 0 ? name : 0)); + if (decl != nullptr && decl->schema() == &Header_section_schema::get_schema()) { + Logger::Warning("Expected " + std::to_string(expected) + " attribute values, found " + std::to_string(tokens_.size()) + " for header entity " + decl->name()); + } else { + Logger::Warning("Expected " + std::to_string(expected) + " attribute values, found " + std::to_string(tokens_.size()) + (name ? std::string(" for instance #" + std::to_string(*name)) : std::string(""))); + } } if (tokens_.empty()) { @@ -289,12 +293,12 @@ IfcEntityInstanceData IfcParse::parse_context::construct(int name, unresolved_re if constexpr (std::is_same_v, IfcParse::Token>) { dispatch_token(name, index, v, param_type && param_type->as_named_type() ? param_type->as_named_type()->declared_type() : nullptr, [this, &storage, name, &references_to_resolve, index, resolve_reference_index](auto v) { if constexpr (std::is_same_v, IfcParse::reference_or_simple_type>) { - if (name > 0) { + if (name) { references_to_resolve.push_back(std::make_pair( // @todo previously this was storage but apparently the // pointer is not constant with the moving and temporary nature // maybe it ought to be and in that case a pointer is more direct - MutableAttributeValue{ name, resolve_reference_index == -1 ? index : (uint8_t) resolve_reference_index }, + MutableAttributeValue{ (uint32_t) *name, resolve_reference_index == -1 ? index : (uint8_t) resolve_reference_index }, v )); } @@ -311,12 +315,12 @@ IfcEntityInstanceData IfcParse::parse_context::construct(int name, unresolved_re } construct_<0>(name, index, *v, pt ? pt->as_aggregation_type() : nullptr, [this, &storage, name, &references_to_resolve, index, resolve_reference_index](const auto& v) { if constexpr (std::is_same_v, std::vector>) { - if (name > 0) { - references_to_resolve.push_back({ {name, resolve_reference_index == -1 ? index : (uint8_t)resolve_reference_index }, v }); + if (name) { + references_to_resolve.push_back({ { (uint32_t) *name, resolve_reference_index == -1 ? index : (uint8_t)resolve_reference_index }, v }); } } else if constexpr (std::is_same_v, std::vector>>) { - if (name > 0) { - references_to_resolve.push_back({ {name, resolve_reference_index == -1 ? index : (uint8_t)resolve_reference_index }, v }); + if (name) { + references_to_resolve.push_back({ { (uint32_t) *name, resolve_reference_index == -1 ? index : (uint8_t)resolve_reference_index }, v }); } } else { storage.set(index, v); @@ -651,7 +655,18 @@ IfcParse::filetype IfcParse::guess_file_type(const std::string& fn) { } } -std::optional> IfcParse::InstanceStreamer::read_instance() { +void IfcParse::InstanceStreamer::bypassTypes(const std::set& type_names) { + for (auto& name : type_names) { + try { + types_to_bypass_.push_back(schema_->declaration_by_name(name)); + } catch (const IfcException&) { + continue; + } + } + } + + +std::optional> IfcParse::InstanceStreamer::readInstance() { std::optional> return_value; if (header_ && yielded_header_instances_ < 3) { @@ -679,7 +694,7 @@ std::optionalstream->eof && !current_id) { + while (good_ && !lexer_->stream->eof() && !current_id) { if (token_stream_[0].type == IfcParse::Token_IDENTIFIER && token_stream_[1].type == IfcParse::Token_OPERATOR && token_stream_[1].value_char == '=' && @@ -698,6 +713,15 @@ std::optionalis(*ty)) { + bypassed_instances_.push_back(current_id); + // Why is this a conditional clause in the loop? + current_id = 0; + goto advance; + } + } + parse_context ps; lexer_->Next(); try { @@ -734,7 +758,7 @@ std::optionalstream->eof && next_token.type == Token_NONE) { + if (!lexer_->stream->eof() && next_token.type == Token_NONE) { good_ = file_open_status::INVALID_SYNTAX; break; } @@ -742,13 +766,17 @@ std::optionalstream)->dropPages(); + return return_value; } IfcUtil::IfcBaseClass* IfcParse::impl::rocks_db_file_storage::create(const IfcParse::declaration* decl) { if (decl->as_entity() || decl->as_type_declaration()) { auto* inst = file->schema()->instantiate(decl, rocks_db_attribute_storage{}); - inst->file_ = file; + // @todo maybe this needs to be set to file? In order to have a context (ie. rocksdb::db*) to write to? + inst->file_ = nullptr; return file->addEntity(inst); } else { throw std::runtime_error("Requires and entity or type declaration"); @@ -764,6 +792,7 @@ IfcUtil::IfcBaseClass* IfcParse::impl::in_memory_file_storage::create(const IfcP } else { throw std::runtime_error("Requires and entity or type declaration"); } - inst->file_ = file; + // file_ should be nullptr in order not to bypass addEntity() behaviour of registration in maps + inst->file_ = nullptr; return file->addEntity(inst); } diff --git a/src/ifcparse/IfcFile.h b/src/ifcparse/IfcFile.h index 030321d9ee..96685a88ee 100644 --- a/src/ifcparse/IfcFile.h +++ b/src/ifcparse/IfcFile.h @@ -88,23 +88,24 @@ IFC_PARSE_API filetype guess_file_type(const std::string& fn); class IFC_PARSE_API InstanceStreamer { private: - IfcSpfStream* stream_; + FileReader* stream_; IfcSpfLexer* lexer_; IfcSpfHeader* header_; boost::circular_buffer token_stream_; const IfcParse::schema_definition* schema_; - const IfcParse::declaration* ifcroot_type_; IfcParse::impl::in_memory_file_storage storage_; IfcParse::file_open_status good_ = IfcParse::file_open_status::SUCCESS; int progress_; IfcParse::unresolved_references references_to_resolve_; int yielded_header_instances_ = 0; + std::vector types_to_bypass_; + std::vector bypassed_instances_; -public: + public: bool coerce_attribute_count = true; operator bool() const { - return good_ && !lexer_->stream->eof; + return good_ && !lexer_->stream->eof(); } IfcParse::file_open_status status() const { @@ -119,6 +120,11 @@ public: return references_to_resolve_; } + const std::vector& bypassed_instances() { + std::sort(bypassed_instances_.begin(), bypassed_instances_.end()); + return bypassed_instances_; + } + const IfcParse::impl::in_memory_file_storage::entities_by_ref_t& inverses() const { return storage_.byref_excl_; } @@ -127,12 +133,26 @@ public: return storage_.byref_excl_; } - InstanceStreamer(const std::string& fn); + std::vector> stealInstances() { + return storage_.steal_instances(); + } + + bool hasSemicolon() const; + + size_t semicolonCount() const; + + void pushPage(const std::string& page); + + InstanceStreamer(); + + InstanceStreamer(const std::string& fn, bool mmap=false); InstanceStreamer(void* data, int length); InstanceStreamer(const IfcParse::schema_definition* schema, IfcParse::IfcSpfLexer* lexer); + void bypassTypes(const std::set& type_names); + ~InstanceStreamer() { delete stream_; if (stream_) { @@ -141,9 +161,12 @@ public: delete header_; } - std::optional> read_instance(); + std::optional> readInstance(); }; +class uninitialized_tag {}; + + /// This class provides access to the entity instances in an IFC file /// The file takes ownership of instances added to this file and deletes them when the file is deleted. class IFC_PARSE_API IfcFile { @@ -170,7 +193,10 @@ public: // @todo temporarily public for header storage_t storage_; -private: + + std::set types_to_bypass_loading_; + + private: file_open_status good_ = file_open_status::SUCCESS; const IfcParse::schema_definition* schema_; @@ -197,16 +223,60 @@ private: public: #ifdef USE_MMAP - IfcFile(const std::string& path, bool mmap = false); -#else - IfcFile(const std::string& path, filetype ty=FT_AUTODETECT, bool readonly=false); + /// + /// Constructs an IfcFile object from a file path, optionally using memory-mapped I/O, only supports IFC-SPF files. + /// + /// UTF-8 file path to an IFC-SPF file + /// Whether to use memory-mapped I/O + IfcFile(const std::string& path, bool mmap); #endif + /// + /// Constructs an IfcFile object from a file path, supports IFC-SPF and the IfcOpenShell-specific RocksDB format. + /// + /// UTF-8 file path to an IFC-SPF file or RocksDB database directory + /// File type of the path + /// Whether to open in read-only mode, only supported on RocksDB databases + IfcFile(const std::string& path, filetype ty=FT_AUTODETECT, bool readonly=false); + + /// + /// Constructs an IfcFile object from a stream containing IFC-SPF data. + /// IfcFile(std::istream& stream, int length); + + /// + /// Constructs an IfcFile object from a memory buffer containing IFC-SPF data. + /// IfcFile(void* data, int length); - IfcFile(IfcParse::IfcSpfStream* stream); - // @nb path is only used in rocksdb mode, for spf file is in-memory only until write() is called + + /// + /// Constructs an IfcFile object from a given IFC SPF stream. + /// + /// A pointer to an IfcParse::FileReader object representing the input IFC SPF data stream. + IfcFile(IfcParse::FileReader* stream); + + /// + /// Constructs an IfcFile object with the specified schema, file type, and file path. + /// @nb path is only used in rocksdb mode, for spf file is in-memory only until write() is called + /// + /// Pointer to the schema definition to use. Defaults to the IFC4 schema if not specified. + /// The file type to use for the file. Defaults to FT_AUTODETECT. + /// The file system path to the IFC file. Defaults to an empty string. IfcFile(const IfcParse::schema_definition* schema = IfcParse::schema_by_name("IFC4"), filetype ty = FT_AUTODETECT, const std::string& path = ""); + /// + /// Constructs an unitialized IfcFile object. Call initialize() later on. Allows to specify which types to bypass during load. + /// + IfcFile(const uninitialized_tag&); + + bool initialize(const std::string& path, filetype ty = FT_AUTODETECT, bool readonly = false); +#ifdef USE_MMAP + bool initialize(const std::string& path, bool mmap); +#endif + + /// @brief Bypass loading of all instances of the specified type name. Only applies to parsed IFC-SPF files. + /// @param type_name case insensitive name of the type to bypass + void bypass_type(const std::string& type_name); + ~IfcFile(); IfcParse::file_open_status good() const { return good_; } diff --git a/src/ifcparse/IfcParse.cpp b/src/ifcparse/IfcParse.cpp index 183047b6fd..3a7c373a7d 100644 --- a/src/ifcparse/IfcParse.cpp +++ b/src/ifcparse/IfcParse.cpp @@ -26,7 +26,7 @@ #include "IfcLogger.h" #include "IfcSchema.h" #include "IfcSIPrefix.h" -#include "IfcSpfStream.h" +#include "FileReader.h" #include "utils.h" #include @@ -102,167 +102,7 @@ void init_locale() { #endif -// -// Opens the file and gets the filesize -// -#ifdef USE_MMAP -IfcSpfStream::IfcSpfStream(const std::string& path, bool mmap) -#else -IfcSpfStream::IfcSpfStream(const std::string& path) -#endif - : stream_(0), - buffer_(0), - valid(false), - eof(false) { -#ifdef _MSC_VER - std::wstring fn_ws = IfcUtil::path::from_utf8(path); - const wchar_t* fn_wide = fn_ws.c_str(); - -#ifdef USE_MMAP - if (mmap) { - mfs = boost::iostreams::mapped_file_source(boost::filesystem::wpath(fn_wide)); - } else { -#endif - stream_ = _wfopen(fn_wide, L"rb"); -#ifdef USE_MMAP - } -#endif - -#else - -#ifdef USE_MMAP - if (mmap) { - mfs = boost::iostreams::mapped_file_source(path); - } else { -#endif - stream_ = fopen(path.c_str(), "rb"); -#ifdef USE_MMAP - } -#endif - -#endif - -#ifdef USE_MMAP - if (mmap) { - if (!mfs.is_open()) { - return; - } - - valid = true; - buffer_ = mfs.data(); - ptr_ = 0; - len_ = mfs.size(); - } else { -#endif - if (stream_ == NULL) { - return; - } - - valid = true; - fseek(stream_, 0, SEEK_END); - size = (unsigned int)ftell(stream_); - rewind(stream_); - char* buffer_rw = new char[size]; - len_ = (unsigned int)fread(buffer_rw, 1, size, stream_); - buffer_ = buffer_rw; - eof = len_ == 0; - ptr_ = 0; - fclose(stream_); - stream_ = nullptr; -#ifdef USE_MMAP - } -#endif -} - -IfcSpfStream::IfcSpfStream(std::istream& stream, int length) - : stream_(0), - buffer_(0) { - eof = false; - size = length; - char* buffer_rw = new char[size]; - stream.read(buffer_rw, size); - buffer_ = buffer_rw; - valid = stream.gcount() == size; - ptr_ = 0; - len_ = length; -} - -IfcSpfStream::IfcSpfStream(void* data, int length) - : stream_(0), - buffer_(0) { - eof = false; - size = length; - buffer_ = (char*)data; - valid = true; - ptr_ = 0; - len_ = length; -} - -IfcSpfStream::~IfcSpfStream() { - Close(); -} - -void IfcSpfStream::Close() { -#ifdef USE_MMAP - if (mfs.is_open()) { - mfs.close(); - return; - } -#endif - delete[] buffer_; - if (stream_ != nullptr) { - fclose(stream_); - } -} - -// -// Seeks an arbitrary position in the file -// -void IfcSpfStream::Seek(unsigned int offset) { - ptr_ = offset; - if (ptr_ >= len_) { - throw IfcException("Reading outside of file limits"); - } - eof = false; -} - -// -// Returns the character at the cursor -// -char IfcSpfStream::Peek() { - return buffer_[ptr_]; -} - -// -// Returns the character at specified offset -// -char IfcSpfStream::Read(unsigned int offset) { - return buffer_[offset]; -} - -// -// Returns the cursor position -// -unsigned int IfcSpfStream::Tell() const { - return ptr_; -} - -// -// Increments cursor and reads new chunk if necessary -// -void IfcSpfStream::Inc() { - if (++ptr_ == len_) { - eof = true; - return; - } - const char current = IfcSpfStream::Peek(); - if (current == '\n' || current == '\r') { - // NB this is recursive. It might as well be a loop. - IfcSpfStream::Inc(); - } -} - -IfcSpfLexer::IfcSpfLexer(IfcParse::IfcSpfStream* stream_) { +IfcSpfLexer::IfcSpfLexer(IfcParse::FileReader* stream_) { stream = stream_; decoder_ = new IfcCharacterDecoder(stream_); } @@ -271,12 +111,12 @@ IfcSpfLexer::~IfcSpfLexer() { delete decoder_; } -unsigned int IfcSpfLexer::skipWhitespace() const { - unsigned int index = 0; - while (!stream->eof) { - char character = stream->Peek(); +size_t IfcSpfLexer::skipWhitespace() const { + size_t index = 0; + while (!stream->eof()) { + char character = stream->peek(); if ((character == ' ' || character == '\r' || character == '\n' || character == '\t')) { - stream->Inc(); + stream->increment(); ++index; } else { break; @@ -285,22 +125,25 @@ unsigned int IfcSpfLexer::skipWhitespace() const { return index; } -unsigned int IfcSpfLexer::skipComment() const { - char character = stream->Peek(); +size_t IfcSpfLexer::skipComment() const { + if (stream->eof()) { + return 0; + } + char character = stream->peek(); if (character != '/') { return 0; } - stream->Inc(); - character = stream->Peek(); + stream->increment(); + character = stream->peek(); if (character != '*') { - stream->Seek(stream->Tell() - 1); + stream->seek(stream->tell() - 1); return 0; } - unsigned int index = 2; + size_t index = 2; char intermediate = 0; - while (!stream->eof) { - character = stream->Peek(); - stream->Inc(); + while (!stream->eof()) { + character = stream->peek(); + stream->increment(); ++index; if (character == '/' && intermediate == '*') { break; @@ -315,19 +158,20 @@ unsigned int IfcSpfLexer::skipComment() const { // Token IfcSpfLexer::Next() { - if (stream->eof) { + if (stream->eof()) { return Token{}; } while ((skipWhitespace() != 0U) || (skipComment() != 0U)) { } - if (stream->eof) { + if (stream->eof()) { return Token{}; } - unsigned int pos = stream->Tell(); - char character = stream->Peek(); + auto& str = GetTempString(); + auto pos = stream->tell(); + char character = stream->read(); // If the cursor is at [()=,;$*] we know token consists of single char if (character == '(' || @@ -336,69 +180,47 @@ Token IfcSpfLexer::Next() { character == ',' || character == ';' || character == '$' || - character == '*') { - stream->Inc(); - return OperatorTokenPtr(this, pos, pos + 1); + character == '*') + { + return OperatorTokenPtr(this, pos, character); } - int len = 0; - - while (!stream->eof) { - - // Read character and increment pointer if not starting a new token - character = stream->Peek(); - if ((len != 0) && (character == '(' || - character == ')' || - character == '=' || - character == ',' || - character == ';' || - character == '/')) { - break; - } - stream->Inc(); - len++; - + if (character == '\'') { // If a string is encountered defer processing to the IfcCharacterDecoder - if (character == '\'') { - decoder_->skip(); + str = *decoder_; + } else { + str.assign(&character, 1); + + while (!stream->eof()) { + // Read character and increment pointer if not starting a new token + character = stream->peek(); + if (character == '(' || + character == ')' || + character == '=' || + character == ',' || + character == ';' || + character == '/') { + break; + } + if (!(character == ' ' || character == '\r' || character == '\n' || character == '\t')) { + str.push_back(character); + } + stream->increment(); } } - Token t; - if (len != 0) { - t = GeneralTokenPtr(this, pos, stream->Tell()); - } else { - t = Token{}; - } - // std::wcout << "token: " << pos << " " << TokenFunc::asStringRef(t).c_str() << std::endl; - return t; -} - -bool IfcSpfStream::is_eof_at(unsigned int local_ptr) const { - return local_ptr >= len_; -} - -void IfcSpfStream::increment_at(unsigned int& local_ptr) { - if (++local_ptr == len_) { - return; - } - const char current = IfcSpfStream::peek_at(local_ptr); - if (current == '\n' || current == '\r') { - IfcSpfStream::increment_at(local_ptr); - } -} - -char IfcSpfStream::peek_at(unsigned int local_ptr) { - return buffer_[local_ptr]; + return GeneralTokenPtr(this, pos, str); } // // Reads a std::string from the file at specified offset // Omits whitespace and comments // -void IfcSpfLexer::TokenString(unsigned int offset, std::string& buffer) { +void IfcSpfLexer::TokenString(size_t offset, std::string& buffer) { buffer.clear(); - while (!stream->is_eof_at(offset)) { - char character = stream->peek_at(offset); + auto local_stream = *this->stream; + local_stream.seek(offset); + while (!local_stream.eof()) { + char character = local_stream.peek(); if (!buffer.empty() && (character == '(' || character == ')' || character == '=' || @@ -407,7 +229,7 @@ void IfcSpfLexer::TokenString(unsigned int offset, std::string& buffer) { character == '/')) { break; } - stream->increment_at(offset); + local_stream.increment(); if (character == ' ' || character == '\r' || character == '\n' || @@ -416,6 +238,7 @@ void IfcSpfLexer::TokenString(unsigned int offset, std::string& buffer) { } if (character == '\'') { // todo, make decoder use local offset ptr + auto offset = local_stream.tell(); buffer = decoder_->get(offset); break; } @@ -424,10 +247,11 @@ void IfcSpfLexer::TokenString(unsigned int offset, std::string& buffer) { } //Note: according to STEP standard, there may be newlines in tokens -inline void RemoveTokenSeparators(IfcSpfStream* stream, unsigned start, unsigned end, std::string& oDestination) { +/* +inline void RemoveTokenSeparators(FileReader* stream, size_t start, size_t end, std::string& oDestination) { oDestination.clear(); for (unsigned i = start; i < end; i++) { - char character = stream->Read(i); + char character = stream->get(i); if (character == ' ' || character == '\r' || character == '\n' || @@ -437,6 +261,7 @@ inline void RemoveTokenSeparators(IfcSpfStream* stream, unsigned start, unsigned oDestination += character; } } +*/ bool ParseInt(const char* pStart, int& val) { char* pEnd; @@ -481,22 +306,17 @@ bool ParseBool(const char* pStart, int& val) { return true; } -Token IfcParse::OperatorTokenPtr(IfcSpfLexer* lexer, unsigned start, unsigned end) { - char first = lexer->stream->Read(start); - Token token(lexer, start, end, Token_OPERATOR); - token.value_char = first; +Token IfcParse::OperatorTokenPtr(IfcSpfLexer* lexer, size_t start, char data) { + Token token(lexer, start, Token_OPERATOR); + token.value_char = data; return token; } -Token IfcParse::GeneralTokenPtr(IfcSpfLexer* lexer, unsigned start, unsigned end) { - Token token(lexer, start, end, Token_NONE); - - //extract token into temp buffer (remove eol-s, no encoding changes) - std::string& tokenStr = lexer->GetTempString(); - RemoveTokenSeparators(lexer->stream, start, end, tokenStr); +Token IfcParse::GeneralTokenPtr(IfcSpfLexer* lexer, size_t start, const std::string& tokenStr) { + Token token(lexer, start, Token_NONE); //determine type of the token - char first = lexer->stream->Read(start); + const char& first = tokenStr.front(); if (first == '#') { token.type = Token_IDENTIFIER; if (!ParseInt(tokenStr.c_str() + 1, token.value_int)) { @@ -676,7 +496,25 @@ boost::dynamic_bitset<> TokenFunc::asBinary(const Token& token) { std::string TokenFunc::toString(const Token& token) { std::string result; - token.lexer->TokenString(token.startPos, result); + if (token.type == Token_OPERATOR) { + result.push_back(token.value_char); + } else if (token.type == Token_INT) { + result = std::to_string(token.value_int); + } else if (token.type == Token_BOOL) { + if (token.value_int == 1) { + result = ".T."; + } else if (token.value_int == 0) { + result = ".F."; + } else { + result = ".U."; + } + } else if (token.type == Token_FLOAT) { + std::ostringstream oss; + oss << std::setprecision(15) << token.value_double; + result = oss.str(); + } else { + token.lexer->TokenString(token.startPos, result); + } return result; } @@ -684,7 +522,7 @@ std::string TokenFunc::toString(const Token& token) { // Reads the arguments from a list of token // Aditionally, registers the ids (i.e. #[\d]+) in the inverse map // -void IfcParse::impl::in_memory_file_storage::load(unsigned entity_instance_name, const IfcParse::entity* entity, parse_context& context, int attribute_index) { +void IfcParse::impl::in_memory_file_storage::load(boost::optional entity_instance_name, const IfcParse::entity* entity, parse_context& context, int attribute_index) { Token next = tokens->Next(); /* @@ -708,8 +546,8 @@ void IfcParse::impl::in_memory_file_storage::load(unsigned entity_instance_name, load(entity_instance_name, entity, context.push(), attribute_index == -1 ? (int) attribute_index_within_data : attribute_index); } else { return_value++; - if (TokenFunc::isIdentifier(next) && entity) { - register_inverse(entity_instance_name, entity, next.value_int, attribute_index == -1 ? attribute_index_within_data : attribute_index); + if (TokenFunc::isIdentifier(next) && entity && entity_instance_name) { + register_inverse(*entity_instance_name, entity, next.value_int, attribute_index == -1 ? (int) attribute_index_within_data : attribute_index); } if (TokenFunc::isKeyword(next)) { @@ -725,7 +563,7 @@ void IfcParse::impl::in_memory_file_storage::load(unsigned entity_instance_name, // the 2nd pass. load(entity_instance_name, entity, ps, attribute_index == -1 ? (int)attribute_index_within_data : attribute_index); auto* simple_type_instance = (schema ? schema : file->schema())->instantiate(decl, ps.construct(entity_instance_name, *references_to_resolve, decl, boost::none, attribute_index == -1 ? (int)attribute_index_within_data : attribute_index)); - read_simple_type_instances.push_back(simple_type_instance); + read_simple_type_instances.emplace_back(simple_type_instance); //@todo decide addEntity(((IfcUtil::IfcBaseClass*)*entity)); context.push(simple_type_instance); simple_type_instance->file_ = file; @@ -758,10 +596,10 @@ IfcEntityInstanceData IfcParse::impl::in_memory_file_storage::read(unsigned int } void IfcParse::impl::in_memory_file_storage::try_read_semicolon() const { - unsigned int old_offset = tokens->stream->Tell(); + auto old_offset = tokens->stream->tell(); Token semilocon = tokens->Next(); if (!TokenFunc::isOperator(semilocon, ';')) { - tokens->stream->Seek(old_offset); + tokens->stream->seek(old_offset); } } @@ -783,7 +621,7 @@ void IfcParse::impl::in_memory_file_storage::unregister_inverse(unsigned id_from namespace { template - std::string to_string_fixed_width(const T& t, size_t w) { + std::string to_string_fixed_width(const T& t, size_t) { // @todo currently inactive std::ostringstream oss; oss << /*std::setfill('0') << std::setw(w) <<*/ t; @@ -1345,30 +1183,49 @@ IfcUtil::IfcBaseClass::set_attribute_value(const std::string& s, const T& t) { // #ifdef USE_MMAP IfcFile::IfcFile(const std::string& fn, bool mmap) { - IfcSpfStream s(fn, mmap); - storage_ = impl::in_memory_file_storage{}; - std::get(storage_).read_from_stream(&s); + initialize(fn, mmap); } -#else -IfcFile::IfcFile(const std::string& path, filetype ty, bool readonly) - : schema_(nullptr) - , max_id_(0) - , _header(this) -{ - // @todo allow for rocksdb from path + +bool IfcParse::IfcFile::initialize(const std::string& fn, bool mmap) { + std::unique_ptr s; + if (mmap) { + s = std::make_unique(fn, FileReader::mmap_tag{}); + } else { + s = std::make_unique(fn); + } + + storage_.emplace<1>(this); + std::get(storage_).read_from_stream(&*s, schema_, max_id_, types_to_bypass_loading_); + + if ((good_ = std::get(storage_).good_)) { + // @todo unify these names, it's already confusing enough as it stands + byid_ = decltype(byid_)(&std::get(storage_).byid_); + byref_excl_ = decltype(byref_excl_)(&std::get(storage_).byref_excl_); + byguid_ = decltype(byguid_)(&std::get(storage_).byguid_); + } + + ifcroot_type_ = schema_ ? schema_->declaration_by_name("IfcRoot") : nullptr; + return good_ == file_open_status::SUCCESS; +} +#endif + +IfcFile::IfcFile(const uninitialized_tag&) + : schema_(nullptr), max_id_(0), _header(this), good_(file_open_status::UNKNOWN), ifcroot_type_(nullptr) {} + +bool IfcParse::IfcFile::initialize(const std::string& path, filetype ty, bool readonly) { if (ty == FT_AUTODETECT) { ty = guess_file_type(path); } if (ty == FT_IFCSPF) { - IfcSpfStream s(path); + FileReader s(path); storage_.emplace<1>(this); - std::get(storage_).read_from_stream(&s, schema_, max_id_); + std::get(storage_).read_from_stream(&s, schema_, max_id_, types_to_bypass_loading_); - if (good_ = std::get(storage_).good_) { + if ((good_ = std::get(storage_).good_)) { // @todo unify these names, it's already confusing enough as it stands byid_ = decltype(byid_)(&std::get(storage_).byid_); byref_excl_ = decltype(byref_excl_)(&std::get(storage_).byref_excl_); - byguid_ = decltype(byguid_)(&std::get(storage_).byguid_); + byguid_ = decltype(byguid_)(&std::get(storage_).byguid_); } // byidentity_ = decltype(byidentity_)(&std::get(storage_).byidentity_); } else if (ty == FT_ROCKSDB) { @@ -1397,16 +1254,34 @@ IfcFile::IfcFile(const std::string& path, filetype ty, bool readonly) // throw std::runtime_error("Unsupported file format"); } ifcroot_type_ = schema_ ? schema_->declaration_by_name("IfcRoot") : nullptr; + return good_ == file_open_status::SUCCESS; +} + +void IfcParse::IfcFile::bypass_type(const std::string& type_name) { + types_to_bypass_loading_.insert(type_name); +} + +IfcFile::IfcFile(const std::string& path, filetype ty, bool readonly) + : schema_(nullptr) + , max_id_(0) + , _header(this) +{ + initialize(path, ty, readonly); } -#endif IfcFile::IfcFile(std::istream& stream, int length) : schema_(nullptr) , max_id_(0) { - IfcSpfStream s(stream, length); + FileReader s(FileReader::caller_fed_tag{}); + + std::string string_data; + string_data.resize(length); + stream.read(string_data.data(), length); + s.pushNextPage(string_data); + storage_.emplace<1>(this); - std::get(storage_).read_from_stream(&s, schema_, max_id_); + std::get(storage_).read_from_stream(&s, schema_, max_id_, types_to_bypass_loading_); good_ = std::get(storage_).good_; ifcroot_type_ = schema_ ? schema_->declaration_by_name("IfcRoot") : nullptr; @@ -1419,9 +1294,10 @@ IfcFile::IfcFile(void* data, int length) : schema_(nullptr) , max_id_(0) { - IfcSpfStream s(data, length); + FileReader s(std::string((char*)data, length), FileReader::caller_fed_tag{}); + storage_.emplace<1>(this); - std::get(storage_).read_from_stream(&s, schema_, max_id_); + std::get(storage_).read_from_stream(&s, schema_, max_id_, types_to_bypass_loading_); good_ = std::get(storage_).good_; ifcroot_type_ = schema_ ? schema_->declaration_by_name("IfcRoot") : nullptr; @@ -1430,12 +1306,12 @@ IfcFile::IfcFile(void* data, int length) byguid_ = decltype(byguid_)(&std::get(storage_).byguid_); } -IfcFile::IfcFile(IfcParse::IfcSpfStream* s) +IfcFile::IfcFile(IfcParse::FileReader* s) : schema_(nullptr) , max_id_(0) { storage_.emplace<1>(this); - std::get(storage_).read_from_stream(s, schema_, max_id_); + std::get(storage_).read_from_stream(s, schema_, max_id_, types_to_bypass_loading_); good_ = std::get(storage_).good_; ifcroot_type_ = schema_ ? schema_->declaration_by_name("IfcRoot") : nullptr; @@ -1475,18 +1351,94 @@ IfcFile::IfcFile(const IfcParse::schema_definition* schema, filetype ty, const s setDefaultHeaderValues(); } -IfcParse::InstanceStreamer::InstanceStreamer(const std::string& fn) - : stream_(new IfcSpfStream(fn)) +bool IfcParse::InstanceStreamer::hasSemicolon() const { + auto local_stream = stream_->clone(); + auto local_lexer = IfcSpfLexer(&local_stream); + Token t; + try { + t = local_lexer.Next(); + } catch (const std::out_of_range&) { + return false; + } + while (t.type != Token_NONE) { + if (TokenFunc::isOperator(t, ';')) { + return true; + } + try { + t = local_lexer.Next(); + } catch (const std::out_of_range&) { + // This most likely happens when a page boundary is contained within a string + break; + } + } + return false; +} + +size_t IfcParse::InstanceStreamer::semicolonCount() const { + auto local_stream = stream_->clone(); + auto local_lexer = IfcSpfLexer(&local_stream); + Token t; + size_t count = 0; + try { + t = local_lexer.Next(); + } catch (const std::out_of_range&) { + return false; + } + while (t.type != Token_NONE) { + if (TokenFunc::isOperator(t, ';')) { + count++; + } + try { + t = local_lexer.Next(); + } catch (const std::out_of_range&) { + // This most likely happens when a page boundary is contained within a string + break; + } + } + return count; +} + +void IfcParse::InstanceStreamer::pushPage(const std::string& page) +{ + stream_->pushNextPage(page); + if (good_ == file_open_status::NO_HEADER) { + header_ = new IfcParse::IfcSpfHeader(lexer_); + if (header_->tryRead() && header_->file_schema()->schema_identifiers().size() == 1) { + try { + schema_ = IfcParse::schema_by_name(header_->file_schema()->schema_identifiers().front()); + good_ = file_open_status::SUCCESS; + } catch (const IfcParse::IfcException&) { + } + } + storage_.file = nullptr; + storage_.schema = schema_; + storage_.tokens = lexer_; + storage_.references_to_resolve = &references_to_resolve_; + } +} + +IfcParse::InstanceStreamer::InstanceStreamer() + : stream_(new FileReader(FileReader::caller_fed_tag{})) + , lexer_(new IfcSpfLexer(stream_)) + , token_stream_(3, Token{}) + , schema_(nullptr) + , progress_(0) +{ + init_locale(); + good_ = file_open_status::NO_HEADER; +} + +IfcParse::InstanceStreamer::InstanceStreamer(const std::string& fn, bool mmap) + : stream_(mmap ? new FileReader(fn, FileReader::mmap_tag{}) : new FileReader(fn)) , lexer_(new IfcSpfLexer(stream_)) , token_stream_(3, Token{}) , schema_(nullptr) - , ifcroot_type_(nullptr) , progress_(0) { init_locale(); good_ = file_open_status::NO_HEADER; - if (*stream_) { + if (stream_->size() && !stream_->eof()) { header_ = new IfcParse::IfcSpfHeader(lexer_); if (header_->tryRead() && header_->file_schema()->schema_identifiers().size() == 1) { try { @@ -1503,17 +1455,16 @@ IfcParse::InstanceStreamer::InstanceStreamer(const std::string& fn) } IfcParse::InstanceStreamer::InstanceStreamer(void* data, int length) - : stream_(new IfcSpfStream(data, length)) + : stream_(new FileReader(std::string((char*) data, length), FileReader::caller_fed_tag{})) , lexer_(new IfcSpfLexer(stream_)) , token_stream_(3, Token{}) , schema_(nullptr) - , ifcroot_type_(nullptr) , progress_(0) { init_locale(); good_ = file_open_status::NO_HEADER; - if (*stream_) { + if (stream_->size() && !stream_->eof()) { header_ = new IfcParse::IfcSpfHeader(lexer_); if (header_->tryRead() && header_->file_schema()->schema_identifiers().size() == 1) { try { @@ -1535,7 +1486,6 @@ IfcParse::InstanceStreamer::InstanceStreamer(const IfcParse::schema_definition* , header_(nullptr) , token_stream_(3, Token{}) , schema_(schema) - , ifcroot_type_(schema->declaration_by_name("IfcRoot")) , progress_(0) { init_locale(); @@ -1546,14 +1496,14 @@ IfcParse::InstanceStreamer::InstanceStreamer(const IfcParse::schema_definition* storage_.references_to_resolve = &references_to_resolve_; } -void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::IfcSpfStream* s, const IfcParse::schema_definition*& schema, unsigned int& max_id) { +void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::FileReader* s, const IfcParse::schema_definition*& schema, unsigned int& max_id, const std::set& typed_to_bypass) { // Initialize a "C" locale for locale-independent // number parsing. See comment above on line 41. init_locale(); tokens = nullptr; - if (!s->valid) { + if (!s->size() || s->eof()) { // @todo set good on parent file good_ = file_open_status::READ_ERROR; return; @@ -1593,22 +1543,24 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::IfcSpfSt auto ifcroot_type_ = schema->declaration_by_name("IfcRoot"); InstanceStreamer streamer(schema, tokens); + streamer.bypassTypes(typed_to_bypass); Logger::Status("Scanning file..."); while (streamer) { - auto inst = streamer.read_instance(); + auto inst = streamer.readInstance(); if (!inst) { // No more instances to read break; } + auto current_id = std::get<0>(*inst); auto instance = schema->instantiate(std::get<1>(*inst), std::move(std::get<2>(*inst))); instance->file_ = file; - instance->id_ = current_id; + instance->id_ = (uint32_t) current_id; if (instance->declaration().is(*ifcroot_type_)) { try { @@ -1641,7 +1593,7 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::IfcSpfSt } // byidentity_[instance->identity()] = instance; - byid_.insert({ current_id, instance }); + byid_.insert({(uint32_t) current_id, instance }); // @nb cannot assign to byid_; // byid_[current_id] = instance; @@ -1651,6 +1603,9 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::IfcSpfSt good_ = streamer.status(); byref_excl_ = streamer.inverses(); + + // Move the storage of simple type instances so that they are retained during the lifetime of the file + read_simple_type_instances = streamer.stealInstances(); Logger::Status("\rDone scanning file "); @@ -1660,11 +1615,16 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::IfcSpfSt return; } + const auto& bypassed = streamer.bypassed_instances(); + for (const auto& p : streamer.references()) { const auto& ref = p.first.name_; const auto& refattr = p.first.index_; if (auto* v = std::get_if(&p.second)) { if (auto* name = std::get_if(v)) { + if (std::binary_search(bypassed.begin(), bypassed.end(), *name)) { + continue; + } auto it = byid_.find(*name); if (it == byid_.end()) { Logger::Error("Instance reference #" + std::to_string(*name) + " used by instance #" + std::to_string(ref) + " at attribute index " + std::to_string(refattr) + " not found at offset " + std::to_string(name->file_offset)); @@ -1690,11 +1650,14 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::IfcSpfSt } else if (auto* inst = std::get_if(v)) { byid_[p.first.name_]->data().set_attribute_value(nullptr, nullptr, 0, p.first.index_, *inst); } - } else if (auto* v = std::get_if>(&p.second)) { + } else if (auto* vv = std::get_if>(&p.second)) { aggregate_of_instance::ptr instances(new aggregate_of_instance); - instances->reserve(v->size()); - for (const auto& vi : *v) { + instances->reserve(vv->size()); + for (const auto& vi : *vv) { if (auto* name = std::get_if(&vi)) { + if (std::binary_search(bypassed.begin(), bypassed.end(), *name)) { + continue; + } auto it = byid_.find(*name); if (it == byid_.end()) { Logger::Error("Instance reference #" + std::to_string(*name) + " used by instance #" + std::to_string(ref) + " at attribute index " + std::to_string(refattr) + " not found at offset " + std::to_string(name->file_offset)); @@ -1723,12 +1686,15 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::IfcSpfSt } else { Logger::Error("Duplicate definition for instance reference"); } - } else if (auto* v = std::get_if>>(&p.second)) { + } else if (auto* vvv = std::get_if>>(&p.second)) { aggregate_of_aggregate_of_instance::ptr instances(new aggregate_of_aggregate_of_instance); - for (const auto& vi : *v) { + for (const auto& vi : *vvv) { std::vector inner; for (const auto& vii : vi) { if (auto* name = std::get_if(&vii)) { + if (std::binary_search(bypassed.begin(), bypassed.end(), *name)) { + continue; + } auto it = byid_.find(*name); if (it == byid_.end()) { Logger::Error("Instance reference #" + std::to_string(*name) + " used by instance #" + std::to_string(ref) + " at attribute index " + std::to_string(refattr) + " not found at offset " + std::to_string(name->file_offset)); @@ -2000,15 +1966,15 @@ IfcUtil::IfcBaseClass* IfcFile::addEntity(IfcUtil::IfcBaseClass* entity, int id) auto attr = entity->get_attribute_value(i); IfcUtil::ArgumentType attr_type = attr.type(); - IfcParse::declaration* decl = 0; + IfcParse::declaration* potentially_length_measure_decl = 0; if (entity->declaration().as_entity() != nullptr) { - decl = 0; + potentially_length_measure_decl = 0; const parameter_type* pt = entity->declaration().as_entity()->attribute_by_index(i)->type_of_attribute(); while (pt->as_aggregation_type() != nullptr) { pt = pt->as_aggregation_type()->type_of_element(); } if (pt->as_named_type() != nullptr) { - decl = pt->as_named_type()->declared_type(); + potentially_length_measure_decl = pt->as_named_type()->declared_type(); } } @@ -2047,7 +2013,7 @@ IfcUtil::IfcBaseClass* IfcFile::addEntity(IfcUtil::IfcBaseClass* entity, int id) } new_entity->set_attribute_value(i, new_instances); - } else if ((decl != nullptr) && decl->is(*schema()->declaration_by_name("IfcLengthMeasure"))) { + } else if ((potentially_length_measure_decl != nullptr) && potentially_length_measure_decl->is(*schema()->declaration_by_name("IfcLengthMeasure"))) { if (boost::math::isnan(conversion_factor)) { std::pair this_file_unit = {nullptr, 1.0}; std::pair other_file_unit = {nullptr, 1.0}; @@ -2280,8 +2246,6 @@ void IfcFile::process_deletion_(IfcUtil::IfcBaseClass* entity) { byid_.erase(entity->id()); - const IfcParse::declaration* ty = &entity->declaration(); - remove_type_ref(entity); // entity_file_map is in place to prevent duplicate definitions with usage of add(). @@ -2888,7 +2852,7 @@ AttributeValue IfcEntityInstanceData::get_attribute_value(void* storage, const I if (storage_) { return AttributeValue(storage_, (uint8_t)index); } else { - return AttributeValue((IfcParse::impl::rocks_db_file_storage*)storage, identity, decl, index); + return AttributeValue((IfcParse::impl::rocks_db_file_storage*)storage, identity, decl, (uint8_t) index); } } diff --git a/src/ifcparse/IfcParse.h b/src/ifcparse/IfcParse.h index 9430866ae7..a034fde218 100644 --- a/src/ifcparse/IfcParse.h +++ b/src/ifcparse/IfcParse.h @@ -32,7 +32,7 @@ #include "ifc_parse_api.h" #include "IfcBaseClass.h" #include "IfcCharacterDecoder.h" -#include "IfcSpfStream.h" +#include "FileReader.h" #include "macros.h" #include "storage.h" @@ -103,27 +103,27 @@ class IFC_PARSE_API TokenFunc { // Functions for creating Tokens from an arbitary file offset // The first 4 bits are reserved for Tokens of type ()=,;$* // -Token OperatorTokenPtr(IfcSpfLexer* tokens, unsigned start, unsigned end); -Token GeneralTokenPtr(IfcSpfLexer* tokens, unsigned start, unsigned end); +Token OperatorTokenPtr(IfcSpfLexer* tokens, size_t start, char data); +Token GeneralTokenPtr(IfcSpfLexer* tokens, size_t start, const std::string& data); -/// A stream of tokens to be read from a IfcSpfStream. +/// A stream of tokens to be read from a FileReader. class IFC_PARSE_API IfcSpfLexer { private: IfcCharacterDecoder* decoder_; - unsigned int skipWhitespace() const; - unsigned int skipComment() const; + size_t skipWhitespace() const; + size_t skipComment() const; public: std::string& GetTempString() const { static my_thread_local std::string string; return string; } - IfcSpfStream* stream; + FileReader* stream; // IfcFile* file; - IfcSpfLexer(IfcSpfStream* stream); + IfcSpfLexer(FileReader* stream); Token Next(); ~IfcSpfLexer(); - void TokenString(unsigned int offset, std::string& result); + void TokenString(size_t offset, std::string& result); }; IFC_PARSE_API aggregate_of_instance::ptr traverse(IfcUtil::IfcBaseClass* instance, int max_level = -1); diff --git a/src/ifcparse/IfcSpfHeader.cpp b/src/ifcparse/IfcSpfHeader.cpp index 7e264631d6..2b98adc4df 100644 --- a/src/ifcparse/IfcSpfHeader.cpp +++ b/src/ifcparse/IfcSpfHeader.cpp @@ -30,12 +30,12 @@ static const char* const DATA = "DATA"; using namespace IfcParse; namespace { - IfcEntityInstanceData read_from_spf_file(IfcParse::impl::in_memory_file_storage* storage, size_t s) { + IfcEntityInstanceData read_from_spf_file(IfcParse::impl::in_memory_file_storage* storage, const IfcParse::entity* decl) { if (storage != nullptr) { parse_context pc; storage->tokens->Next(); storage->load(-1, nullptr, pc, -1); - return pc.construct(-1, *storage->references_to_resolve, nullptr, s, -1); + return pc.construct(boost::none, *storage->references_to_resolve, decl, decl->as_entity()->attribute_count(), -1); } else { // std::unreachable(); return IfcEntityInstanceData(in_memory_attribute_storage(10)); @@ -153,19 +153,19 @@ void IfcSpfHeader::read() { readTerminal(Header_section_schema::file_description::Class().name_uc(), NONE); delete file_description_; - file_description_ = new Header_section_schema::file_description(read_from_spf_file(storage_, Header_section_schema::file_description::Class().attribute_count())); + file_description_ = new Header_section_schema::file_description(read_from_spf_file(storage_, &Header_section_schema::file_description::Class())); file_description_->file_ = file_; readSemicolon(); readTerminal(Header_section_schema::file_name::Class().name_uc(), NONE); delete file_name_; - file_name_ = new Header_section_schema::file_name(read_from_spf_file(storage_, Header_section_schema::file_name::Class().attribute_count())); + file_name_ = new Header_section_schema::file_name(read_from_spf_file(storage_, &Header_section_schema::file_name::Class())); file_name_->file_ = file_; readSemicolon(); readTerminal(Header_section_schema::file_schema::Class().name_uc(), NONE); delete file_schema_; - file_schema_ = new Header_section_schema::file_schema(read_from_spf_file(storage_, Header_section_schema::file_schema::Class().attribute_count())); + file_schema_ = new Header_section_schema::file_schema(read_from_spf_file(storage_, &Header_section_schema::file_schema::Class())); file_schema_->file_ = file_; readSemicolon(); } diff --git a/src/ifcparse/IfcSpfStream.h b/src/ifcparse/IfcSpfStream.h deleted file mode 100644 index 7d42a9cd52..0000000000 --- a/src/ifcparse/IfcSpfStream.h +++ /dev/null @@ -1,84 +0,0 @@ -/******************************************************************************** - * * - * This file is part of IfcOpenShell. * - * * - * IfcOpenShell is free software: you can redistribute it and/or modify * - * it under the terms of the Lesser GNU General Public License as published by * - * the Free Software Foundation, either version 3.0 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 * - * Lesser GNU General Public License for more details. * - * * - * You should have received a copy of the Lesser GNU General Public License * - * along with this program. If not, see . * - * * - ********************************************************************************/ - -/********************************************************************************* - * * - * Reads a file and provides functions to access its * - * contents randomly and character by character * - * * - ********************************************************************************/ - -#ifndef IFCSPFSTREAM_H -#define IFCSPFSTREAM_H - -#include "ifc_parse_api.h" - -#include - -#ifdef USE_MMAP -#include -#endif - -namespace IfcParse { -/// The IfcSpfStream class represents a ISO 10303-21 IFC-SPF file in memory. -/// The file is interpreted as a sequence of tokens which are lazily -/// interpreted only when requested. -class IFC_PARSE_API IfcSpfStream { - private: -#ifdef USE_MMAP - boost::iostreams::mapped_file_source mfs; -#endif - FILE* stream_; - const char* buffer_; - unsigned int ptr_; - unsigned int len_; - - public: - bool valid; - bool eof; - unsigned int size; -#ifdef USE_MMAP - IfcSpfStream(const std::string& path, bool mmap = false); -#else - IfcSpfStream(const std::string& path); -#endif - IfcSpfStream(std::istream& stream, int length); - IfcSpfStream(void* data, int length); - ~IfcSpfStream(); - /// Returns the character at the cursor - char Peek(); - /// Returns the character at specified offset - char Read(unsigned int offset); - /// Increment the file cursor and reads new page if necessary - void Inc(); - void Close(); - /// Moves the file cursor to an arbitrary offset in the file - void Seek(unsigned int offset); - /// Returns the cursor position - unsigned int Tell() const; - - bool is_eof_at(unsigned int) const; - void increment_at(unsigned int&); - char peek_at(unsigned int); - - operator bool() const { return valid && !eof; } -}; -} // namespace IfcParse - -#endif diff --git a/src/ifcparse/IfcUtil.cpp b/src/ifcparse/IfcUtil.cpp index 7aab44d391..a47083262f 100644 --- a/src/ifcparse/IfcUtil.cpp +++ b/src/ifcparse/IfcUtil.cpp @@ -76,8 +76,8 @@ void aggregate_of_instance::push(const aggregate_of_instance::ptr& instance) { } } } -unsigned int aggregate_of_instance::size() const { return (unsigned int)list_.size(); } -void aggregate_of_instance::reserve(unsigned capacity) { list_.reserve((size_t)capacity); } +size_t aggregate_of_instance::size() const { return list_.size(); } +void aggregate_of_instance::reserve(size_t capacity) { list_.reserve(capacity); } aggregate_of_instance::it aggregate_of_instance::begin() { return list_.begin(); } aggregate_of_instance::it aggregate_of_instance::end() { return list_.end(); } IfcUtil::IfcBaseClass* aggregate_of_instance::operator[](int i) { diff --git a/src/ifcparse/aggregate_of_instance.h b/src/ifcparse/aggregate_of_instance.h index aff12f426a..bcce94ce6e 100644 --- a/src/ifcparse/aggregate_of_instance.h +++ b/src/ifcparse/aggregate_of_instance.h @@ -48,8 +48,8 @@ class IFC_PARSE_API aggregate_of_instance { it begin(); it end(); IfcUtil::IfcBaseClass* operator[](int index); - unsigned int size() const; - void reserve(unsigned capacity); + size_t size() const; + void reserve(size_t capacity); bool contains(IfcUtil::IfcBaseClass*) const; template @@ -81,7 +81,7 @@ class aggregate_of { } it begin() { return list_.begin(); } it end() { return list_.end(); } - unsigned int size() const { return (unsigned int)list_.size(); } + size_t size() const { return list_.size(); } aggregate_of_instance::ptr generalize() { aggregate_of_instance::ptr result(new aggregate_of_instance()); for (it i = begin(); i != end(); ++i) { diff --git a/src/ifcparse/rocksdb_map_adapter.h b/src/ifcparse/rocksdb_map_adapter.h index a6254356c6..da2346789f 100644 --- a/src/ifcparse/rocksdb_map_adapter.h +++ b/src/ifcparse/rocksdb_map_adapter.h @@ -274,7 +274,7 @@ public: } iterator& operator++() { -#if IFOPSH_WITH_ROCKSDB +#ifdef IFOPSH_WITH_ROCKSDB if (it_) { it_->Next(); check_valid(); diff --git a/src/ifcparse/set_to_map_transformer.h b/src/ifcparse/set_to_map_transformer.h index 21bf597f4f..eccbdfa766 100644 --- a/src/ifcparse/set_to_map_transformer.h +++ b/src/ifcparse/set_to_map_transformer.h @@ -106,7 +106,7 @@ public: return iterator(base_map_->find(k), &transform_); } - size_t erase(const key_type& k) { + size_t erase(const key_type&) { // @todo return 0; } diff --git a/src/ifcparse/storage.h b/src/ifcparse/storage.h index d0ebf4320c..60378c982e 100644 --- a/src/ifcparse/storage.h +++ b/src/ifcparse/storage.h @@ -125,7 +125,7 @@ namespace IfcParse { class IfcFile; class IfcSpfLexer; - class IfcSpfStream; + class FileReader; enum TokenType { Token_NONE, @@ -142,7 +142,7 @@ namespace IfcParse { struct Token { IfcSpfLexer* lexer; //TODO: remove it from here - unsigned startPos; + size_t startPos; TokenType type; union { char value_char; //types: OPERATOR @@ -154,7 +154,7 @@ namespace IfcParse { startPos(0), type(Token_NONE) { } - Token(IfcSpfLexer* _lexer, unsigned _startPos, unsigned /*_endPos*/, TokenType _type) + Token(IfcSpfLexer* _lexer, size_t _startPos, TokenType _type) : lexer(_lexer), startPos(_startPos), type(_type) { @@ -184,15 +184,18 @@ namespace IfcParse { void push(IfcUtil::IfcBaseClass* inst); - IfcEntityInstanceData construct(int name, unresolved_references& references_to_resolve, const IfcParse::declaration* decl, boost::optional expected_size, int resolve_reference_index, bool coerce_attribute_count=true); + IfcEntityInstanceData construct(boost::optional name, unresolved_references& references_to_resolve, const IfcParse::declaration* decl, boost::optional expected_size, int resolve_reference_index, bool coerce_attribute_count=true); }; namespace impl { struct IFC_PARSE_API in_memory_file_storage { - std::vector read_simple_type_instances; + std::vector> read_simple_type_instances; + std::vector> steal_instances() { + return std::move(read_simple_type_instances); + } IfcParse::IfcSpfLexer* tokens; - // IfcParse::IfcSpfStream* stream; + // IfcParse::FileReader* stream; // Either one of these needs to be set IfcParse::IfcFile* file; @@ -214,14 +217,10 @@ namespace IfcParse { typedef entity_instance_by_name_t::iterator iterator; in_memory_file_storage(IfcParse::IfcFile* f = nullptr) : tokens(nullptr), file(f), schema(nullptr) {} - ~in_memory_file_storage() { - for (auto* inst : read_simple_type_instances) { - delete inst; - } - } in_memory_file_storage(const in_memory_file_storage&) = delete; in_memory_file_storage(const in_memory_file_storage&&) = delete; + class type_iterator : public entities_by_type_t::const_iterator { public: using iterator_category = std::forward_iterator_tag; @@ -261,7 +260,7 @@ namespace IfcParse { entities_by_ref_t byref_excl_; entity_instance_by_guid_t byguid_; - void load(unsigned entity_instance_name, const IfcParse::entity* entity, parse_context&, int attribute_index = -1); + void load(boost::optional entity_instance_name, const IfcParse::entity* entity, parse_context&, int attribute_index = -1); void try_read_semicolon() const; void register_inverse(unsigned, const IfcParse::entity* from_entity, int inst_id, int attribute_index); @@ -269,7 +268,7 @@ namespace IfcParse { // @todo is this still used IfcEntityInstanceData read(unsigned int index); - void read_from_stream(IfcParse::IfcSpfStream* stream, const IfcParse::schema_definition*& schema, unsigned int& max_id); + void read_from_stream(IfcParse::FileReader* stream, const IfcParse::schema_definition*& schema, unsigned int& max_id, const std::set& typed_to_bypass); file_open_status good_ = file_open_status::SUCCESS; diff --git a/src/ifcpatch/ifcpatch/recipes/MergeStyles.py b/src/ifcpatch/ifcpatch/recipes/MergeStyles.py index cb2780a360..5c1fa2f4fa 100644 --- a/src/ifcpatch/ifcpatch/recipes/MergeStyles.py +++ b/src/ifcpatch/ifcpatch/recipes/MergeStyles.py @@ -44,11 +44,12 @@ class Patcher(ifcpatch.BasePatcher): uniques = {} i = 0 for element in self.file.by_type(ifc_class): - data = "-".join([str(a) for a in element]) - if unique := uniques.get(data, None): + ifc_class = element.is_a() + key = "-".join([str(a) for a in element]) + if unique := uniques.get(ifc_class, {}).get(key, None): ifcopenshell.util.element.replace_element(element, unique) self.file.remove(element) i += 1 else: - uniques[data] = element + uniques.setdefault(ifc_class, {})[key] = element print(f"Replaced {i} {ifc_class}") diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py index 97c24c59a1..6b5536299d 100644 --- a/src/ifctester/ifctester/facet.py +++ b/src/ifctester/ifctester/facet.py @@ -206,6 +206,12 @@ class Entity(Facet): except: # If the user has specified a class that doesn't exist in the version results = [] + if not self.name.endswith("TYPE"): + try: + for element_type in ifc_file.by_type(f"{self.name}Type"): + results.extend(ifcopenshell.util.element.get_types(element_type)) + except: + pass else: results = [] ifc_classes = [t for t in ifc_file.wrapped_data.types() if t.upper() == self.name] @@ -223,12 +229,25 @@ class Entity(Facet): is_pass = inst.is_a().upper() == self.name reason = None - if not is_pass: + if ( + not is_pass + and inst.file.schema == "IFC2X3" + and not self.name.endswith("TYPE") + and (element_type := ifcopenshell.util.element.get_type(inst)) + ): + is_pass = element_type.is_a().upper() == f"{self.name}TYPE" + reason = {"type": "NAME", "actual": element_type.is_a().upper()[:-4]} + elif not is_pass: reason = {"type": "NAME", "actual": inst.is_a().upper()} if is_pass and self.predefinedType: - predefined_type = ifcopenshell.util.element.get_predefined_type(inst) - is_pass = predefined_type == self.predefinedType + if self.predefinedType == "USERDEFINED": + is_pass = ifcopenshell.util.element.is_userdefined_type(inst) + if not is_pass: + predefined_type = ifcopenshell.util.element.get_predefined_type(inst) + else: + predefined_type = ifcopenshell.util.element.get_predefined_type(inst) + is_pass = predefined_type == self.predefinedType if not is_pass: reason = {"type": "PREDEFINEDTYPE", "actual": predefined_type} diff --git a/src/ifctester/test/test_facet.py b/src/ifctester/test/test_facet.py index 10d94b2f03..167201979b 100644 --- a/src/ifctester/test/test_facet.py +++ b/src/ifctester/test/test_facet.py @@ -172,10 +172,10 @@ class TestEntity: facet = Entity(name="IFCWALL", predefinedType="USERDEFINED") ifc = ifcopenshell.file() run( - "A predefined type must always specify a meaningful type, not USERDEFINED itself", + "A predefined type may specify USERDEFINED itself", facet=facet, inst=ifc.createIfcWall(PredefinedType="USERDEFINED", ObjectType="WALDO"), - expected=False, + expected=True, ) ifc = ifcopenshell.file() @@ -230,6 +230,32 @@ class TestEntity: wall3 = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall", predefined_type="BAZFOO") run("Restrictions an be specified for the predefined type 3/3", facet=facet, inst=wall3, expected=False) + def test_ifc2x3_occurrence_type_mapping(self): + set_facet("entity") + + ifc = ifcopenshell.file(schema="IFC2X3") + application = ifcopenshell.api.owner.add_application(ifc) + person = ifcopenshell.api.owner.add_person( + ifc, identification="LPARTEE", family_name="Partee", given_name="Leeable" + ) + organisation = ifcopenshell.api.owner.add_organisation( + ifc, identification="AWB", name="Architects Without Ballpens" + ) + user = ifcopenshell.api.owner.add_person_and_organisation(ifc, person=person, organisation=organisation) + ifcopenshell.api.owner.settings.get_user = lambda x: user + ifcopenshell.api.owner.settings.get_application = lambda x: application + + element = ifcopenshell.api.root.create_entity(ifc, "IfcFlowTerminal") + element_type = ifcopenshell.api.root.create_entity(ifc, "IfcAirTerminalType") + ifcopenshell.api.type.assign_type(ifc, related_objects=[element], relating_type=element_type) + facet = Entity(name="IFCAIRTERMINAL") + assert facet.filter(ifc) == [element] + run("In IFC2X3 the type class is checked instead 1/2", facet=facet, inst=element, expected=True) + + facet = Entity(name="IFCELECTRICAPPLIANCE") + assert facet.filter(ifc) == [] + run("In IFC2X3 the type class is checked instead 2/2", facet=facet, inst=element, expected=False) + def test_to_string_required_applicability(self): spec = ifctester.ids.Specification(name="Foo", minOccurs=1, maxOccurs="unbounded") facet = Entity(name="IFCWALL") diff --git a/src/ifcwrap/IfcParseWrapper.i b/src/ifcwrap/IfcParseWrapper.i index 45a617916e..858705ebf0 100644 --- a/src/ifcwrap/IfcParseWrapper.i +++ b/src/ifcwrap/IfcParseWrapper.i @@ -36,7 +36,8 @@ private: %ignore IfcParse::InstanceStreamer::InstanceStreamer(const IfcParse::schema_definition* schema, IfcParse::IfcSpfLexer* lexer); -%ignore IfcParse::InstanceStreamer::read_instance; +%ignore IfcParse::InstanceStreamer::readInstance; +%ignore IfcParse::InstanceStreamer::stealInstances; %ignore in_memory_file_storage; %ignore rocks_db_file_storage; @@ -650,6 +651,8 @@ private: %extend IfcParse::IfcSpfHeader { // Cast to base class pointers for SWIG, because // it has no idea about the schema definitions. + // The code to access these methods as attributes + // is in file.py IfcUtil::IfcBaseClass* file_description_py() { return $self->file_description(); } @@ -659,14 +662,6 @@ private: IfcUtil::IfcBaseClass* file_schema_py() { return $self->file_schema(); } - - %pythoncode %{ - # Hide the getters with read-only property implementations - # self.file_ref is set in ifcopenshell.file.header() - file_description = property(lambda self: self.file_ref(self.file_description_py())) - file_name = property(lambda self: self.file_ref(self.file_name_py())) - file_schema = property(lambda self: self.file_ref(self.file_schema_py())) - %} }; %extend IfcParse::FileDescription { @@ -1002,7 +997,7 @@ private: %} %extend IfcParse::InstanceStreamer { - PyObject* read_instance_py() { + PyObject* readInstancePy(bool type_as_declaration_instance=false) { auto simply_type_to_dictionary = [&](IfcUtil::IfcBaseClass* t) -> PyObject* { const auto& nm = t->declaration().name(); auto ifc_val = t->get_attribute_value(0); @@ -1070,7 +1065,7 @@ private: Py_INCREF(Py_None); return Py_None; } - auto inst = self->read_instance(); + auto inst = self->readInstance(); if (!inst) { Py_INCREF(Py_None); return Py_None; @@ -1088,7 +1083,13 @@ private: { const std::string& key_cpp = "type"; auto name_py = pythonize(key_cpp); - auto value_py = pythonize(std::get<1>(*inst)->name()); + PyObject* value_py; + if (type_as_declaration_instance) { + // @todo should this just be the default behavior? + value_py = pythonize(std::get<1>(*inst)); + } else { + value_py = pythonize(std::get<1>(*inst)->name()); + } PyDict_SetItem(d, name_py, value_py); Py_DECREF(name_py); Py_DECREF(value_py); diff --git a/src/serializers/RocksDbSerializer.cpp b/src/serializers/RocksDbSerializer.cpp index 014b519807..f5e95e7a3b 100644 --- a/src/serializers/RocksDbSerializer.cpp +++ b/src/serializers/RocksDbSerializer.cpp @@ -135,7 +135,7 @@ void RocksDbSerializer::write_streaming_() { streamer.coerce_attribute_count = false; while (streamer) { - auto inst = streamer.read_instance(); + auto inst = streamer.readInstance(); if (inst) { // name can be zero in case of header instances auto name = std::get<0>(*inst); @@ -259,7 +259,8 @@ void RocksDbSerializer::write_streaming_() { } // @nb we also need to delete them - delete inst; + // not anymore, as they are now registered as unique_ptr in the in_memory_file_storage + // delete inst; } // Entity type as numeric ref to index_in_schema diff --git a/win/build-deps.cmd b/win/build-deps.cmd index 037e090d47..3c3a174a5f 100644 --- a/win/build-deps.cmd +++ b/win/build-deps.cmd @@ -35,7 +35,7 @@ if NOT "%CD%" == "%batpath%" ( set PROJECT_NAME=IfcOpenShell -call utils\cecho.cmd 15 0 "This script fetches and builds all %PROJECT_NAME% dependencies" +call utils\cecho.cmd 0 15 "This script fetches and builds all %PROJECT_NAME% dependencies" echo. :: Enable the delayed environment variable expansion needed in vs-cfg.cmd. diff --git a/win/utils/cecho.cmd b/win/utils/cecho.cmd index 2923fcb986..f0eb1b3749 100644 --- a/win/utils/cecho.cmd +++ b/win/utils/cecho.cmd @@ -1,4 +1,42 @@ -:: Usage: call cecho.cmd background foreground "message here" -:: NOTES/TODOS 1) This is super slow 2) leading spaces are omitted 3) printing string with quotes doesn't work (quotes must be escaped awkwardly) -@powershell -command write-host -background %~1 -foreground "%~2" "%3" -@exit /b +@echo off +setlocal ENABLEDELAYEDEXPANSION + +rem ========================================================== +rem cecho.cmd - Fast ANSI color echo for Windows 10+ +rem Emulates PowerShell escape codes like `t and `n +rem Usage: call cecho.cmd "Message" +rem ========================================================== + +for /F %%A in ('echo prompt $E ^| cmd') do set "ESC=%%A" + +set "BG=%~1" +set "FG=%~2" +set "TEXT=%~3" + +rem Replace PowerShell-style escapes with real characters +set "TEXT=%TEXT:`t= %" +set "TEXT=%TEXT:`"="%" + +rem Foreground color table (0–15) +set "FGCOLOR[0]=30" & set "FGCOLOR[1]=34" & set "FGCOLOR[2]=32" & set "FGCOLOR[3]=36" +set "FGCOLOR[4]=31" & set "FGCOLOR[5]=35" & set "FGCOLOR[6]=33" & set "FGCOLOR[7]=37" +set "FGCOLOR[8]=90" & set "FGCOLOR[9]=94" & set "FGCOLOR[10]=92" & set "FGCOLOR[11]=96" +set "FGCOLOR[12]=91" & set "FGCOLOR[13]=95" & set "FGCOLOR[14]=93" & set "FGCOLOR[15]=97" + +rem Background color table (0–7) +set "BGCOLOR[0]=40" & set "BGCOLOR[1]=44" & set "BGCOLOR[2]=42" & set "BGCOLOR[3]=46" +set "BGCOLOR[4]=41" & set "BGCOLOR[5]=45" & set "BGCOLOR[6]=43" & set "BGCOLOR[7]=47" + +set "FGC=!FGCOLOR[%FG%]!" +set "BGC=!BGCOLOR[%BG%]!" + +if defined BGC ( + set "STYLE=!ESC![!BGC!;!FGC!m" +) else ( + set "STYLE=!ESC![!FGC!m" +) + +echo|set /p="!STYLE!!TEXT!!ESC![0m" +echo. +endlocal +exit /b 0