Merge remote-tracking branch 'origin/v0.8.0' into test-ci

This commit is contained in:
Thomas Krijnen
2025-10-26 09:50:01 +01:00
119 changed files with 3737 additions and 1306 deletions
+18 -14
View File
@@ -50,6 +50,7 @@ jobs:
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
pip install src/bcf --no-deps pip install src/bcf --no-deps
pip install git+https://github.com/zdhoward/aud pip install git+https://github.com/zdhoward/aud
pip install pytest-xdist==3.8.0
- name: Install C++ dependencies - name: Install C++ dependencies
run: | run: |
@@ -120,20 +121,23 @@ jobs:
sudo make -j $(nproc) sudo make -j $(nproc)
sudo make install sudo make install
- name: Run IfcConvert on Sample files # - name: Run IfcConvert on Sample files
run: | # run: |
(find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \ # (find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \
echo $i | tee -a log; \ # echo $i | tee -a log; \
timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \ # timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \
echo $i $? >> statuses; \ # echo $i $? >> statuses; \
done) || true # done) || true
echo Failed # echo Failed
grep -v 0$ statuses # grep -v 0$ statuses
grep -v 0$ statuses | wc -l # grep -v 0$ statuses | wc -l
echo Succeeded # echo Succeeded
grep 0$ statuses # grep 0$ statuses
grep 0$ statuses | wc -l # 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 - name: Test ifcopenshell-python
run: | run: |
@@ -143,7 +147,7 @@ jobs:
mv ifcopenshell ifcopenshell-local # Force testing on installed module mv ifcopenshell ifcopenshell-local # Force testing on installed module
pip install -e ../ifcpatch --no-deps # Needed for sql.py tests. pip install -e ../ifcpatch --no-deps # Needed for sql.py tests.
ERROR=0 ERROR=0
make test || ERROR=1 make test-parallel || ERROR=1
cd ../bcf && make test || ERROR=1 cd ../bcf && make test || ERROR=1
pip install requests pip install requests
cd ../bsdd && make test || ERROR=1 cd ../bsdd && make test || ERROR=1
+4 -1
View File
@@ -109,4 +109,7 @@ src/bonsai/bonsai/bim/schema/Brick.ttl
bonsaiDecoratorForLoads.code-workspace bonsaiDecoratorForLoads.code-workspace
dev_environment.bat dev_environment.bat
.pixi/ .pixi/
src/ifcopenshell-python/ifcopenshell/express/*.exp
src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
+18 -18
View File
@@ -37,25 +37,25 @@ Contents
| Name | Description | License | Service | | 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) | | [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 | 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/) | | [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 | 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/) | | [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 | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later | | [ifc2ca](https://docs.ifcopenshell.org/ifc2ca.html) | 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/) | | [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 | 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/) | | [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 | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later | | [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\* | | 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/) | | [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 | 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/) | | [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 | 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) | [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 | 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/) | | [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 | 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/) | | [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 | 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/) | | [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 | 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) | [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 | 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) | | [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 | 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/) | | [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 | 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) | [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 | 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/) | | [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: The IfcOpenShell C++ codebase is split into multiple interal libraries:
+6 -7
View File
@@ -426,7 +426,7 @@ if(WASM_BUILD)
else() else()
# @todo review this, shouldn't this be all possible header-only now? # @todo review this, shouldn't this be all possible header-only now?
# ... or rewritten using C++17 features? # ... 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() endif()
if(USE_MMAP) if(USE_MMAP)
@@ -1044,11 +1044,7 @@ if(BUILD_IFCGEOM)
foreach(schema ${SCHEMA_VERSIONS}) foreach(schema ${SCHEMA_VERSIONS})
set(IFCGEOM_SCHEMA_LIBRARIES ${IFCGEOM_SCHEMA_LIBRARIES} geometry_mapping_ifc${schema}) set(IFCGEOM_SCHEMA_LIBRARIES ${IFCGEOM_SCHEMA_LIBRARIES} geometry_mapping_ifc${schema})
endforeach() endforeach()
if(WASM_BUILD) set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
else()
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
endif()
endif() endif()
if(BUILD_CONVERT OR BUILD_IFCPYTHON) if(BUILD_CONVERT OR BUILD_IFCPYTHON)
@@ -1177,7 +1173,10 @@ if(BUILD_IFCGEOM)
add_library(geometry_mapping_ifc${schema} STATIC ${IFCGEOM_FILES}) 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}") 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}) list(APPEND mapping_libraries geometry_mapping_ifc${schema})
endforeach() endforeach()
+1 -1
View File
@@ -154,7 +154,7 @@ SWIG_VERSION = "4.1.0"
OPENCOLLADA_VERSION = "v1.6.68" OPENCOLLADA_VERSION = "v1.6.68"
HDF5_VERSION = "1.13.1" 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 MPFR_VERSION = "3.1.6" # latest is 4.1.0
CGAL_VERSION = "v5.6.3" CGAL_VERSION = "v5.6.3"
USD_VERSION = "23.05" USD_VERSION = "23.05"
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

@@ -13,10 +13,10 @@ DATA;
#6=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); #6=IFCSIMPLEPROPERTYTEMPLATE('0AK5C2UpL4$eaac2LszAx$',$,'HasUnderlay','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); #7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
#8=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.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.); #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.); #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 ifc 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.); #13=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#14=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); #14=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
#15=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); #15=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
+11 -1
View File
@@ -36,6 +36,7 @@ from bonsai.bim.ifc import IfcStore
from mathutils import Vector from mathutils import Vector
from typing import Union from typing import Union
from logging import Logger from logging import Logger
from math import radians
class IfcExporter: class IfcExporter:
@@ -104,7 +105,16 @@ class IfcExporter:
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]: 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) 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) bpy.ops.bim.update_representation(obj=obj.name)
# update_representation might not apply scale if the object has openings # update_representation might not apply scale if the object has openings
# reset it, so let user know that the scale wasn't saved. # reset it, so let user know that the scale wasn't saved.
-2
View File
@@ -222,8 +222,6 @@ def refresh_ui_data():
if isinstance(ifc_file := tool.Ifc.get(), ifcopenshell.sqlite): if isinstance(ifc_file := tool.Ifc.get(), ifcopenshell.sqlite):
ifc_file.clear_cache() 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: if tool.Web.get_web_props().is_connected:
tool.Web.send_webui_data() tool.Web.send_webui_data()
+3
View File
@@ -952,6 +952,9 @@ class IfcImporter:
if not props.ifc_file: if not props.ifc_file:
props.ifc_file = self.ifc_import_settings.input_file props.ifc_file = self.ifc_import_settings.input_file
self.file = tool.Ifc.get() 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): def calculate_unit_scale(self):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
@@ -94,6 +94,9 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
def _execute(self, context): 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(): for obj in tool.Blender.get_selected_objects():
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if not element: 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) aggregate = ifcopenshell.util.element.get_aggregate(element)
if not aggregate: if not aggregate:
continue continue
# Track this aggregate for later checking
aggregates_to_check.add(aggregate)
core.unassign_object( core.unassign_object(
tool.Ifc, tool.Ifc,
tool.Aggregate, 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"]) pset = tool.Ifc.get().by_id(pset["id"])
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset) 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): class BIM_OT_enable_editing_aggregate(bpy.types.Operator):
"""Enable editing aggregation relationship""" """Enable editing aggregation relationship"""
@@ -282,7 +282,11 @@ class BIM_PT_classification_references(Panel, ReferenceUI):
@classmethod @classmethod
def poll(cls, context): 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: def get_object_name(self, context: bpy.types.Context) -> str:
assert (obj := context.active_object) assert (obj := context.active_object)
+27 -3
View File
@@ -818,7 +818,12 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator): class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.merge_identical_objects" bl_idname = "bim.merge_identical_objects"
bl_label = "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"} bl_options = {"REGISTER", "UNDO"}
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration] 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)), 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: if TYPE_CHECKING:
object_type: tool.Debug.PurgeMergeObjectType 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): def _execute(self, context):
object_type: str = self.object_type object_type: str = self.object_type
if object_type in ("PROFILE", "TYPE"): if object_type in ("PROFILE", "TYPE"):
self.report({"ERROR"}, f"Unsupported object type {object_type}.") self.report({"ERROR"}, f"Unsupported object type {object_type}.")
return {"CANCELLED"} 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" plural_object_type = f"{object_type.lower().replace('_', ' ')}s"
if merged_data: 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(): for element_type, element_names in merged_data.items():
print(f"- {element_type}:") print(f"- {element_type}:")
for name in element_names: 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()) merged = sum(len(v) for v in merged_data.values())
msg = " See system console for details." if merged else "" 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: if merged == 0:
return return
@@ -61,6 +61,7 @@ classes = (
operator.EnableEditingAssignedProduct, operator.EnableEditingAssignedProduct,
operator.EnableEditingElementFilter, operator.EnableEditingElementFilter,
operator.EnableEditingText, operator.EnableEditingText,
operator.ExcludeAnnotation,
operator.ExpandSheet, operator.ExpandSheet,
operator.LoadDrawings, operator.LoadDrawings,
operator.LoadReferences, operator.LoadReferences,
+8 -3
View File
@@ -235,13 +235,18 @@ class DecoratorData:
cut_cache = {} cut_cache = {}
slice_cache = {} slice_cache = {}
fill_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 @classmethod
def load(cls, handler): def load(cls, handler):
cls.is_loaded = True cls.is_loaded = True
cls.cut_cache = {}
cls.layerset_cache = {}
cls.fill_cache = {}
text = {} text = {}
dimension = {} dimension = {}
@@ -22,6 +22,7 @@ import blf
import math import math
import bmesh import bmesh
import shapely import shapely
import numpy as np
import ifcopenshell import ifcopenshell
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.representation import ifcopenshell.util.representation
@@ -1625,6 +1626,7 @@ class CutDecorator:
if cls.installed: if cls.installed:
cls.uninstall() cls.uninstall()
handler = cls() handler = cls()
handler.cache_camera_matrix()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW") cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
@classmethod @classmethod
@@ -1727,6 +1729,30 @@ class CutDecorator:
shader.uniform_float("color", color) shader.uniform_float("color", color)
batch.draw(shader) 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: def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
has_cut_cache = element.id() in DecoratorData.cut_cache has_cut_cache = element.id() in DecoratorData.cut_cache
has_fill_cache = element.id() in DecoratorData.fill_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. # 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 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) 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) self.recalculate_fill(context, obj, element)
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None: 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 # 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 # 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") cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
if not DecoratorData.is_loaded: DecoratorData.clear_cache()
DecoratorData.load(handler)
@classmethod @classmethod
def uninstall(cls): def uninstall(cls):
@@ -1961,5 +1986,8 @@ class DecorationsHandler:
if not DrawingsData.is_loaded: if not DrawingsData.is_loaded:
DrawingsData.load() DrawingsData.load()
if not DecoratorData.is_loaded:
DecoratorData.load(self)
for obj, decorator in DecoratorData.data["object_decorators"]: for obj, decorator in DecoratorData.data["object_decorators"]:
decorator.decorate(context, obj) decorator.decorate(context, obj)
@@ -210,12 +210,6 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
should_duplicate_annotations=self.should_duplicate_annotations, 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): class CreateDrawing(bpy.types.Operator):
"""Creates/refreshes a .svg drawing """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")) context.scene.render.filepath = str(Path(svg_path).with_suffix(".png"))
assert (drawing_style := self.cprops.get_active_drawing_style()) assert (drawing_style := self.cprops.get_active_drawing_style())
tool.Blender.sync_render_visibility()
if drawing_style.render_type == "DEFAULT": if drawing_style.render_type == "DEFAULT":
bpy.ops.render.render(write_still=True) bpy.ops.render.render(write_still=True)
else: else:
@@ -625,7 +621,7 @@ class CreateDrawing(bpy.types.Operator):
path.attrib["d"] = d path.attrib["d"] = d
group.append(g) 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]"): 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(): if "projection" in el.get("class", "").split():
continue continue
@@ -794,8 +790,9 @@ class CreateDrawing(bpy.types.Operator):
edge_bm.to_mesh(edge_mesh) edge_bm.to_mesh(edge_mesh)
edge_bm.free() 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] 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) bpy.ops.render.render(write_still=False)
os.replace(actual_path, svg_path) os.replace(actual_path, svg_path)
@@ -839,6 +836,8 @@ class CreateDrawing(bpy.types.Operator):
if tool.Drawing.is_camera_orthographic(): if tool.Drawing.is_camera_orthographic():
self.generate_bisect_linework(context, root) 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.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root) self.move_elements_to_top(root)
@@ -936,12 +935,14 @@ class CreateDrawing(bpy.types.Operator):
if self.cprops.cut_mode == "BISECT": if self.cprops.cut_mode == "BISECT":
self.remove_cut_linework(root) self.remove_cut_linework(root)
self.generate_bisect_linework(context, 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.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root) self.move_elements_to_top(root)
elif self.cprops.cut_mode == "OPENCASCADE": elif self.cprops.cut_mode == "OPENCASCADE":
self.move_projection_to_bottom(root) 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.merge_linework_and_add_metadata(root)
self.move_elements_to_top(root) self.move_elements_to_top(root)
@@ -1251,6 +1252,8 @@ class CreateDrawing(bpy.types.Operator):
def get_svg_classes(self, element, layer=None): def get_svg_classes(self, element, layer=None):
classes = [element.is_a()] classes = [element.is_a()]
# ─── Material ──────────────────────────────────────────────
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True) material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
material_name = "" material_name = ""
if material: if material:
@@ -1263,6 +1266,7 @@ class CreateDrawing(bpy.types.Operator):
else: else:
classes.append("material-null") classes.append("material-null")
# ─── Layer ─────────────────────────────────────────────────
if layer: if layer:
classes.append(layer.is_a()) classes.append(layer.is_a())
layer_material = layer.Material layer_material = layer.Material
@@ -1275,12 +1279,19 @@ class CreateDrawing(bpy.types.Operator):
layer_material_category = tool.Drawing.canonicalise_class_name(layer_material.Category) layer_material_category = tool.Drawing.canonicalise_class_name(layer_material.Category)
classes.append(f"layer-material-category-{layer_material_category}") classes.append(f"layer-material-category-{layer_material_category}")
# ─── Metadata ──────────────────────────────────────────────
for key in self.metadata: for key in self.metadata:
value = ifcopenshell.util.selector.get_element_value(element, key) value = ifcopenshell.util.selector.get_element_value(element, key)
if value: if value:
classes.append( classes.append(
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value)) 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 return classes
def is_manifold(self, obj) -> bool: def is_manifold(self, obj) -> bool:
@@ -1347,7 +1358,13 @@ class CreateDrawing(bpy.types.Operator):
join_criteria = join_criteria.split(",") join_criteria = join_criteria.split(",")
else: else:
# Drawing convention states that same objects classes with the same material are merged when cut. # 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") group = root.find("{http://www.w3.org/2000/svg}g")
joined_paths = {} joined_paths = {}
@@ -1476,11 +1493,10 @@ class CreateDrawing(bpy.types.Operator):
joined_paths.setdefault(hash_keys, []).append(el) joined_paths.setdefault(hash_keys, []).append(el)
for key, els in joined_paths.items(): for key, els in joined_paths.items():
polygons = [] queue = []
classes = set()
for el in els: 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"]) classes.add(el.attrib["{http://www.ifcopenshell.org/ns}guid"])
is_closed_polygon = False is_closed_polygon = False
for path in el.findall("{http://www.w3.org/2000/svg}path"): 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]) coords.append(coords[0])
if len(coords) > 2 and coords[0] == coords[-1]: if len(coords) > 2 and coords[0] == coords[-1]:
is_closed_polygon = True is_closed_polygon = True
polygons.append(shapely.Polygon(coords)) queue.append((shapely.Polygon(coords), classes))
if is_closed_polygon: if is_closed_polygon:
el.getparent().remove(el) el.getparent().remove(el)
try: while queue:
merged_polygons = shapely.ops.unary_union(polygons) polygon, polygon_classes = queue.pop()
except: for polygon2, polygon2_classes in queue[:]:
print("Warning. Portions of the merge failed. Please report a bug!", polygons) try:
merged_polygons = polygons 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") g = etree.Element("g")
path = etree.SubElement(g, "path") 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" 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: for interior in polygon.interiors:
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z" d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
path.attrib["d"] = d path.attrib["d"] = d
g.set("class", " ".join(list(classes))) g.set("class", " ".join(list(polygon_classes)))
group.append(g) group.append(g)
def drawing_to_model_co(self, x: float, y: float) -> Vector: def drawing_to_model_co(self, x: float, y: float) -> Vector:
@@ -1861,13 +1876,15 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
props = tool.Drawing.get_document_props() 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(): if not tool.Drawing.get_active_drawing_item():
cls.poll_message_set("No drawing selected.") cls.poll_message_set("No drawing selected.")
return False 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 return True
def _execute(self, context): def _execute(self, context):
@@ -2221,6 +2238,7 @@ class ActivateModel(bpy.types.Operator):
) )
tool.Blender.reset_object_visibility() tool.Blender.reset_object_visibility()
tool.Drawing.hide_all_drawing_collections()
tool.Blender.update_viewport() tool.Blender.update_viewport()
bonsai.bim.handler.refresh_ui_data() bonsai.bim.handler.refresh_ui_data()
@@ -2317,9 +2335,8 @@ class ActivateDrawingBase(tool.Ifc.Operator):
dprops.active_drawing_id = self.drawing dprops.active_drawing_id = self.drawing
dprops.drawing_styles.clear() dprops.drawing_styles.clear()
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"): bpy.ops.bim.reload_drawing_styles()
bpy.ops.bim.reload_drawing_styles() bpy.ops.bim.activate_drawing_style()
bpy.ops.bim.activate_drawing_style()
if tool.Drawing.is_camera_orthographic(): if tool.Drawing.is_camera_orthographic():
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing)) 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 camera = context.scene.camera
assert camera assert camera
camera_props = tool.Drawing.get_camera_props(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): if camera_props.update_representation(camera.matrix_world):
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="") bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
# See 6452 and 6478. # Restore the scale after update if needed
# bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT") if is_reflected:
camera.scale = (-1, -1, -1)
return {"FINISHED"} return {"FINISHED"}
@@ -2811,8 +2840,13 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
if not props.schedules: if not props.schedules:
cls.poll_message_set("No schedule selected.") cls.poll_message_set("No schedule selected.")
return False return False
prefs = tool.Blender.get_addon_preferences() if not props.sheets:
return props.schedules and props.sheets and prefs.data_dir 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): def _execute(self, context):
props = tool.Drawing.get_document_props() props = tool.Drawing.get_document_props()
@@ -2879,8 +2913,13 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
if not props.references: if not props.references:
cls.poll_message_set("No reference selected.") cls.poll_message_set("No reference selected.")
return False return False
bim_props = tool.Blender.get_bim_props() if not props.sheets:
return props.references and props.sheets and bim_props.data_dir 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): def _execute(self, context):
props = tool.Drawing.get_document_props() props = tool.Drawing.get_document_props()
@@ -3729,3 +3768,19 @@ class OpenDocumentationWebUi(bpy.types.Operator):
else: else:
bpy.ops.bim.open_web_browser(page="documentation") bpy.ops.bim.open_web_browser(page="documentation")
return {"FINISHED"} 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)
@@ -498,6 +498,9 @@ class BIMCameraProperties(PropertyGroup):
name="Linework Mode", name="Linework Mode",
update=get_update_layer_callback("linework_mode", "LineworkMode"), 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( fill_mode: EnumProperty(
items=[ items=[
("NONE", "None", "Disable filling areas seen in projection"), ("NONE", "None", "Disable filling areas seen in projection"),
@@ -859,6 +862,19 @@ class BIMAnnotationProperties(PropertyGroup):
) )
is_adding_type: bpy.props.BoolProperty(default=False) is_adding_type: bpy.props.BoolProperty(default=False)
type_name: bpy.props.StringProperty(name="Name", default="TYPEX") 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: if TYPE_CHECKING:
object_type: str object_type: str
+3 -1
View File
@@ -99,12 +99,13 @@ class BIM_PT_camera(Panel):
row = self.layout.row() row = self.layout.row()
row.prop(props, "linework_mode") row.prop(props, "linework_mode")
row = self.layout.row()
row.prop(props, "generate_material_layers")
if props.linework_mode == "OPENCASCADE": if props.linework_mode == "OPENCASCADE":
row = self.layout.row() row = self.layout.row()
row.prop(props, "fill_mode") row.prop(props, "fill_mode")
row = self.layout.row() row = self.layout.row()
row.prop(props, "cut_mode") row.prop(props, "cut_mode")
row = self.layout.row() row = self.layout.row()
row.prop(props, "width") row.prop(props, "width")
row = self.layout.row() 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" text = "Exclude Filter" if ElementFiltersData.data["has_exclude_filter"] else "No Exclude Filter Found"
icon = "GREASEPENCIL" if ElementFiltersData.data["has_exclude_filter"] else "ADD" icon = "GREASEPENCIL" if ElementFiltersData.data["has_exclude_filter"] else "ADD"
row.label(text=text, icon="FILTER") 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" row.operator("bim.enable_editing_element_filter", icon=icon, text="").filter_mode = "EXCLUDE"
@@ -228,7 +228,6 @@ class AnnotationToolUI:
def draw_type_selection_interface(cls): def draw_type_selection_interface(cls):
# shared by both sidebar and header # shared by both sidebar and header
object_type = cls.props.object_type object_type = cls.props.object_type
row = cls.layout.row(align=True) row = cls.layout.row(align=True)
row.label(text="", icon="FILE_VOLUME") row.label(text="", icon="FILE_VOLUME")
prop_with_search(row, cls.props, "object_type", text="") 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") add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
if object_type in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP: 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( add_layout_hotkey_operator(
cls.layout, cls.layout,
"Bulk Tag", "Bulk Tag",
@@ -299,6 +301,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
return return
related_objects = bpy.context.selected_objects related_objects = bpy.context.selected_objects
created_objects = []
for related_object in related_objects: for related_object in related_objects:
obj = core.add_annotation( obj = core.add_annotation(
tool.Ifc, tool.Ifc,
@@ -311,7 +315,15 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
), ),
enable_editing=False, 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): def hotkey_S_A(self):
if bpy.ops.bim.add_annotation.poll(): if bpy.ops.bim.add_annotation.poll():
@@ -341,4 +353,5 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
continue continue
related_object = tool.Ifc.get_object(related_product) 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)
@@ -109,8 +109,9 @@ def block_scale(scene: bpy.types.Scene) -> None:
if obj.type == "CAMERA": if obj.type == "CAMERA":
camera = tool.Ifc.get_entity(obj) camera = tool.Ifc.get_entity(obj)
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW": if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
obj.scale = (-1, -1, -1) # Only update if scale isn't already (-1, -1, -1)
obj.rotation_euler = (0.0, 0.0, math.radians(180)) if obj.scale != (-1, -1, -1):
obj.scale = (-1, -1, -1)
else: else:
if obj.scale != (1, 1, 1): if obj.scale != (1, 1, 1):
obj.scale = (1, 1, 1) obj.scale = (1, 1, 1)
@@ -858,6 +858,10 @@ class OverrideDelete(bpy.types.Operator):
objects_to_remove = context.selected_objects objects_to_remove = context.selected_objects
self.process_arrays(context) self.process_arrays(context)
# Track aggregates before deleting their parts
aggregates_to_check = self.track_aggregates(objects_to_remove)
clear_active_object = True clear_active_object = True
for i, obj in enumerate(objects_to_remove, 1): for i, obj in enumerate(objects_to_remove, 1):
@@ -882,13 +886,28 @@ class OverrideDelete(bpy.types.Operator):
continue continue
if ifcopenshell.util.element.get_pset(element, "BBIM_Array"): if ifcopenshell.util.element.get_pset(element, "BBIM_Array"):
self.report({"INFO"}, "Elements that are part of an array cannot be deleted.") 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) tool.Geometry.delete_ifc_object(obj)
elif tool.Geometry.is_representation_item(obj): elif tool.Geometry.is_representation_item(obj):
tool.Geometry.delete_ifc_item(obj) tool.Geometry.delete_ifc_item(obj)
else: else:
bpy.data.objects.remove(obj) 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: for opening in tool.Model.get_model_props().openings:
if opening.obj is not None and not tool.Ifc.get_entity(opening.obj): if opening.obj is not None and not tool.Ifc.get_entity(opening.obj):
bpy.data.objects.remove(opening.obj) bpy.data.objects.remove(opening.obj)
@@ -921,6 +940,49 @@ class OverrideDelete(bpy.types.Operator):
data["old_file"].redo() data["old_file"].redo()
tool.Ifc.set(data["new_file"]) 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: def process_arrays(self, context: bpy.types.Context) -> None:
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
selected_objects = set(context.selected_objects) 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") item_mesh = bpy.data.meshes.new("tmp")
tool.Ifc.link(item, item_mesh) tool.Ifc.link(item, item_mesh)
item_obj = bpy.data.objects.new("tmp", 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) tool.Geometry.name_item_object(item_obj, item)
item_obj.matrix_world = obj.matrix_world item_obj.matrix_world = obj.matrix_world
bpy.context.collection.objects.link(item_obj) bpy.context.collection.objects.link(item_obj)
+1 -1
View File
@@ -41,7 +41,7 @@ class SolarData:
@classmethod @classmethod
def sun_position(cls): def sun_position(cls):
return tool.Blender.get_sun_position_addon() return tool.Blender.get_addon("sun_position")
@classmethod @classmethod
def sites(cls): def sites(cls):
@@ -202,7 +202,7 @@ class RadianceRender(bpy.types.Operator):
print(f"Camera position: {camera_position}") print(f"Camera position: {camera_position}")
print(f"Camera direction: {camera_direction}") 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( # azimuth, elevation = sun_position.sun_calc.get_sun_coordinates(
# sun_pos_props.time, # sun_pos_props.time,
# sun_pos_props.latitude, # sun_pos_props.latitude,
+1 -1
View File
@@ -40,7 +40,7 @@ from bpy.types import PropertyGroup
from bonsai.bim.module.light.data import SolarData from bonsai.bim.module.light.data import SolarData
from bonsai.bim.module.light.decorator import SolarDecorator 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() now = datetime.datetime.now()
with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f: with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f:
+34 -3
View File
@@ -373,7 +373,12 @@ class BIMStairProperties(PropertyGroup):
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0: if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
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) is_editing: bpy.props.BoolProperty(default=False)
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE") width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE")
@@ -407,6 +412,12 @@ class BIMStairProperties(PropertyGroup):
default="CONCRETE", default="CONCRETE",
update=validate_nosing_value, 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( custom_first_last_tread_run: bpy.props.FloatVectorProperty(
name="Custom First / Last Treads Widths", name="Custom First / Last Treads Widths",
description='Specify custom first / last treads widths, different from the general "Tread Run". Leave 0 to disable.', 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 top_slab_depth: float
has_top_nib: bool has_top_nib: bool
stair_type: str stair_type: str
custom_tread_lock: bool
custom_first_last_tread_run: tuple[float, float] custom_first_last_tread_run: tuple[float, float]
nosing_length: float nosing_length: float
nosing_depth: float nosing_depth: float
@@ -480,8 +492,11 @@ class BIMStairProperties(PropertyGroup):
} }
stair_kwargs.update(generic_props) stair_kwargs.update(generic_props)
# defined here to appear last in UI # If locked, use tread_run for both first and last treads
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run 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: if not convert_to_project_units:
return stair_kwargs 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) stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, self.non_si_units_props)
return stair_kwargs 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): def set_props_kwargs_from_ifc_data(self, kwargs):
kwargs = tool.Model.convert_data_to_si_units(kwargs, self.non_si_units_props) 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: for prop_name in kwargs:
setattr(self, prop_name, kwargs[prop_name]) setattr(self, prop_name, kwargs[prop_name])
+4 -2
View File
@@ -188,7 +188,8 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
props = tool.Model.get_stair_props(obj) props = tool.Model.get_stair_props(obj)
ifc_file = tool.Ifc.get() 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") pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
if not pset: if not pset:
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="BBIM_Stair") 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 assert element
props = tool.Model.get_stair_props(obj) 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 props.is_editing = False
regenerate_stair_mesh(obj) regenerate_stair_mesh(obj)
tool.Model.add_body_representation(obj) tool.Model.add_body_representation(obj)
+26 -1
View File
@@ -302,13 +302,38 @@ class BIM_PT_stair(bpy.types.Panel):
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="") row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
row = self.layout.row(align=True) row = self.layout.row(align=True)
for prop_name in props.get_props_kwargs(): 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) 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 prop_readable_name = props.bl_rna.properties[prop_name].name
self.layout.label(text=f"{prop_readable_name}:") self.layout.label(text=f"{prop_readable_name}:")
self.layout.prop(props, prop_name, text="") self.layout.prop(props, prop_name, text="")
else: else:
self.layout.prop(props, prop_name) self.layout.prop(props, prop_name)
if prop_name == "height": # Weak but we just want to insert this inside props drawing if prop_name == "height": # Weak but we just want to insert this inside props drawing
row_length = self.layout.row(align=True) row_length = self.layout.row(align=True)
row_length.prop(props, "total_length_target") row_length.prop(props, "total_length_target")
@@ -2522,6 +2522,7 @@ class FlipClippingPlane(bpy.types.Operator):
obj = context.active_object obj = context.active_object
if obj in tool.Project.get_project_props().clipping_planes_objs: if obj in tool.Project.get_project_props().clipping_planes_objs:
obj.rotation_euler[0] += radians(180) obj.rotation_euler[0] += radians(180)
obj.rotation_euler[0] %= radians(360)
context.view_layer.update() context.view_layer.update()
return {"FINISHED"} return {"FINISHED"}
+3 -3
View File
@@ -450,9 +450,9 @@ class BIMProjectProperties(PropertyGroup):
self.mvd = header_data.mvd self.mvd = header_data.mvd
self.author_name = header_data.author_name self.author_name = header_data.author_name
self.author_email = header_data.author_email self.author_email = header_data.author_email
self.organisation_name = header_data.organization_name self.organisation_name = header_data.organisation_name
self.organisation_email = header_data.organization_email self.organisation_email = header_data.organisation_email
self.authorisation = header_data.authorization self.authorisation = header_data.authorisation
if TYPE_CHECKING: if TYPE_CHECKING:
is_editing: bool is_editing: bool
+2 -1
View File
@@ -653,4 +653,5 @@ class BIM_PT_purge(Panel):
row = layout.row(align=True) row = layout.row(align=True)
row.label(text=f"{object_type.replace('_', ' ').capitalize()}:") row.label(text=f"{object_type.replace('_', ' ').capitalize()}:")
row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = object_type 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
+10 -16
View File
@@ -242,14 +242,11 @@ class BIM_PT_object_psets(Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
if not (obj := context.active_object): return (
return False (obj := tool.Blender.get_active_object())
ifc_id = tool.Blender.get_ifc_definition_id(obj) and (element := tool.Ifc.get_entity(obj))
if not ifc_id: and element.is_a("IfcObjectDefinition")
return False )
if not tool.Ifc.get_object_by_identifier(ifc_id):
return False
return True
def draw(self, context): def draw(self, context):
if not ObjectPsetsData.is_loaded: if not ObjectPsetsData.is_loaded:
@@ -388,14 +385,11 @@ class BIM_PT_object_qtos(Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
if not (obj := context.active_object): return (
return False (obj := tool.Blender.get_active_object())
ifc_id = tool.Blender.get_ifc_definition_id(obj) and (element := tool.Ifc.get_entity(obj))
if not ifc_id: and element.is_a("IfcObjectDefinition")
return False )
if not tool.Ifc.get_object_by_identifier(ifc_id):
return False
return True
def draw(self, context): def draw(self, context):
if not ObjectQtosData.is_loaded: if not ObjectQtosData.is_loaded:
@@ -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.", 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( element = core.assign_class(
tool.Ifc, tool.Ifc,
tool.Collector, tool.Collector,
@@ -326,13 +327,9 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
predefined_type=predefined_type, predefined_type=predefined_type,
should_add_representation=False, should_add_representation=False,
) )
representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context)
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation) ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation( bonsai.core.geometry.switch_representation(
tool.Ifc, tool.Ifc, tool.Geometry, obj=obj, representation=representation
tool.Geometry,
obj=obj,
representation=representation,
) )
else: else:
+1 -1
View File
@@ -101,7 +101,7 @@ class ColourByPropertyData:
elif pset.endswith("BaseQuantities"): elif pset.endswith("BaseQuantities"):
keys.extend([f'/.*BaseQuantities/."{name}"' for name in properties.keys() if name != "id"]) keys.extend([f'/.*BaseQuantities/."{name}"' for name in properties.keys() if name != "id"])
else: 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] results = [(k, k, "") for k in keys]
return default + results return default + results
@@ -105,6 +105,7 @@ classes = (
operator.RemoveWorkTime, operator.RemoveWorkTime,
operator.ReorderTask, operator.ReorderTask,
operator.SaveAnimationColorScheme, operator.SaveAnimationColorScheme,
operator.SelectTaskElements,
operator.SelectTaskRelatedInputs, operator.SelectTaskRelatedInputs,
operator.SelectTaskRelatedProducts, operator.SelectTaskRelatedProducts,
operator.SelectUnassignedWorkScheduleProducts, operator.SelectUnassignedWorkScheduleProducts,
@@ -243,6 +243,9 @@ class SequenceData:
data["NestingIndex"] = None data["NestingIndex"] = None
for rel in task.Nests or []: for rel in task.Nests or []:
data["NestingIndex"] = rel.RelatedObjects.index(task) 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 cls.data["tasks"][task.id()] = data
@classmethod @classmethod
@@ -1281,6 +1281,17 @@ class DisableEditingSequence(bpy.types.Operator):
return {"FINISHED"} 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): class SelectTaskRelatedProducts(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.select_task_related_products" bl_idname = "bim.select_task_related_products"
bl_label = "Select All Output Products" bl_label = "Select All Output Products"
+15 -3
View File
@@ -75,6 +75,8 @@ def update_active_task_index(self, context):
bonsai.bim.module.pset.data.refresh() bonsai.bim.module.pset.data.refresh()
if self.editing_task_type == "SEQUENCE": if self.editing_task_type == "SEQUENCE":
tool.Sequence.load_task_properties() tool.Sequence.load_task_properties()
if self.should_auto_select:
tool.Sequence.select_active_task_elements(task)
def update_active_task_outputs(self, context): def update_active_task_outputs(self, context):
@@ -485,6 +487,9 @@ class BIMWorkScheduleProperties(PropertyGroup):
other_columns: EnumProperty( other_columns: EnumProperty(
items=[ items=[
("Controls.Calendar", "Calendar", ""), ("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", name="Special Columns",
) )
@@ -518,9 +523,13 @@ class BIMWorkScheduleProperties(PropertyGroup):
active_task_input_index: IntProperty(name="Active Task Input Index") active_task_input_index: IntProperty(name="Active Task Input Index")
task_outputs: CollectionProperty(name="Task Outputs", type=TaskProduct) task_outputs: CollectionProperty(name="Task Outputs", type=TaskProduct)
active_task_output_index: IntProperty(name="Active Task Output Index") 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_outputs: BoolProperty(
show_nested_resources: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_resources) name="Show Nested Task Elements", default=False, update=update_active_task_outputs
show_nested_inputs: BoolProperty(name="Show Nested Tasks", default=False, update=update_active_task_inputs) )
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_input_tasks: CollectionProperty(name="Product Task Inputs", type=TaskProduct)
product_output_tasks: CollectionProperty(name="Product Task Outputs", type=TaskProduct) product_output_tasks: CollectionProperty(name="Product Task Outputs", type=TaskProduct)
active_product_output_task_index: IntProperty(name="Active Product Output Task Index") active_product_output_task_index: IntProperty(name="Active Product Output Task Index")
@@ -531,6 +540,9 @@ class BIMWorkScheduleProperties(PropertyGroup):
filter_by_active_schedule: BoolProperty( filter_by_active_schedule: BoolProperty(
name="Filter By Active Schedule", default=False, update=update_filter_by_active_schedule 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: if TYPE_CHECKING:
work_schedule_predefined_types: str work_schedule_predefined_types: str
+36 -14
View File
@@ -290,13 +290,21 @@ class BIM_PT_work_schedules(Panel):
row2 = self.layout.row(align=True) row2 = self.layout.row(align=True)
row2.alignment = "RIGHT" row2.alignment = "RIGHT"
row2.prop(self.props, "enable_reorder", text="", icon="SORTALPHA") col = row2.column()
row2.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING") row_ = col.row(align=True)
row2.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id op = row_.operator("bim.select_task_elements", icon="RESTRICT_SELECT_OFF", text="Select Elements")
row2.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = ( 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 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 ifc_definition_id
) )
row.operator("bim.add_task", text="Add", icon="ADD").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 input_id = self.props.task_inputs[self.props.active_task_input_index].ifc_definition_id
op.related_object = input_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 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 = col.row()
row2.template_list("BIM_UL_task_inputs", "", self.props, "task_inputs", self.props, "active_task_input_index") 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.related_object_type = "RESOURCE"
op.resource = self.props.task_resources[self.props.active_task_resource_index].ifc_definition_id 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", icon="OUTLINER", text="")
row2.prop(self.props, "show_nested_resources", text="Show Nested")
row2 = col.row() row2 = col.row()
row2.template_list( 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 output_id = self.props.task_outputs[self.props.active_task_output_index].ifc_definition_id
op.relating_product = output_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 op.task = task.ifc_definition_id
row2 = col.row() row2.prop(self.props, "show_nested_outputs", icon="OUTLINER", text="")
row2.prop(self.props, "show_nested_outputs", text="Show Nested")
row2 = col.row() row2 = col.row()
row2.template_list( row2.template_list(
"BIM_UL_task_outputs", "", self.props, "task_outputs", self.props, "active_task_output_index" "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 = split1.split(factor=0.9 - min(0.5, 0.15 * len(self.props.columns)))
split2.prop(item, "name", emboss=False, text="") 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": if self.props.active_task_id and self.props.editing_task_type == "ATTRIBUTES":
row.prop( row.prop(
@@ -960,6 +966,7 @@ class BIM_UL_tasks(UIList):
row: bpy.types.UILayout, row: bpy.types.UILayout,
item: Optional[bpy.types.PropertyGroup] = None, item: Optional[bpy.types.PropertyGroup] = None,
task: Optional[dict[str, Any]] = None, task: Optional[dict[str, Any]] = None,
ifc_definition_id: int = 0,
*, *,
header: bool = False, header: bool = False,
) -> None: ) -> None:
@@ -999,6 +1006,21 @@ class BIM_UL_tasks(UIList):
row.label(text=item.derived_calendar + "*") row.label(text=item.derived_calendar + "*")
else: else:
row.label(text=item.calendar or "-") 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: else:
ifc_class, name = column.name.split(".") ifc_class, name = column.name.split(".")
if header: if header:
@@ -70,7 +70,7 @@ class GridDecorator:
grid_props = tool.Spatial.get_grid_props() grid_props = tool.Spatial.get_grid_props()
for axis in grid_props.grid_axes: 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 continue
if obj.select_get() and context.mode != "OBJECT": if obj.select_get() and context.mode != "OBJECT":
continue continue
@@ -125,21 +125,22 @@ class GridDecorator:
unselected_edges = [] unselected_edges = []
grid_props = tool.Spatial.get_grid_props() grid_props = tool.Spatial.get_grid_props()
for axis in grid_props.grid_axes: for axis in grid_props.grid_axes:
if (obj := axis.obj) and obj.hide_get() == False: if not (obj := axis.obj) or obj.visible_get() is False:
if obj.select_get(): continue
if context.mode != "OBJECT": if obj.select_get():
continue if context.mode != "OBJECT":
edges = selected_edges continue
verts = selected_verts edges = selected_edges
else: verts = selected_verts
edges = unselected_edges else:
verts = unselected_verts edges = unselected_edges
i = len(verts) verts = unselected_verts
edges.append([i, i + 1]) i = len(verts)
matrix_world = obj.matrix_world edges.append([i, i + 1])
v1 = matrix_world @ obj.data.vertices[0].co matrix_world = obj.matrix_world
v2 = matrix_world @ obj.data.vertices[1].co v1 = matrix_world @ obj.data.vertices[0].co
verts.extend([v1, v2]) v2 = matrix_world @ obj.data.vertices[1].co
verts.extend([v1, v2])
if unselected_verts: if unselected_verts:
self.draw_batch("LINES", unselected_verts, decorator_color_background, unselected_edges) self.draw_batch("LINES", unselected_verts, decorator_color_background, unselected_edges)
+5 -8
View File
@@ -34,14 +34,11 @@ class BIM_PT_type(Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
if not context.active_object: return (
return False (obj := tool.Blender.get_active_object())
element = tool.Ifc.get_entity(context.active_object) and (element := tool.Ifc.get_entity(obj))
if not element: and (element.is_a("IfcObject") or element.is_a("IfcTypeObject"))
return False )
if not element.is_a("IfcProduct") and not element.is_a("IfcTypeProduct"):
return True
return True
def draw(self, context): def draw(self, context):
if not TypeData.is_loaded: if not TypeData.is_loaded:
+26 -35
View File
@@ -108,10 +108,6 @@ def add_sheet(ifc: type[tool.Ifc], drawing: type[tool.Drawing], titleblock: ifco
def regenerate_sheet( def regenerate_sheet(
drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance
) -> Union[list[tool.Drawing.SheetWarningType], None]: ) -> Union[list[tool.Drawing.SheetWarningType], None]:
warnings = drawing.validate_sheet_files(sheet)
if warnings:
return warnings
titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK") titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK")
assert titleblock_uri assert titleblock_uri
@@ -320,6 +316,7 @@ def duplicate_drawing(
) -> ifcopenshell.entity_instance: ) -> ifcopenshell.entity_instance:
drawing_name = drawing_tool.ensure_unique_drawing_name(drawing_tool.get_name(drawing)) drawing_name = drawing_tool.ensure_unique_drawing_name(drawing_tool.get_name(drawing))
new_drawing = ifc.run("root.copy_class", product=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.copy_representation(drawing, new_drawing)
drawing_tool.set_name(new_drawing, drawing_name) drawing_tool.set_name(new_drawing, drawing_name)
group = drawing_tool.get_drawing_group(new_drawing) group = drawing_tool.get_drawing_group(new_drawing)
@@ -475,46 +472,40 @@ def sync_references(
if not drawing_tool.has_annotation(drawing): if not drawing_tool.has_annotation(drawing):
return return
context = drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing)) if not (context := drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))):
if not context:
return return
group = drawing_tool.get_drawing_group(drawing) 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) 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 for reference_element in potential_reference_elements:
should_create_annotation = False if not drawing_tool.get_drawing_reference_annotation(drawing, reference_element):
if annotation := drawing_tool.generate_reference_annotation(drawing, reference_element, context):
# 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:
ifc.run("drawing.assign_product", relating_product=reference_element, related_object=annotation) ifc.run("drawing.assign_product", relating_product=reference_element, related_object=annotation)
ifc.run("group.assign_group", group=group, products=[annotation]) ifc.run("group.assign_group", group=group, products=[annotation])
collector.assign(ifc.get_object(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: def select_assigned_product(drawing: type[tool.Drawing], context: bpy.types.Context) -> None:
drawing.select_assigned_product(context) drawing.select_assigned_product(context)
+23 -12
View File
@@ -1506,28 +1506,21 @@ class Blender(bonsai.core.tool.Blender):
return bpy.context.preferences.addons[blender_package_name].preferences return bpy.context.preferences.addons[blender_package_name].preferences
@classmethod @classmethod
def get_sun_position_addon(cls) -> Union[types.ModuleType, None]: def get_addon(cls, name: str) -> Union[types.ModuleType, None]:
# Check if it's installed as legacy Blender addon.
import importlib import importlib
try: try:
sun_position = importlib.import_module("sun_position") return importlib.import_module(name) # Legacy Blender addon
except ImportError: except ImportError:
sun_position = None pass
if sun_position:
return sun_position
for package_name in bpy.context.preferences.addons.keys(): for package_name in bpy.context.preferences.addons.keys():
if package_name.endswith(".sun_position"): if package_name.endswith(f".{name}"):
try: try:
sun_position = importlib.import_module(package_name) return importlib.import_module(package_name)
return sun_position
except ModuleNotFoundError: except ModuleNotFoundError:
pass pass
return sun_position
@classmethod @classmethod
def get_sun_props(cls) -> Union[SunPosProperties, None]: def get_sun_props(cls) -> Union[SunPosProperties, None]:
assert (scene := bpy.context.scene) assert (scene := bpy.context.scene)
@@ -1889,6 +1882,24 @@ class Blender(bonsai.core.tool.Blender):
obj.select_set(True) obj.select_set(True)
bpy.context.view_layer.objects.active = previously_active 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 @classmethod
def hide_objects(cls, objs): def hide_objects(cls, objs):
previously_selected = {o.name for o in bpy.context.selected_objects} previously_selected = {o.name for o in bpy.context.selected_objects}
+10 -3
View File
@@ -51,9 +51,13 @@ class Collector(bonsai.core.tool.Collector):
if tool.Geometry.is_locked(element): if tool.Geometry.is_locked(element):
tool.Geometry.lock_object(obj) tool.Geometry.lock_object(obj)
element = (element.PartOfU or element.PartOfV or element.PartOfW)[0] 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"): elif element.is_a("IfcGrid"):
if tool.Geometry.is_locked(element): if tool.Geometry.is_locked(element):
tool.Geometry.lock_object(obj) tool.Geometry.lock_object(obj)
if not tool.Spatial.get_grid_props().is_visible:
obj.hide_viewport = True
if element.is_a("IfcProject"): if element.is_a("IfcProject"):
if tool.Geometry.is_locked(element): if tool.Geometry.is_locked(element):
@@ -69,7 +73,8 @@ class Collector(bonsai.core.tool.Collector):
tool.Geometry.lock_object(obj) tool.Geometry.lock_object(obj)
collection = cls._create_project_child_collection("IfcSpace") collection = cls._create_project_child_collection("IfcSpace")
cls.link_collection_object_safe(collection, obj) 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"): elif element.is_a("IfcStructuralItem"):
collection = cls._create_project_child_collection("IfcStructuralItem") collection = cls._create_project_child_collection("IfcStructuralItem")
cls.link_collection_object_safe(collection, obj) cls.link_collection_object_safe(collection, obj)
@@ -93,7 +98,8 @@ class Collector(bonsai.core.tool.Collector):
cls.link_collection_object_safe(collection, obj) cls.link_collection_object_safe(collection, obj)
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) 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) 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 ( elif (
tool.Ifc.get_schema() != "IFC2X3" tool.Ifc.get_schema() != "IFC2X3"
and element.is_a("IfcSpatialElement") and element.is_a("IfcSpatialElement")
@@ -105,7 +111,8 @@ class Collector(bonsai.core.tool.Collector):
cls.link_collection_object_safe(collection, obj) cls.link_collection_object_safe(collection, obj)
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) 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) 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": elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
if collection := cls._create_own_collection(obj): if collection := cls._create_own_collection(obj):
cls.link_collection_object_safe(collection, obj) cls.link_collection_object_safe(collection, obj)
+6 -8
View File
@@ -273,18 +273,16 @@ class Cost(bonsai.core.tool.Cost):
return return
props = cls.get_cost_props() props = cls.get_cost_props()
props.cost_item_type_products.clear() props.cost_item_type_products.clear()
# TODO implement process and resource types props.cost_item_processes.clear()
# props.cost_item_processes.clear() props.cost_item_resources.clear()
# props.cost_item_resources.clear()
for rel in cost_item.Controls or []: for rel in cost_item.Controls or []:
for related_object in rel.RelatedObjects: for related_object in rel.RelatedObjects:
if related_object.is_a("IfcTypeProduct"): if related_object.is_a("IfcTypeProduct"):
new = props.cost_item_type_products.add() new = props.cost_item_type_products.add()
# TODO implement process and resource types elif related_object.is_a("IfcProcess"):
# elif related_object.is_a("IfcProcess"): new = props.cost_item_processes.add()
# new = props.cost_item_processes.add() elif related_object.is_a("IfcResource"):
# elif related_object.is_a("IfcResource"): new = props.cost_item_resources.add()
# new = props.cost_item_resources.add()
new.ifc_definition_id = related_object.id() new.ifc_definition_id = related_object.id()
new.name = related_object.Name or "Unnamed" new.name = related_object.Name or "Unnamed"
+104 -17
View File
@@ -18,6 +18,7 @@
from __future__ import annotations from __future__ import annotations
import os import os
import re
import json import json
import bmesh import bmesh
import bpy import bpy
@@ -33,7 +34,7 @@ import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore from bonsai.bim.ifc import IfcStore
from mathutils import Vector from mathutils import Vector
from collections import defaultdict 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 from collections.abc import Iterable
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -136,12 +137,30 @@ class Debug(bonsai.core.tool.Debug):
"PERSON", "PERSON",
"PERSON_AND_ORGANIZATION", "PERSON_AND_ORGANIZATION",
], ],
by_name_or_identification_only: bool = False,
) -> dict[str, list[str]]: ) -> dict[str, list[str]]:
"""Merge identical objects. """Merge identical objects.
Note that Styles UI (or other UI) should be updated manually after using this method. 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: def get_hash(element: ifcopenshell.entity_instance) -> int:
data = element.get_info_2(include_identifier=False, recursive=True) data = element.get_info_2(include_identifier=False, recursive=True)
if object_type == "APPLICATION": if object_type == "APPLICATION":
@@ -152,6 +171,49 @@ class Debug(bonsai.core.tool.Debug):
data["TheOrganization"] = element.TheOrganization.id() data["TheOrganization"] = element.TheOrganization.id()
return hash(json.dumps(data, sort_keys=True)) 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() ifc_file = tool.Ifc.get()
merged_element_types: dict[str, list[str]] = {} merged_element_types: dict[str, list[str]] = {}
@@ -179,22 +241,47 @@ class Debug(bonsai.core.tool.Debug):
for element_type in element_types: for element_type in element_types:
elements = ifc_file.by_type(element_type, include_subtypes=False) elements = ifc_file.by_type(element_type, include_subtypes=False)
# Calculate hashes. # Calculate hashes or name keys.
hash_to_elements: defaultdict[int, list[ifcopenshell.entity_instance]] = defaultdict(list) hash_to_elements: defaultdict[Union[int, str], list[ifcopenshell.entity_instance]]
for element in elements:
# Except for styles, ignore unnamed elements as they may be not safe to merge if by_name_or_identification_only:
merge_optional_names = ("STYLE", "PERSON") # Group by name/identifier only (with .XXX suffix normalization)
not_optional_name = ("APPLICATION", "ORGANIZATION") hash_to_elements = defaultdict(list)
has_no_name = ("PERSON_AND_ORGANIZATION",) for element in elements:
if ( name_key = get_name_key(element)
object_type not in merge_optional_names # Skip elements without a valid identifier
and object_type not in not_optional_name if not name_key:
and object_type not in has_no_name continue
and not element.Name hash_to_elements[name_key].append(element)
):
continue # Sort elements within each group to keep the one without suffix (or lowest suffix)
element_hash = get_hash(element) for name_key in hash_to_elements:
hash_to_elements[element_hash].append(element) # 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] = [] merged_elements_names: list[str] = []
# Merge elements. # Merge elements.
+498 -280
View File
@@ -208,6 +208,27 @@ class Drawing(bonsai.core.tool.Drawing):
return obj 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 @classmethod
def ensure_annotation_in_drawing_plane( def ensure_annotation_in_drawing_plane(
cls, obj: bpy.types.Object, camera: Optional[bpy.types.Object] = None 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) entity = tool.Ifc.get_entity(obj)
if not entity: if not entity:
return return
for rel in entity.HasAssignments: return tool.Ifc.get_object(cls.get_annotation_drawing(entity))
if rel.is_a("IfcRelAssignsToGroup"):
for e in rel.RelatedObjects:
if e.ObjectType == "DRAWING":
return tool.Ifc.get_object(e)
if not camera: if not camera:
camera = get_camera_from_annotation_object(obj) or bpy.context.scene.camera camera = get_camera_from_annotation_object(obj) or bpy.context.scene.camera
@@ -236,7 +253,11 @@ class Drawing(bonsai.core.tool.Drawing):
@classmethod @classmethod
def setup_annotation_object( 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: ) -> None:
"""Finish object's adjustments after both object and entity are created""" """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 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 @classmethod
def is_annotation_object_type( def is_annotation_object_type(
cls, element: ifcopenshell.entity_instance, object_types: Union[str, Sequence[str]] 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]: def get_assigned_product(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
for rel in element.HasAssignments: for rel in element.HasAssignments:
if rel.is_a("IfcRelAssignsToProduct"): 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 @classmethod
def import_annotations_in_group(cls, group: ifcopenshell.entity_instance) -> None: 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 cls, drawing: ifcopenshell.entity_instance
) -> list[ifcopenshell.entity_instance]: ) -> list[ifcopenshell.entity_instance]:
elements = [] 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"): for element in tool.Ifc.get().by_type("IfcAnnotation"):
if element in existing_references or element == drawing: if element in existing_references or element == drawing:
continue continue
@@ -1411,14 +1490,25 @@ class Drawing(bonsai.core.tool.Drawing):
"GlobalReferencing", False "GlobalReferencing", False
): ):
elements.append(element) elements.append(element)
for element in tool.Ifc.get().by_type("IfcGridAxis"): for element in tool.Ifc.get().by_type("IfcGrid"):
elements.append(element) 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) target_view = tool.Drawing.get_drawing_target_view(drawing)
if target_view in ("SECTION_VIEW", "ELEVATION_VIEW"): if target_view in ("SECTION_VIEW", "ELEVATION_VIEW"):
for element in tool.Ifc.get().by_type("IfcBuildingStorey"): for element in tool.Ifc.get().by_type("IfcBuildingStorey"):
if element in existing_references:
continue
elements.append(element) elements.append(element)
return elements 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 @classmethod
def get_drawing_reference_annotation( def get_drawing_reference_annotation(
cls, drawing: ifcopenshell.entity_instance, reference_element: ifcopenshell.entity_instance cls, drawing: ifcopenshell.entity_instance, reference_element: ifcopenshell.entity_instance
@@ -1430,7 +1520,7 @@ class Drawing(bonsai.core.tool.Drawing):
# IfcRelAssignsToProduct.RelatingProduct = IfcGrid # IfcRelAssignsToProduct.RelatingProduct = IfcGrid
# IfcRelAssignsToProduct.Name = IfcGridAxis.AxisTag # IfcRelAssignsToProduct.Name = IfcGridAxis.AxisTag
grid = None grid = None
for attribute in ("PartOfW", "PartOfV", "PartOfU"): for attribute in ("PartOfU", "PartOfV", "PartOfW"):
if getattr(reference_element, attribute, None): if getattr(reference_element, attribute, None):
grid = getattr(reference_element, attribute)[0] grid = getattr(reference_element, attribute)[0]
break break
@@ -1452,6 +1542,26 @@ class Drawing(bonsai.core.tool.Drawing):
if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct == reference_element: if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct == reference_element:
return 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 @classmethod
def generate_reference_annotation( def generate_reference_annotation(
cls, cls,
@@ -1461,333 +1571,410 @@ class Drawing(bonsai.core.tool.Drawing):
) -> ifcopenshell.entity_instance: ) -> ifcopenshell.entity_instance:
if reference_element.is_a("IfcGridAxis"): if reference_element.is_a("IfcGridAxis"):
return cls.generate_grid_axis_reference_annotation(drawing, reference_element, context) return cls.generate_grid_axis_reference_annotation(drawing, reference_element, context)
elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING": 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") target_view = ifcopenshell.util.element.get_pset(reference_element, "EPset_Drawing", "TargetView")
if target_view == "ELEVATION_VIEW": if target_view == "ELEVATION_VIEW":
ensure_referenced_drawing_obj_exists(reference_element)
return cls.generate_elevation_reference_annotation(drawing, reference_element, context) return cls.generate_elevation_reference_annotation(drawing, reference_element, context)
elif target_view == "SECTION_VIEW": elif target_view == "SECTION_VIEW":
ensure_referenced_drawing_obj_exists(reference_element)
return cls.generate_section_reference_annotation(drawing, reference_element, context) return cls.generate_section_reference_annotation(drawing, reference_element, context)
elif reference_element.is_a("IfcBuildingStorey"): elif reference_element.is_a("IfcBuildingStorey"):
return cls.generate_storey_annotation(drawing, reference_element, context) 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 @classmethod
def generate_storey_annotation( def generate_storey_annotation(
cls, cls,
drawing: ifcopenshell.entity_instance, drawing: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance, storey: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance,
) -> 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 import bonsai.bim.module.drawing.helper as helper
camera = tool.Ifc.get_object(drawing) camera = tool.Ifc.get_object(drawing)
assert isinstance(camera, bpy.types.Object) if camera.data.type != "ORTHO":
assert isinstance((camera_data := camera.data), bpy.types.Camera) return
props = tool.Drawing.get_camera_props(camera_data) 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 # Get cutting plane as a line
reference_obj = tool.Ifc.get_object(reference_element) verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
assert isinstance(reference_obj, bpy.types.Object) 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: bounds = helper.ortho_view_frame(camera.data)
mat = reference_obj.matrix_world.copy() if not (points := helper.clip_segment(bounds, [v1, v2])):
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation return
xyz[2] = 0 for v in points:
xyz = camera.matrix_world @ xyz v.z = 0
mat.translation = xyz return points
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
@classmethod @classmethod
def generate_section_reference_annotation( def generate_section_reference_annotation(
cls, cls,
drawing: ifcopenshell.entity_instance, drawing: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance, section: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance,
) -> 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) 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: @classmethod
mat = reference_obj.matrix_world.copy() def regenerate_section_reference_annotation(
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation cls,
xyz[2] = 0 drawing: ifcopenshell.entity_instance,
xyz = camera.matrix_world @ xyz annotation: ifcopenshell.entity_instance,
mat.translation = xyz section: ifcopenshell.entity_instance,
annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05)) context: ifcopenshell.entity_instance,
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset ) -> ifcopenshell.entity_instance:
mat.translation += annotation_offset if not (points := cls.generate_section_reference_points(drawing, section)):
return mat return
def clip_to_camera_boundary( camera = tool.Ifc.get_object(drawing)
mesh: bpy.types.Mesh, bounds: tuple[float, float, float, float, float, float] settings = ifcopenshell.geom.settings()
) -> Union[bpy.types.Mesh, None]: settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
mesh.verts.ensure_lookup_table() shape = ifcopenshell.geom.create_shape(settings, annotation)
points = [v.co for v in mesh.verts[0:2]] m = ifcopenshell.util.shape.get_shape_matrix(shape)
points = helper.clip_segment(bounds, points) mw = cls.get_default_annotation_matrix(camera)
if points is None: existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
return None
mesh.verts[0].co = points[0]
mesh.verts[1].co = points[1]
return mesh
if cls.is_perpendicular(camera, reference_obj) and cls.is_intersecting(camera, reference_obj): new_points = None
reference_mesh = cls.get_camera_block(reference_obj) if not np.allclose(m, np.array(mw), atol=1e-4):
obj_matrix = to_camera_coords(camera, reference_obj) 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 if new_points:
# this into a section line we: if representation := ifcopenshell.util.representation.get_representation(annotation, context):
# 1. Select the 4 +Z vertices local to the reference element. This ifcopenshell.api.geometry.unassign_representation(
# is the cutting plane. tool.Ifc.get(), product=annotation, representation=representation
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:] builder = ShapeBuilder(tool.Ifc.get())
global_cutting_plane_verts = [reference_obj.matrix_world @ v for v in cutting_plane_verts] unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
# 2. Project the cutting plane onto our viewing camera. new_points = [p / unit_scale for p in new_points]
verts_local_to_camera = [camera.matrix_world.inverted() @ v for v in global_cutting_plane_verts] representation = builder.get_representation(context, [builder.polyline(new_points)])
# 3. Collapse verts with the same XY coords, and set Z to be just ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation)
# below the clip_start so it's visible if obj := tool.Ifc.get_object(annotation):
collapsed_verts = [] obj.matrix_world = mw
for vert in verts_local_to_camera: bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
if not [True for v in collapsed_verts if (vert.xy - v.xy).length < 1e-2]: bonsai.core.geometry.switch_representation(
collapsed_verts.append(mathutils.Vector((vert.x, vert.y, -camera.data.clip_start - 0.05))) tool.Ifc, tool.Geometry, obj=obj, representation=representation
# 4. The first two vertices is the section line )
section_line = collapsed_verts[0:2] else:
# 5. Sort the vertices in the +X direction so that the vertices are ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw))
# ordered to "point" in the direction of the section cut. annotation.Name = section.Name or "Unnamed"
section_line = sorted( return annotation
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
@classmethod @classmethod
def generate_elevation_reference_annotation( def generate_elevation_reference_annotation(
cls, cls,
drawing: ifcopenshell.entity_instance, drawing: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance, elevation: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance,
) -> ifcopenshell.entity_instance: ) -> ifcopenshell.entity_instance:
reference_obj = tool.Ifc.get_object(reference_element)
reference_obj.matrix_world
camera = tool.Ifc.get_object(drawing) camera = tool.Ifc.get_object(drawing)
if camera.data.type != "ORTHO":
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix: return
mat = reference_obj.matrix_world.copy() settings = ifcopenshell.geom.settings()
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation shape = ifcopenshell.geom.create_shape(settings, elevation)
xyz[2] = 0 m = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
xyz = camera.matrix_world @ xyz if cls.is_matrix_perpendicular(camera.matrix_world, m) and cls.does_shape_intersect_camera(shape, camera):
mat.translation = xyz obj = bpy.data.objects.new(elevation.Name or "Unnamed", None)
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)
obj.empty_display_size = 0.1 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( element = cls.run_root_assign_class(
obj=obj, obj=obj, ifc_class="IfcAnnotation", predefined_type="ELEVATION", should_add_representation=False
ifc_class="IfcAnnotation",
predefined_type="ELEVATION",
should_add_representation=False,
context=context,
ifc_representation_class=None,
) )
if representation := ifcopenshell.util.representation.get_representation(element, context): element.Name = elevation.Name or "Unnamed"
cls.reload_representation(obj=obj, representation=representation)
return element 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 @classmethod
def generate_grid_axis_reference_annotation( def generate_grid_axis_reference_annotation(
cls, cls,
drawing: ifcopenshell.entity_instance, drawing: ifcopenshell.entity_instance,
reference_element: ifcopenshell.entity_instance, axis: ifcopenshell.entity_instance,
context: ifcopenshell.entity_instance, context: ifcopenshell.entity_instance,
) -> Union[ifcopenshell.entity_instance, None]: ) -> Union[ifcopenshell.entity_instance, None]:
import bonsai.bim.module.drawing.helper as helper if not (points := cls.generate_grid_axis_reference_points(drawing, axis)):
return
target_view = tool.Drawing.get_drawing_target_view(drawing)
camera = tool.Ifc.get_object(drawing) camera = tool.Ifc.get_object(drawing)
assert isinstance(camera, bpy.types.Object) mesh = bpy.data.meshes.new("Mesh")
assert isinstance(camera_data := camera.data, bpy.types.Camera) obj = bpy.data.objects.new(axis.AxisTag or "-", mesh)
obj.matrix_world = cls.get_default_annotation_matrix(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)
element = cls.run_root_assign_class( element = cls.run_root_assign_class(
obj=obj, obj=obj, ifc_class="IfcAnnotation", predefined_type="GRID", should_add_representation=False
ifc_class="IfcAnnotation",
predefined_type="GRID",
should_add_representation=True,
context=context,
ifc_representation_class=None,
) )
if representation := ifcopenshell.util.representation.get_representation(element, context): element.Name = axis.AxisTag or "-"
cls.reload_representation(obj=obj, representation=representation) 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 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 @classmethod
def is_perpendicular(cls, a: bpy.types.Object, b: bpy.types.Object) -> bool: 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))] 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 True
return False 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 @classmethod
def get_camera_block(cls, obj: bpy.types.Object) -> dict: def get_camera_block(cls, obj: bpy.types.Object) -> dict:
assert isinstance(camera := obj.data, bpy.types.Camera) 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"]) b_tree = mathutils.bvhtree.BVHTree.FromPolygons(b_block["verts"], b_block["faces"])
return bool(a_tree.overlap(b_tree)) 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 @classmethod
def replace_text_literal_variables( def replace_text_literal_variables(
cls, cls,
@@ -2568,3 +2775,14 @@ class Drawing(bonsai.core.tool.Drawing):
ifcopenshell.api.document.remove_reference(tool.Ifc.get(), reference=reference) ifcopenshell.api.document.remove_reference(tool.Ifc.get(), reference=reference)
tool.Drawing.import_sheets() 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)
+5 -5
View File
@@ -233,20 +233,20 @@ class Geometry(bonsai.core.tool.Geometry):
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if not element: if not element:
return return
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING": elif element.is_a("IfcAnnotation"):
return bonsai.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element) 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"): elif element.is_a("IfcRelSpaceBoundary"):
ifcopenshell.api.boundary.remove_boundary(ifc_file, boundary=element) ifcopenshell.api.boundary.remove_boundary(ifc_file, boundary=element)
tool.Boundary.undecorate_boundary(obj) tool.Boundary.undecorate_boundary(obj)
return bpy.data.objects.remove(obj) return bpy.data.objects.remove(obj)
elif element.is_a("IfcGridAxis"): elif element.is_a("IfcGridAxis"):
is_last_axis = False
# Deleting the last W axis is OK # Deleting the last W axis is OK
if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or ( if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
(grid := element.PartOfV) and len(grid[0].VAxes) == 1 (grid := element.PartOfV) and len(grid[0].VAxes) == 1
): ):
is_last_axis = True
if is_last_axis:
return return
ifcopenshell.api.grid.remove_grid_axis(ifc_file, axis=element) ifcopenshell.api.grid.remove_grid_axis(ifc_file, axis=element)
return bpy.data.objects.remove(obj) return bpy.data.objects.remove(obj)
+59 -37
View File
@@ -1399,7 +1399,6 @@ class Model(bonsai.core.tool.Model):
number_of_risers = number_of_treads + 1 number_of_risers = number_of_treads + 1
tread_rise = height / number_of_risers 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_overlap = max(nosing_length, 0)
nosing_tread_gap = -min(nosing_length, 0) nosing_tread_gap = -min(nosing_length, 0)
nosing_overlap_offset = -V_(nosing_overlap, 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]) default_tread_offset = Vector([tread_run, tread_rise])
def get_tread_data(i): def get_tread_data(i):
if custom_tread_run: # Check if this is first or last tread with custom run
current_tread_run = None current_tread_run = None
if i == 0: if i == 0 and custom_first_last_tread_run[0] is not None:
current_tread_run = custom_first_last_tread_run[0] current_tread_run = custom_first_last_tread_run[0]
elif i == number_of_risers - 1: 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] 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 return default_tread_offset, default_tread_verts
# treads # treads
@@ -1450,9 +1456,13 @@ class Model(bonsai.core.tool.Model):
for i in range(number_of_risers): for i in range(number_of_risers):
last_vert_i = len(vertices) - 1 last_vert_i = len(vertices) - 1
tread_offset, tread_verts = get_tread_data(i) 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) # Skip adding vertices/edges for zero-width treads
vertices.extend(current_tread_verts) 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 current_offset += tread_offset
if stair_type == "WOOD/STEEL": 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) default_tread_offset = V_(tread_run + nosing_tread_gap, tread_rise)
def get_tread_data(i): def get_tread_data(i):
if custom_tread_run: # Check if this is first or last tread with custom run
current_tread_run = None current_tread_run = None
if i == 0 and custom_first_last_tread_run[0] != 0: if i == 0 and custom_first_last_tread_run[0] is not None:
current_tread_run = custom_first_last_tread_run[0] current_tread_run = custom_first_last_tread_run[0]
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] != 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] 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 return default_tread_offset, default_tread_verts
# each tread is a separate shape # each tread is a separate shape
cur_offset = V_(0, 0) cur_offset = V_(0, 0)
tread_index = 0
for i in range(number_of_risers): for i in range(number_of_risers):
tread_offset, tread_verts = get_tread_data(i) 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 # Skip adding vertices/edges for zero-width treads
verts_to_add = ( if tread_verts:
(cur_vertex, cur_vertex + 1), cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts]
(cur_vertex + 1, cur_vertex + 2), vertices.extend(cur_trade_shape)
(cur_vertex + 2, cur_vertex + 3),
(cur_vertex + 3, cur_vertex), cur_vertex = tread_index * 4
) verts_to_add = (
edges.extend(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 cur_offset += tread_offset
elif stair_type == "GENERIC": elif stair_type == "GENERIC":
+16 -9
View File
@@ -512,14 +512,12 @@ class Sequence(bonsai.core.tool.Sequence):
@classmethod @classmethod
def get_task_inputs(cls, task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: def get_task_inputs(cls, task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
props = cls.get_work_schedule_props() props = cls.get_work_schedule_props()
is_deep = props.show_nested_inputs return ifcopenshell.util.sequence.get_task_inputs(task, is_recursive=props.show_nested_inputs)
return ifcopenshell.util.sequence.get_task_inputs(task, is_deep)
@classmethod @classmethod
def get_task_outputs(cls, task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: def get_task_outputs(cls, task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
props = cls.get_work_schedule_props() props = cls.get_work_schedule_props()
is_deep = props.show_nested_outputs return ifcopenshell.util.sequence.get_task_outputs(task, is_recursive=props.show_nested_outputs)
return ifcopenshell.util.sequence.get_task_outputs(task, is_deep)
@classmethod @classmethod
def are_entities_same_class(cls, entities: list[ifcopenshell.entity_instance]) -> bool: 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: if not task:
return return
props = cls.get_work_schedule_props() props = cls.get_work_schedule_props()
is_deep = props.show_nested_resources return ifcopenshell.util.sequence.get_task_resources(task, props.show_nested_resources)
return ifcopenshell.util.sequence.get_task_resources(task, is_deep)
@classmethod @classmethod
def load_task_inputs(cls, inputs: list[ifcopenshell.entity_instance]) -> None: def load_task_inputs(cls, inputs: list[ifcopenshell.entity_instance]) -> None:
@@ -1582,7 +1579,7 @@ class Sequence(bonsai.core.tool.Sequence):
@classmethod @classmethod
def create_new_task_json(cls, task, json, type_map=None, baseline_schedule=None): def create_new_task_json(cls, task, json, type_map=None, baseline_schedule=None):
task_time = task.TaskTime 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 = "" string_resources = ""
resources_usage = "" resources_usage = ""
@@ -1689,8 +1686,8 @@ class Sequence(bonsai.core.tool.Sequence):
) -> list[ifcopenshell.entity_instance]: ) -> list[ifcopenshell.entity_instance]:
products = [] products = []
for task in ifcopenshell.util.sequence.get_root_tasks(work_schedule): 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_inputs(task, is_recursive=True))
products.extend(ifcopenshell.util.sequence.get_task_outputs(task, is_deep=True)) products.extend(ifcopenshell.util.sequence.get_task_outputs(task, is_recursive=True))
return products return products
@classmethod @classmethod
@@ -1789,6 +1786,16 @@ class Sequence(bonsai.core.tool.Sequence):
cls.load_task_outputs(outputs) cls.load_task_outputs(outputs)
cls.load_task_resources(task) 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 @classmethod
def refresh_task_resources(cls): def refresh_task_resources(cls):
task = cls.get_highlighted_task() task = cls.get_highlighted_task()
-1
View File
@@ -175,7 +175,6 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod @classmethod
def select_products(cls, products: Iterable[ifcopenshell.entity_instance], unhide: bool = False) -> None: def select_products(cls, products: Iterable[ifcopenshell.entity_instance], unhide: bool = False) -> None:
bpy.ops.object.select_all(action="DESELECT")
for product in products: for product in products:
obj = tool.Ifc.get_object(product) obj = tool.Ifc.get_object(product)
if obj and bpy.context.view_layer.objects.get(obj.name): if obj and bpy.context.view_layer.objects.get(obj.name):
+238
View File
@@ -176,6 +176,27 @@ class TestStairCalculatedParams(NewFile):
calculated_data["Length"] += -0.2 + 0.1 calculated_data["Length"] += -0.2 + 0.1
self.compare_data(pset_data, calculated_data) 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 # overlap affects stair length only by first tread
pset_data = pset_data_base.copy() pset_data = pset_data_base.copy()
calculated_data = calculated_data_base.copy() calculated_data = calculated_data_base.copy()
@@ -296,6 +317,94 @@ class TestGenerateStair2DProfile(NewFile):
generated_profile = subject.generate_stair_2d_profile(**kwargs) generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile) 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): def test_create_wood_steel_stair(self):
kwargs = { kwargs = {
"height": 1.0, "height": 1.0,
@@ -348,6 +457,100 @@ class TestGenerateStair2DProfile(NewFile):
generated_profile = subject.generate_stair_2d_profile(**kwargs) generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile) 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): def test_create_generic_stair(self):
kwargs = {"height": 1.0, "number_of_treads": 3, "stair_type": "GENERIC", "tread_run": 0.3, "width": 1.2} kwargs = {"height": 1.0, "number_of_treads": 3, "stair_type": "GENERIC", "tread_run": 0.3, "width": 1.2}
verts_data = ( verts_data = (
@@ -381,6 +584,41 @@ class TestGenerateStair2DProfile(NewFile):
generated_profile = subject.generate_stair_2d_profile(**kwargs) generated_profile = subject.generate_stair_2d_profile(**kwargs)
self.compare_data(generated_profile, expected_profile) 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): class TestUsingArrays(NewFile):
def setup_array(self, add_second_layer=False, sync_children=False): def setup_array(self, add_second_layer=False, sync_children=False):
+2 -2
View File
@@ -96,11 +96,11 @@ writer.write()
### CLI app for converting IFC files to CSV, ODS or XLSX format. ### CLI app for converting IFC files to CSV, ODS or XLSX format.
Usage: 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: Arguments:
input_file (str): The path to the input IFC file to process. 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: Options:
-l, --log log_file (str): The path to the file where errors should be logged. Default is process.log. -l, --log log_file (str): The path to the file where errors should be logged. Default is process.log.
+6 -3
View File
@@ -319,8 +319,8 @@ class Ifc5Dwriter:
cost_schedule: Optional[ifcopenshell.entity_instance] = None, cost_schedule: Optional[ifcopenshell.entity_instance] = None,
): ):
""" """
:param file: IFC file to exprot cost schedules from. :param file: IFC file to export cost schedules from.
:param output: Output directory for csv files. :param output: Output directory.
:param cost_schedule: exported cost schedule. If not provided, will export all available schedules. :param cost_schedule: exported cost schedule. If not provided, will export all available schedules.
""" """
self.output = output self.output = output
@@ -404,6 +404,7 @@ class Ifc5DCsvWriter(Ifc5Dwriter):
import csv import csv
super().write() super().write()
os.makedirs(self.output, exist_ok=True)
for sheet, data in self.sheet_data.items(): for sheet, data in self.sheet_data.items():
with open( with open(
os.path.join(self.output, "{}.csv".format(data["Name"])), "w", newline="", encoding="utf-8" 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")) ns1.addElement(Number(decimalplaces="2", minintegerdigits="1", grouping="true"))
self.doc.styles.addElement(ns1) self.doc.styles.addElement(ns1)
os.makedirs(self.output, exist_ok=True)
file_name = "" file_name = ""
for cost_schedule in self.cost_schedules: for cost_schedule in self.cost_schedules:
if file_name: if file_name:
@@ -537,6 +539,7 @@ class Ifc5DXlsxWriter(Ifc5Dwriter):
import xlsxwriter import xlsxwriter
super().write() super().write()
os.makedirs(self.output, exist_ok=True)
file_name = "" file_name = ""
for cost_schedule in self.cost_schedules: for cost_schedule in self.cost_schedules:
if file_name: if file_name:
@@ -683,7 +686,7 @@ class Ifc5DPdfWriter(Ifc5Dwriter):
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("input", type=str, help="Specify an IFC file to process") 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("-l", "--log", type=str, help="Specify where errors should be logged", default="process.log")
parser.add_argument( parser.add_argument(
"-f", "--format", type=str, help="Choose which format to export in (CSV/ODS/XLSX)", default="CSV" "-f", "--format", type=str, help="Choose which format to export in (CSV/ODS/XLSX)", default="CSV"
+28 -9
View File
@@ -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::string>&); void parse_filter(geom_filter &, const std::vector<std::string>&);
std::vector<IfcGeom::filter_t> setup_filters(const std::vector<geom_filter>&, const std::string&); std::vector<IfcGeom::filter_t> setup_filters(const std::vector<geom_filter>&, 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 // from https://stackoverflow.com/questions/31696328/boost-program-options-using-zero-parameter-options-multiple-times
struct verbosity_counter { struct verbosity_counter {
@@ -965,7 +965,10 @@ int main(int argc, char** argv) {
time_t start,end; time_t start,end;
time(&start); 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); write_log(!quiet);
serializer.reset(); serializer.reset();
IfcUtil::path::delete_file(IfcUtil::path::to_utf8(output_temp_filename)); /**< @todo Windows Unicode support */ 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 <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
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; time_t start, end;
// Prevent IfcFile::Init() prints by setting output to null temporarily // 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); time(&start);
bool requires_init = false;
#ifdef WITH_IFCXML #ifdef WITH_IFCXML
if (boost::ends_with(boost::to_lower_copy(filename), ".ifcxml")) { if (boost::ends_with(boost::to_lower_copy(filename), ".ifcxml")) {
ifc_file = IfcParse::parse_ifcxml(filename); ifc_file = IfcParse::parse_ifcxml(filename);
} else } else
#endif #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 #ifdef USE_MMAP
ifc_file = new IfcParse::IfcFile(filename, mmap); if (mmap) {
ifc_file->initialize(filename, mmap);
requires_init = false;
}
#else #else
(void)mmap; (void)mmap;
ifc_file = new IfcParse::IfcFile(filename);
#endif #endif
} if (requires_init) {
ifc_file->initialize(filename);
}
if (!ifc_file || !ifc_file->good()) { if (!ifc_file || !ifc_file->good()) {
Logger::Error("Unable to parse input file '" + filename + "'"); Logger::Error("Unable to parse input file '" + filename + "'");
+1
View File
@@ -172,6 +172,7 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_,
} }
auto interpolated_loop = polygon_from_points(points); auto interpolated_loop = polygon_from_points(points);
interpolated_loop->external = w1->external;
if (interpolated->kind() == taxonomy::FACE) { if (interpolated->kind() == taxonomy::FACE) {
std::static_pointer_cast<taxonomy::face>(interpolated)->children.push_back(interpolated_loop); std::static_pointer_cast<taxonomy::face>(interpolated)->children.push_back(interpolated_loop);
} else { } else {
+44 -31
View File
@@ -118,7 +118,8 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
auto jt = it + 1; auto jt = it + 1;
std::array<taxonomy::item::ptr, 2> fa = { *it, *jt }; std::array<taxonomy::item::ptr, 2> fa = { *it, *jt };
std::array<TopoDS_Shape, 2> shps; std::array<TopoDS_Shape, 2> shps;
std::array<TopoDS_Wire, 2> ws; std::vector<std::array<TopoDS_Wire, 2>> ws;
ws.emplace_back();
for (int i = 0; i < 2; ++i) { for (int i = 0; i < 2; ++i) {
if (fa[i]->kind() == taxonomy::FACE) { if (fa[i]->kind() == taxonomy::FACE) {
if (!convert(std::static_pointer_cast<taxonomy::face>(fa[i]), shps[i])) { if (!convert(std::static_pointer_cast<taxonomy::face>(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) { if (shps[i].ShapeType() != TopAbs_FACE && shps[i].ShapeType() != TopAbs_WIRE) {
return false; return false;
} }
// @todo this is only outer wire
if (shps[i].ShapeType() == TopAbs_FACE) { 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 { } else {
ws[i] = TopoDS::Wire(shps[i]); ws[0][i] = TopoDS::Wire(shps[i]);
} }
} }
if (shps[0].ShapeType() == TopAbs_FACE) { 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]); 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 for (auto& wp : ws) {
TopoDS_Vertex e1a, e1b, e3a, e3b; BRepTools_WireExplorer a(wp[0]);
TopExp::Vertices(e1, e1a, e1b, true); BRepTools_WireExplorer b(wp[1]);
TopExp::Vertices(e3, e3a, e3b, true); for (; a.More() && b.More(); a.Next(), b.Next()) {
auto e2 = BRepBuilderAPI_MakeEdge(e1b, e3a).Edge(); auto& e1 = a.Current();
auto e4 = BRepBuilderAPI_MakeEdge(e3b, e1a).Edge(); // auto e3 = TopoDS::Edge(b.Current().Reversed());
auto& e3 = b.Current();
/*
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(); // Documentation says unconnected edges are automatically connected, but this is not the case
BB.Add(comp, f); TopoDS_Vertex e1a, e1b, e3a, e3b;
auto g = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e3b, e3a, e1a, true).Wire()).Face(); TopExp::Vertices(e1, e1a, e1b, true);
BB.Add(comp, g); 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);
}
} }
} }
+41 -45
View File
@@ -25,62 +25,58 @@ using namespace ifcopenshell::geometry;
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAxis2PlacementLinear* inst) { taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAxis2PlacementLinear* inst) {
if (!inst->Location()->as<IfcSchema::IfcPointByDistanceExpression>()) if (!inst->Location()->as<IfcSchema::IfcPointByDistanceExpression>()) {
Logger::Error(std::runtime_error("Location must be IfcPointByDistanceExpression for IfcAxis2PlacementLinear")); 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<taxonomy::matrix4>(map(inst->Location())); taxonomy::matrix4::ptr m = taxonomy::cast<taxonomy::matrix4>(map(inst->Location()));
o = m->components().col(3).head<3>(); o = m->components().col(3).head<3>();
// From 8.9.3.4 IfcAxis2PlacementLinear there are 4 cases that need to be considered // From 8.9.3.4 IfcAxis2PlacementLinear there are 4 cases that need to be considered
// 1) Axis is given but not RefDirection // 1) Axis is given but not RefDirection
// 2) RefDirection is given but not Axis // 2) RefDirection is given but not Axis
// 3) Neither Axis or RefDirection are provided // 3) Neither Axis or RefDirection are provided
// 4) Both Axis and RefDirection are provided // 4) Both Axis and RefDirection are provided
const bool hasAxis = inst->Axis() != nullptr; const bool hasAxis = inst->Axis() != nullptr;
const bool hasRef = inst->RefDirection() != nullptr; const bool hasRef = inst->RefDirection() != nullptr;
if (hasAxis != hasRef) { /*
if (hasAxis != hasRef) {
Logger::Warning("Axis and RefDirection should be specified together", inst); Logger::Warning("Axis and RefDirection should be specified together", inst);
} }
*/
if (hasAxis && !hasRef) if (hasAxis && !hasRef) {
{ taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis())); axis = *a->components_;
axis = *a->components_;
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
// refDirection is not necessarily orthogonal to axis. // refDirection is not necessarily orthogonal to axis.
// axis.cross(refDirection) gives y. y.cross(axis) gives x=refDirection // axis.cross(refDirection) gives y. y.cross(axis) gives x=refDirection
refDirection = axis.cross(refDirection).cross(axis); refDirection = axis.cross(refDirection).cross(axis);
} } else if (!hasAxis && hasRef) {
else if (!hasAxis && hasRef) taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
{ refDirection = *r->components_;
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection())); Eigen::Vector3d up(0, 0, 1);
refDirection = *r->components_; axis = refDirection.cross(up.cross(refDirection));
Eigen::Vector3d up(0, 0, 1); } else if (!hasAxis && !hasRef) {
axis = refDirection.cross(up.cross(refDirection)); refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
} Eigen::Vector3d up(0, 0, 1);
else if (!hasAxis && !hasRef) axis = refDirection.cross(up.cross(refDirection));
{ } else {
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
Eigen::Vector3d up(0, 0, 1); axis = *a->components_;
axis = refDirection.cross(up.cross(refDirection));
}
else
{
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
axis = *a->components_;
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection())); taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
refDirection = *r->components_; refDirection = *r->components_;
refDirection = axis.cross(refDirection).cross(axis); // refDirection needs to be orthogonal to axis refDirection = axis.cross(refDirection).cross(axis); // refDirection needs to be orthogonal to axis
} }
// axis and refDirection need to be orthogonal // axis and refDirection need to be orthogonal
return taxonomy::make<taxonomy::matrix4>(o, axis, refDirection); return taxonomy::make<taxonomy::matrix4>(o, axis, refDirection);
} }
#endif #endif
+1 -1
View File
@@ -24,7 +24,7 @@ using namespace ifcopenshell::geometry;
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcBoundingBox* inst) { taxonomy::ptr mapping::map_impl(const IfcSchema::IfcBoundingBox* inst) {
if (!settings_.get<settings::KeepBoundingBoxes>().get()) { if (!settings_.get<settings::KeepBoundingBoxes>().get()) {
// @todo make sure it doesn't log. failed_on_purpose_.insert(inst);
return nullptr; return nullptr;
} }
@@ -81,7 +81,13 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in
rot = taxonomy::matrix4( rot = taxonomy::matrix4(
Eigen::Vector3d(0, 0, 0), Eigen::Vector3d(0, 0, 0),
taxonomy::cast<taxonomy::direction3>(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0); taxonomy::cast<taxonomy::direction3>(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<taxonomy::direction3>(map(csp->RefDirection()))->ccomponents()
).ccomponents().block<3, 3>(0, 0);
}
profile_rotations.push_back(rot); profile_rotations.push_back(rot);
} }
if (faces.size() != profile_offsets.size()) { if (faces.size() != profile_offsets.size()) {
+8 -1
View File
@@ -82,7 +82,14 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) {
rot = taxonomy::matrix4( rot = taxonomy::matrix4(
Eigen::Vector3d(0, 0, 0), Eigen::Vector3d(0, 0, 0),
taxonomy::cast<taxonomy::direction3>(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0); taxonomy::cast<taxonomy::direction3>(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<taxonomy::direction3>(map(csp->RefDirection()))->ccomponents())
.ccomponents()
.block<3, 3>(0, 0);
}
profile_rotations.push_back(rot); profile_rotations.push_back(rot);
} }
#else #else
+12
View File
@@ -372,6 +372,18 @@ const IfcUtil::IfcBaseEntity* mapping::get_single_material_association(const Ifc
} }
} }
#endif #endif
#ifdef SCHEMA_HAS_IfcMaterialConstituentSet
if (associated_material->as<IfcSchema::IfcMaterialConstituentSet>()) {
IfcSchema::IfcMaterialConstituentSet* constituentset = associated_material->as<IfcSchema::IfcMaterialConstituentSet>();
if (settings_.get<settings::LayersetFirst>().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
} }
} }
} }
+5
View File
@@ -79,6 +79,11 @@ build-urls:
test: test:
pytest -p no:pytest-blender test --ignore=test/util/test_shape_builder.py 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 .PHONY: test-mathutils
test-mathutils: test-mathutils:
pytest -p no:pytest-blender test/util/test_shape_builder.py pytest -p no:pytest-blender test/util/test_shape_builder.py
@@ -60,6 +60,7 @@ import zipfile
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from typing import Optional, Union, TYPE_CHECKING, Any, overload, Literal from typing import Optional, Union, TYPE_CHECKING, Any, overload, Literal
from collections.abc import Sequence
if TYPE_CHECKING: if TYPE_CHECKING:
import ifcopenshell.express.schema_class 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 path: Union[os.PathLike, str], format: Optional[str] = None, *, should_stream: bool = False, readonly: bool = False
) -> Union[_file, sqlite, _stream]: ... ) -> Union[_file, sqlite, _stream]: ...
def open( 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]: ) -> Union[_file, sqlite, _stream]:
"""Loads an IFC dataset from a filepath """Loads an IFC dataset from a filepath
@@ -186,7 +192,17 @@ def open(
if should_stream: if should_stream:
return stream(path) return stream(path)
if readonly: # Temporary conditional see #7131. Remove once newer builds don't segfault on Linux. 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: else:
f = ifcopenshell_wrapper.open(str(path.absolute())) f = ifcopenshell_wrapper.open(str(path.absolute()))
return file(f) return file(f)
@@ -299,20 +315,44 @@ def guess_format(path: Path) -> Literal[".ifc", ".ifcZIP", ".ifcXML", ".ifcJSON"
return None 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 """Streams the content of a file path from disk, yielding each instance
as a dictionary. as a dictionary.
Args: Args:
path (Union[Path, str]): input file path 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: Yields:
dict: entity instance dictionaries dict: entity instance dictionaries
""" """
streamer = ifcopenshell_wrapper.InstanceStreamer(str(path)) if page_size:
while streamer: import builtins
if inst := streamer.read_instance_py():
yield inst 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): def stream2_from_string(data: str):
@@ -143,7 +143,7 @@ class Usecase:
return next(e for e in self.file.get_inverse(self.task_time) if e.is_a("IfcTask")) return next(e for e in self.file.get_inverse(self.task_time) if e.is_a("IfcTask"))
def handle_resource_calculation(self): 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: for resource in resources:
if ifcopenshell.util.constraint.is_attribute_locked(resource, "Usage.ScheduleWork"): if ifcopenshell.util.constraint.is_attribute_locked(resource, "Usage.ScheduleWork"):
ifcopenshell.api.resource.calculate_resource_usage(self.file, resource=resource) ifcopenshell.api.resource.calculate_resource_usage(self.file, resource=resource)
@@ -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)
@@ -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
)
@@ -0,0 +1 @@
#endif
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* 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. *
* *
********************************************************************************/
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* 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. *
* *
********************************************************************************/
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* 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. *
* *
********************************************************************************/
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* 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. *
* *
********************************************************************************/
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* 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. *
* *
********************************************************************************/
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* 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. *
* *
********************************************************************************/
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* 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. *
* *
********************************************************************************/
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* 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. *
* *
********************************************************************************/
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/********************************************************************************
* *
* 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. *
* *
********************************************************************************/
@@ -0,0 +1,2 @@
#ifdef USE_IFC4
@@ -0,0 +1,2 @@
#ifdef USE_IFC4X1
@@ -0,0 +1,2 @@
#ifndef USE_IFC4
@@ -0,0 +1,2 @@
#ifndef USE_IFC4X1
@@ -0,0 +1,2 @@
#ifndef USE_IFC4X1
+29 -6
View File
@@ -248,6 +248,10 @@ READ_ERROR = ifcopenshell_wrapper.file_open_status.READ_ERROR
NO_HEADER = ifcopenshell_wrapper.file_open_status.NO_HEADER NO_HEADER = ifcopenshell_wrapper.file_open_status.NO_HEADER
UNSUPPORTED_SCHEMA = ifcopenshell_wrapper.file_open_status.UNSUPPORTED_SCHEMA UNSUPPORTED_SCHEMA = ifcopenshell_wrapper.file_open_status.UNSUPPORTED_SCHEMA
INVALID_SYNTAX = ifcopenshell_wrapper.file_open_status.INVALID_SYNTAX INVALID_SYNTAX = ifcopenshell_wrapper.file_open_status.INVALID_SYNTAX
try:
UNKNOWN = ifcopenshell_wrapper.file_open_status.UNKNOWN
except:
UNKNOWN = 5 # Workaround
import struct 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])) 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: class file:
"""Base class for containing IFC files. """Base class for containing IFC files.
@@ -513,7 +535,7 @@ class file:
""" """
wrapped_data: ifcopenshell_wrapper.file wrapped_data: ifcopenshell_wrapper.file
header: ifcopenshell_wrapper.IfcSpfHeader header: file_header
units: dict[str, entity_instance] = {} units: dict[str, entity_instance] = {}
history_size: int = 64 history_size: int = 64
history: list[Transaction] history: list[Transaction]
@@ -586,8 +608,11 @@ class file:
"Unsupported schema: %s" % ",".join(self.header.file_schema.schema_identifiers), "Unsupported schema: %s" % ",".join(self.header.file_schema.schema_identifiers),
), ),
INVALID_SYNTAX: lambda: (Error, "Syntax error during parse, check logs"), 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()]() }[f.good().value()]()
raise exc(msg) if exc is not None:
raise exc(msg)
else: else:
args = filter(None, [schema]) args = filter(None, [schema])
args = map(ifcopenshell_wrapper.schema_by_name, args) args = map(ifcopenshell_wrapper.schema_by_name, args)
@@ -1025,12 +1050,10 @@ class file:
@property @property
def header(self): def header(self):
try: # Temporary workaround until new builds are ready. See #7131. try:
h = self.wrapped_data.header() return file_header(self, self.wrapped_data.header())
except: except:
return self.wrapped_data.header return self.wrapped_data.header
object.__setattr__(h, "file_ref", lambda inst: entity_instance.wrap_value(inst, file=self))
return h
@property @property
def storage(self) -> Optional[rocksdb_file_storage]: def storage(self) -> Optional[rocksdb_file_storage]:
@@ -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,
}
)
)
@@ -117,6 +117,7 @@ def get_pset(
if ( if (
psets_only psets_only
and not pset.is_a("IfcPropertySet") and not pset.is_a("IfcPropertySet")
and not pset.is_a("IfcPreDefinedPropertySet")
and not (is_ifc2x3 and pset.is_a("IfcExtendedMaterialProperties")) and not (is_ifc2x3 and pset.is_a("IfcExtendedMaterialProperties"))
): ):
pset = None pset = None
@@ -126,7 +127,11 @@ def get_pset(
if type_pset is not None and not prop: if type_pset is not None and not prop:
if psets_only or qtos_only: if psets_only or qtos_only:
type_pset_element = element.file.by_id(type_pset["id"]) 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 type_pset = None
elif qtos_only and not type_pset_element.is_a("IfcElementQuantity"): elif qtos_only and not type_pset_element.is_a("IfcElementQuantity"):
type_pset = None type_pset = None
@@ -177,7 +182,7 @@ def get_psets(
psets = {} psets = {}
if element.is_a("IfcTypeObject"): if element.is_a("IfcTypeObject"):
for definition in element.HasPropertySets or []: 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 continue
if qtos_only and not definition.is_a("IfcElementQuantity"): if qtos_only and not definition.is_a("IfcElementQuantity"):
continue continue
@@ -213,7 +218,11 @@ def get_psets(
for relationship in is_defined_by: for relationship in is_defined_by:
if relationship.is_a("IfcRelDefinesByProperties"): if relationship.is_a("IfcRelDefinesByProperties"):
definition = relationship.RelatingPropertyDefinition 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 continue
if qtos_only and not definition.is_a("IfcElementQuantity"): if qtos_only and not definition.is_a("IfcElementQuantity"):
continue continue
@@ -565,6 +574,40 @@ def get_predefined_type(element: ifcopenshell.entity_instance) -> Union[str, Non
return predefined_type 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]: def get_type(element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
"""Retrieves the construction type element of an element occurrence. """Retrieves the construction type element of an element occurrence.
@@ -329,61 +329,28 @@ def guess_date_range(work_schedule: ifcopenshell.entity_instance):
return earliest, latest return earliest, latest
def get_direct_task_outputs(task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: def get_task_outputs(
return [rel.RelatingProduct for rel in task.HasAssignments if rel.is_a("IfcRelAssignsToProduct")] 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]: def get_task_inputs(
if not is_deep: task: ifcopenshell.entity_instance, is_recursive: bool = False
return get_direct_task_outputs(task) ) -> set[ifcopenshell.entity_instance]:
else: if is_recursive:
return [output for nested_task in get_all_nested_tasks(task) for output in get_direct_task_outputs(nested_task)] 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]: def get_task_resources(
if not is_deep: task: ifcopenshell.entity_instance, is_recursive: bool = False
return [ ) -> set[ifcopenshell.entity_instance]:
object if is_recursive:
for rel in task.OperatesOn return {r for subtask in [task] + list(get_all_nested_tasks(task)) for r in get_task_resources(subtask)}
if rel.is_a("IfcRelAssignsToProcess") return {o for rel in task.OperatesOn for o in rel.RelatedObjects if o.is_a("IfcResource")}
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 has_task_outputs(task: ifcopenshell.entity_instance) -> bool: def has_task_outputs(task: ifcopenshell.entity_instance) -> bool:
@@ -320,14 +320,16 @@ def log_internal_cpp_errors(
chr_offset_re = re.compile(r"at offset (\d+)\s*") chr_offset_re = re.compile(r"at offset (\d+)\s*")
for_instance_re = re.compile(r"\s*for instance #(\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: if log_content is None:
log_content = ifcopenshell.get_log() log_content = ifcopenshell.get_log()
msgs = list(map(json.loads, filter(None, log_content.split("\n")))) msgs = list(map(json.loads, filter(None, log_content.split("\n"))))
chr_offsets = [chr_offset_re.findall(m["message"]) for m in msgs] chr_offsets = [chr_offset_re.findall(m["message"]) for m in msgs]
instance_messages = [for_instance_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 # The file is opened in binary mode, in order
# to correspond with the offsets reported by # to correspond with the offsets reported by
# IfcOpenShell C++ # IfcOpenShell C++
@@ -348,7 +350,7 @@ def log_internal_cpp_errors(
else: else:
logger.error("For instance:\n %s\n%s", line, m) 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): for instid, msg in zip(instance_messages, msgs):
if instid: if instid:
m = for_instance_re.sub("", msg["message"]) m = for_instance_re.sub("", msg["message"])
@@ -376,6 +378,12 @@ def log_internal_cpp_errors(
else: else:
logger.error(m) 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, ...]]] = {} entity_attribute_map: dict[tuple[str, str], tuple[entity_type, tuple[attribute, ...]]] = {}
@@ -1,12 +1,12 @@
ISO-10303-21; ISO-10303-21;
HEADER; HEADER;
FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); 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')); FILE_SCHEMA(('IFC4X3_ADD2'));
ENDSEC; ENDSEC;
DATA; DATA;
#1=IFCGEOGRAPHICCRS('EPSG:4326',$,$,$,$); #1=IFCGEOGRAPHICCRS('EPSG:4326',$,$,$,$,$);
#2=IFCGEOGRAPHICCRS('EPSG:3857',$,$,$,$); #2=IFCGEOGRAPHICCRS('EPSG:3857',$,$,$,$,$);
#3=IFCRIGIDOPERATION(#1,#2,IFCPLANEANGLEMEASURE(0.),IFCPLANEANGLEMEASURE(0.),0.); #3=IFCRIGIDOPERATION(#1,#2,IFCPLANEANGLEMEASURE(0.),IFCPLANEANGLEMEASURE(0.),0.);
ENDSEC; ENDSEC;
END-ISO-10303-21; END-ISO-10303-21;
@@ -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
@@ -23,6 +23,11 @@ import pytest
import ifcopenshell import ifcopenshell
import tempfile import tempfile
try:
import psutil
except ImportError:
psutil = None
fn = os.path.join(os.path.dirname(__file__), "fixtures/ColumnPSetsOfSets.ifc") 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(): def test_file():
f = ifcopenshell.open(fn) f = ifcopenshell.open(fn)
assert f[139].RelatingPropertyDefinition.is_a("IfcPropertySetDefinitionSet") assert f[139].RelatingPropertyDefinition.is_a("IfcPropertySetDefinitionSet")
assert {x.id() for x in f[139].RelatingPropertyDefinition[0]} == {136, 138} 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(): def test_rocks():
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as d:
rfn = os.path.join(d, os.path.basename(fn)) rfn = os.path.join(d, os.path.basename(fn))
@@ -40,9 +40,11 @@ class TestWrite:
def test_write_ifcspf(self): def test_write_ifcspf(self):
self.assert_model_is_written("model.ifc") self.assert_model_is_written("model.ifc")
@pytest.mark.skip(reason="IFCXML writing not supported")
def test_write_ifcxml(self): def test_write_ifcxml(self):
self.assert_model_is_written("model.ifcXML") self.assert_model_is_written("model.ifcXML")
@pytest.mark.skip(reason="IFCXML writing not supported")
def test_write_ifc_zip_ifcxml_format(self): def test_write_ifc_zip_ifcxml_format(self):
self.assert_model_is_written("model.ifcZIP", format=".ifcXML", zipped=True) 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): def test_write_anyextension_ifczip_ifcspf_format(self):
self.assert_model_is_written("model.anyextension", format=".ifc", zipped=True) 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): def test_write_anyextension_ifcxml_format(self):
self.assert_model_is_written("model.anyextension", format=".ifcXML") 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): def test_write_anyextension_ifczip_ifcxml_format(self):
self.assert_model_is_written("model.anyextension", format=".ifcXML", zipped=True) self.assert_model_is_written("model.anyextension", format=".ifcXML", zipped=True)
@@ -43,7 +43,7 @@ import ifcopenshell.util.element as subject
from ifcopenshell.util.shape_builder import ShapeBuilder from ifcopenshell.util.shape_builder import ShapeBuilder
class TestIFC2X3MaterialProfilePsts(test.bootstrap.IFC2X3): class TestIFC2X3MaterialProfilePsets(test.bootstrap.IFC2X3):
def test_get_profile_pset(self): def test_get_profile_pset(self):
profile = ifcopenshell.api.profile.add_parameterized_profile(self.file, "IfcRectangleProfileDef") profile = ifcopenshell.api.profile.add_parameterized_profile(self.file, "IfcRectangleProfileDef")
pset = ifcopenshell.api.pset.add_pset(self.file, profile, "") 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") is None
assert subject.get_pset(self.file.create_entity("IfcPerson"), "name", "a") 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): class TestGetPsetsIFC4(test.bootstrap.IFC4):
def test_getting_the_psets_of_a_product_as_a_dictionary(self): 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}) 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()}} 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): class TestGetPropertyDefinitionIFC4(test.bootstrap.IFC4):
def test_getting_the_properties_of_a_pset(self): 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" 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): class TestGetTypeIFC4(test.bootstrap.IFC4):
def test_getting_the_type_of_a_product(self): def test_getting_the_type_of_a_product(self):
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
+326
View File
@@ -0,0 +1,326 @@
#include "FileReader.h"
#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <list>
#include <stdexcept>
#include <unordered_map>
#include <utility>
#include <vector>
#include <deque>
#ifdef USE_MMAP
#include <boost/iostreams/device/mapped_file.hpp>
#include <boost/filesystem/path.hpp>
#endif
#include "utils.h"
namespace {
#if defined(_WIN32)
inline void file_seek_abs(FILE* f, std::uint64_t off) {
if (_fseeki64(f, static_cast<long long>(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_t>(off), SEEK_SET) != 0)
throw std::runtime_error("fseeko failed");
}
#endif
} // namespace
using namespace IfcParse;
struct FullBufferImpl final : FileReader::Impl {
std::vector<char> 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<size_t> lru_;
struct Entry {
FileReader::Page page;
std::list<size_t>::iterator it;
};
mutable std::unordered_map<size_t, Entry> map_;
PagedFileImpl(const std::string& fn, size_t page_size, size_t cap)
: fn_(fn), page_size_(std::max<size_t>(512, page_size)), capacity_(std::max<size_t>(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<size_t, Entry>::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<size_t>(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<PushedSequentialImpl>, FileReader::Impl {
// Deque of pages, front is earliest in file.
std::deque<FileReader::Page> 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<PushedSequentialImpl*>(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<FullBufferImpl>(fn);
}
IfcParse::FileReader::FileReader(const std::string& fn, const mmap_tag&)
: cursor_(0)
{
#ifdef USE_MMAP
impl_ = std::make_shared<MMapImpl>(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<PushedSequentialImpl>();
}
IfcParse::FileReader::FileReader(const std::string& content, const caller_fed_tag&)
{
impl_ = std::make_shared<PushedSequentialImpl>();
impl_->pushNextPage(content);
}
IfcParse::FileReader::FileReader(const std::string& fn, size_t page_size, size_t page_capacity)
: cursor_(0)
{
impl_ = std::make_shared<PagedFileImpl>(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);
}
+133
View File
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
/*********************************************************************************
* *
* 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 <cstddef>
#include <memory>
#include <optional>
#include <string>
#include <variant>
#include <stdexcept>
#include <vector>
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<char> 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> impl_;
size_t cursor_ = 0;
};
} // namespace IfcParse
#endif
+18 -55
View File
@@ -29,7 +29,7 @@
#include "IfcCharacterDecoder.h" #include "IfcCharacterDecoder.h"
#include "IfcException.h" #include "IfcException.h"
#include "IfcSpfStream.h" #include "FileReader.h"
#include "IfcLogger.h" #include "IfcLogger.h"
#include <codecvt> #include <codecvt>
@@ -77,7 +77,7 @@
using namespace IfcParse; using namespace IfcParse;
IfcCharacterDecoder::IfcCharacterDecoder(IfcParse::IfcSpfStream* stream) { IfcCharacterDecoder::IfcCharacterDecoder(IfcParse::FileReader* stream) {
stream_ = stream; stream_ = stream;
codepage_ = 0; codepage_ = 0;
} }
@@ -86,49 +86,9 @@ IfcCharacterDecoder::~IfcCharacterDecoder() {
} }
namespace { 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; unsigned int parse_state = 0;
builder_.clear(); builder_.clear();
builder_.push_back('\''); builder_.push_back('\'');
@@ -137,7 +97,7 @@ class pure_impure_helper {
unsigned int hex = 0; unsigned int hex = 0;
unsigned int hex_count = 0; unsigned int hex_count = 0;
while ((current_char = peek()) != 0) { while ((current_char = stream_.peek()) != 0) {
if (EXPECTS_CHARACTER(parse_state)) { if (EXPECTS_CHARACTER(parse_state)) {
builder_.push_back(IfcUtil::convert_codepage(codepage, current_char + 0x80)); builder_.push_back(IfcUtil::convert_codepage(codepage, current_char + 0x80));
parse_state = 0; parse_state = 0;
@@ -178,7 +138,7 @@ class pure_impure_helper {
} else if (IS_HEXADECIMAL(current_char) && EXPECTS_HEX(parse_state)) { } else if (IS_HEXADECIMAL(current_char) && EXPECTS_HEX(parse_state)) {
if (IS_LOWERCASE_HEX(current_char)) { if (IS_LOWERCASE_HEX(current_char)) {
Logger::Warning("Lowercase hexadecimal character '" + std::string(1, 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."); ". It is recommended to use uppercase for hexadecimal.");
} }
hex <<= 4; hex <<= 4;
@@ -202,12 +162,12 @@ class pure_impure_helper {
if (parse_state == APOSTROPHE && current_char != '\'') { if (parse_state == APOSTROPHE && current_char != '\'') {
break; break;
} }
throw IfcInvalidTokenException(tell(), current_char); throw IfcInvalidTokenException(stream_.tell(), current_char);
} else { } else {
parse_state = hex = hex_count = 0; parse_state = hex = hex_count = 0;
builder_.push_back(current_char); builder_.push_back(current_char);
} }
increment(); stream_.increment();
} }
builder_.push_back('\''); builder_.push_back('\'');
@@ -248,22 +208,25 @@ class pure_impure_helper {
} }
throw IfcParse::IfcException("Invalid conversion mode"); throw IfcParse::IfcException("Invalid conversion mode");
} }
};
} // namespace } // namespace
IfcCharacterDecoder::operator std::string() { 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) { std::string IfcCharacterDecoder::get(size_t& ptr) {
return pure_impure_helper(stream_, ptr).get(mode, substitution_character); 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() { void IfcCharacterDecoder::skip() {
unsigned int parse_state = 0; unsigned int parse_state = 0;
char current_char; char current_char;
unsigned int hex_count = 0; unsigned int hex_count = 0;
while ((current_char = stream_->Peek()) != 0) { while ((current_char = stream_->peek()) != 0) {
if (EXPECTS_CHARACTER(parse_state)) { if (EXPECTS_CHARACTER(parse_state)) {
parse_state = 0; parse_state = 0;
} else if (current_char == '\'' && (parse_state == 0U)) { } else if (current_char == '\'' && (parse_state == 0U)) {
@@ -318,11 +281,11 @@ void IfcCharacterDecoder::skip() {
if (parse_state == APOSTROPHE && current_char != '\'') { if (parse_state == APOSTROPHE && current_char != '\'') {
break; break;
} }
throw IfcInvalidTokenException(stream_->Tell(), current_char); throw IfcInvalidTokenException(stream_->tell(), current_char);
} else { } else {
parse_state = hex_count = 0; parse_state = hex_count = 0;
} }
stream_->Inc(); stream_->increment();
} }
} }
+4 -4
View File
@@ -27,7 +27,7 @@
#ifndef IFCCHARACTERDECODER_H #ifndef IFCCHARACTERDECODER_H
#define IFCCHARACTERDECODER_H #define IFCCHARACTERDECODER_H
#include "IfcSpfStream.h" #include "FileReader.h"
#include <string> #include <string>
@@ -41,7 +41,7 @@ namespace IfcParse {
class IFC_PARSE_API IfcCharacterDecoder { class IFC_PARSE_API IfcCharacterDecoder {
private: private:
IfcParse::IfcSpfStream* stream_; IfcParse::FileReader* stream_;
int codepage_; int codepage_;
public: public:
@@ -52,7 +52,7 @@ class IFC_PARSE_API IfcCharacterDecoder {
}; };
static ConversionMode mode; static ConversionMode mode;
static char substitution_character; static char substitution_character;
IfcCharacterDecoder(IfcParse::IfcSpfStream* stream); IfcCharacterDecoder(IfcParse::FileReader* stream);
~IfcCharacterDecoder(); ~IfcCharacterDecoder();
// Only advances the underlying token stream read pointer // Only advances the underlying token stream read pointer
// to the next token. // to the next token.
@@ -62,7 +62,7 @@ class IFC_PARSE_API IfcCharacterDecoder {
operator std::string(); operator std::string();
// Gets a decoded string representation at the offset provided, // Gets a decoded string representation at the offset provided,
// does not mutate the underlying token stream read pointer. // does not mutate the underlying token stream read pointer.
std::string get(unsigned int&); std::string get(size_t&);
}; };
} // namespace IfcParse } // namespace IfcParse
+17 -8
View File
@@ -38,7 +38,7 @@ namespace {
} }
#ifdef IFOPSH_WITH_ROCKSDB #ifdef IFOPSH_WITH_ROCKSDB
else { else {
T val; T val = T{};
const bool is_header = entity_or_type->schema() == &Header_section_schema::get_schema(); const bool is_header = entity_or_type->schema() == &Header_section_schema::get_schema();
if constexpr ( if constexpr (
// the following types cannot be directly deserialized from rocksdb, but need to be constructed // 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_)) + "|" + (is_header ? entity_or_type->name() : std::to_string(instance_name_)) + "|" +
std::to_string(index_), &str); std::to_string(index_), &str);
impl::deserialize(array_.db_ptr, str, val); impl::deserialize(array_.db_ptr, str, val);
} else {
static_assert(
std::is_same_v<T, EnumerationReference> ||
std::is_same_v<std::remove_cv_t<std::remove_pointer_t<T>>, IfcUtil::IfcBaseClass>,
"RocksDB deserialization must be specialized for this EnumerationReference and IfcBaseClass*"
);
} }
return val; return val;
} }
@@ -194,13 +200,15 @@ AttributeValue::operator IfcUtil::IfcBaseClass* () const
std::to_string(index_), &str); std::to_string(index_), &str);
size_t v; size_t v;
memcpy(&v, str.data() + 2, sizeof(size_t)); memcpy(&v, str.data() + 2, sizeof(size_t));
if (str[1] == 'i') { if (str.size() > 1 && str[1] == 'i') {
// entity reference, by #Name // entity reference, by #Name
return array_.db_ptr->assert_existance(v, IfcParse::impl::rocks_db_file_storage::entityinstance_ref); 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 // type reference by Identity
return array_.db_ptr->assert_existance(v, IfcParse::impl::rocks_db_file_storage::typedecl_ref); return array_.db_ptr->assert_existance(v, IfcParse::impl::rocks_db_file_storage::typedecl_ref);
} } else {
throw std::runtime_error("Invalid data encountered");
}
} }
#endif #endif
} }
@@ -339,28 +347,28 @@ bool impl::serialize(std::string& val, const aggregate_of_aggregate_of_instance:
return true; return true;
} }
bool impl::serialize(std::string& val, const Blank& t) bool impl::serialize(std::string& val, const Blank&)
{ {
val.resize(1); val.resize(1);
val[0] = TypeEncoder::encode_type<Blank>(); val[0] = TypeEncoder::encode_type<Blank>();
return true; return true;
} }
bool impl::serialize(std::string& val, const Derived& t) bool impl::serialize(std::string& val, const Derived&)
{ {
val.resize(1); val.resize(1);
val[0] = TypeEncoder::encode_type<Derived>(); val[0] = TypeEncoder::encode_type<Derived>();
return true; 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.resize(1);
val[0] = TypeEncoder::encode_type<empty_aggregate_t>(); val[0] = TypeEncoder::encode_type<empty_aggregate_t>();
return true; 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.resize(1);
val[0] = TypeEncoder::encode_type<empty_aggregate_of_aggregate_t>(); val[0] = TypeEncoder::encode_type<empty_aggregate_of_aggregate_t>();
@@ -397,6 +405,7 @@ bool impl::deserialize(IfcParse::impl::rocks_db_file_storage*, const std::string
} else { } else {
return false; return false;
} }
return true;
} }
bool impl::deserialize(IfcParse::impl::rocks_db_file_storage*, const std::string& val, boost::dynamic_bitset<>& t) { bool impl::deserialize(IfcParse::impl::rocks_db_file_storage*, const std::string& val, boost::dynamic_bitset<>& t) {
+3 -4
View File
@@ -155,7 +155,7 @@ struct TypeEncoder_t<parameter_pack<Types...>> {
using TypeEncoder = TypeEncoder_t<type_variant_parameter_pack>; using TypeEncoder = TypeEncoder_t<type_variant_parameter_pack>;
struct IFC_PARSE_API MutableAttributeValue { struct IFC_PARSE_API MutableAttributeValue {
int name_; uint32_t name_;
uint8_t index_; uint8_t index_;
}; };
@@ -165,7 +165,7 @@ namespace IfcParse {
} }
} }
#if IFOPSH_WITH_ROCKSDB #ifdef IFOPSH_WITH_ROCKSDB
namespace impl { namespace impl {
@@ -266,7 +266,6 @@ namespace impl {
if (val[0] != TypeEncoder::encode_type<T>()) { if (val[0] != TypeEncoder::encode_type<T>()) {
return false; return false;
} }
auto s = (val.size() - 1) / sizeof(T);
memcpy(&t, val.data() + 1, sizeof(T)); memcpy(&t, val.data() + 1, sizeof(T));
return true; return true;
} }
@@ -404,7 +403,7 @@ public:
template<typename Visitor> template<typename Visitor>
auto apply_visitor(void* storage, const IfcParse::declaration* decl, std::size_t identity, std::size_t index, Visitor&& visitor) const { 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? // @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>(visitor)); AttributeValue((IfcParse::impl::rocks_db_file_storage*)storage, identity, decl, (uint8_t) index).apply_visitor(std::forward<Visitor>(visitor));
} }
#endif #endif
}; };

Some files were not shown because too many files have changed in this diff Show More