mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-07 00:11:36 +00:00
Compare commits
174 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fe491b7b5 | |||
| 6df5632f91 | |||
| 391a412e65 | |||
| b10415b1b3 | |||
| b1c44d42e3 | |||
| 5f65e21878 | |||
| 0d3be9d101 | |||
| eea585a3cd | |||
| 83f7416679 | |||
| dc5ac872e3 | |||
| 1789406299 | |||
| e05318162d | |||
| b503061dc0 | |||
| 90ccaf4aab | |||
| 05ff47fcff | |||
| c40ecdd550 | |||
| 3a0622e9df | |||
| 0bb03750dc | |||
| 89defa9b6b | |||
| 459aad54aa | |||
| 6f3065f6ce | |||
| 9c8f3be52a | |||
| 69863ce4a9 | |||
| c4a992a9b5 | |||
| e5ae19fd34 | |||
| afe617fb93 | |||
| d922f0b9ad | |||
| f9d2d494b1 | |||
| 806aa14bd0 | |||
| 80066ba9e7 | |||
| 7cb1f07a0b | |||
| bdb52dcffd | |||
| 933ad6c801 | |||
| 1594b9505c | |||
| 325745a822 | |||
| 74f8b047fb | |||
| 0f8c6ffe9e | |||
| fa46fe0cf0 | |||
| f0a1efa1f2 | |||
| 71c1058e9e | |||
| 7575bd4dfb | |||
| d697ed3384 | |||
| aed70c8a02 | |||
| e82ca0e932 | |||
| 5910b39766 | |||
| ea9e0c9ab4 | |||
| 2e951eb952 | |||
| 2497611abd | |||
| 90bf56d9ba | |||
| e1b73ef0ef | |||
| bcb5fd7810 | |||
| 40c5145a13 | |||
| e304c3ad31 | |||
| 0494bd9677 | |||
| 6d4f45c586 | |||
| 0f9030e33b | |||
| fc0cdfabb9 | |||
| ab0b44c8a2 | |||
| 37e9a0f4fe | |||
| 2dac137801 | |||
| 8ffa6fcf47 | |||
| 8824e6e2c2 | |||
| 0787eabdab | |||
| 0a34a37d65 | |||
| 5dbfe539ac | |||
| 280976f77c | |||
| 4c53f4359d | |||
| 0a547a14ea | |||
| 206ec8776a | |||
| c3b43d573e | |||
| d1453e1139 | |||
| 5b5dcb3c6d | |||
| 4f467d199e | |||
| a6d20c0cc4 | |||
| d5d942bb2e | |||
| 9e2c7e653f | |||
| 200c0bf773 | |||
| afb6befce6 | |||
| f42efbade2 | |||
| 968aa710ee | |||
| 61b03dc60c | |||
| 381b0d379a | |||
| f48b5b9d92 | |||
| e87597b8d7 | |||
| b12dddc2f0 | |||
| f3076ba038 | |||
| 908a001ed8 | |||
| 1a8c47888a | |||
| 5e46a759a6 | |||
| 0e3f8cba8d | |||
| 0ce12c9297 | |||
| 1f47aadc94 | |||
| bb3cd7e49c | |||
| edd01d44de | |||
| 52b899d070 | |||
| 008292df0f | |||
| 87bfd182d7 | |||
| 8fcc622b8a | |||
| 9415878f57 | |||
| 919ac41a74 | |||
| 80093f3777 | |||
| 28f5d6e91c | |||
| 550779deee | |||
| e2cf60f0c1 | |||
| 546a042d40 | |||
| ee8433655d | |||
| 86b89f2153 | |||
| 34f4aa430a | |||
| 67678cb7f1 | |||
| f05b0d946d | |||
| 486651e0b7 | |||
| 5885924bb7 | |||
| 29434d623c | |||
| 092705ae68 | |||
| 5f777691cf | |||
| 46913422b3 | |||
| ebcdabff07 | |||
| ead849625d | |||
| 2cfa5201ca | |||
| 8dafcc6b88 | |||
| 626bb8b011 | |||
| 721f74f045 | |||
| f52aafdd6a | |||
| b0ae0a2c82 | |||
| 4d688170e0 | |||
| 322a2179e5 | |||
| 31ace7c941 | |||
| 8014980b88 | |||
| 65237cab65 | |||
| 155827528a | |||
| fb15435296 | |||
| 6bba3ebeeb | |||
| 3343f3fb35 | |||
| 1b451d45cf | |||
| f5f676cf51 | |||
| 1dd477d382 | |||
| 96d05300bb | |||
| 0073a4c0a2 | |||
| d3a5071d9d | |||
| 471bd34fc6 | |||
| 44cc12763b | |||
| b68aca04ae | |||
| dd22264877 | |||
| cc6ef0268f | |||
| feecb89c8e | |||
| cb5c488cfd | |||
| b68e8dedd2 | |||
| 0975eb455e | |||
| df0e8bba8c | |||
| 71d5239823 | |||
| 92ae79f64e | |||
| 9c1f636cd4 | |||
| 23c60b38fd | |||
| 89c039252b | |||
| 612bd0b0a0 | |||
| 3414d3c7ef | |||
| 6c1b3bcbdc | |||
| ceba8d51c5 | |||
| 0c3fbfa2b8 | |||
| 05805bb557 | |||
| 3edb006c35 | |||
| 60d85b4259 | |||
| e8a382b02a | |||
| 2b3ec66017 | |||
| b36cd6442e | |||
| fbd6755666 | |||
| ffdee45cf9 | |||
| 51bf4951b3 | |||
| 9c8ae71323 | |||
| 8a95ec130f | |||
| b2f5e4b11c | |||
| 91a2504629 | |||
| 1eaf110d3d | |||
| 9688db67b6 |
@@ -34,11 +34,6 @@ jobs:
|
||||
lfs: true
|
||||
token: ${{ secrets.BUILD_REPO_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
brew update
|
||||
@@ -72,6 +67,10 @@ jobs:
|
||||
# We don't use gmpcxx, but it comes preinstalled on macos runner as arm64 bottle
|
||||
# and CGAL detects it and breaks cross compilation.
|
||||
brew uninstall --ignore-dependencies gmp
|
||||
# Otherwise Python will fallback to use arm64 `pkg-config`,
|
||||
# will pick up arm64 libraries ('zstd' in particular),
|
||||
# and break the build.
|
||||
/usr/local/bin/brew install pkg-config
|
||||
# Required by Python.
|
||||
/usr/local/bin/brew install gettext openssl
|
||||
fi
|
||||
|
||||
@@ -14,33 +14,54 @@ jobs:
|
||||
submodules: recursive
|
||||
path: IfcOpenShell
|
||||
|
||||
- name: Checkout Pyodide
|
||||
- name: Checkout Build Repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
repository: pyodide/pyodide
|
||||
ref: '0.28.0a3'
|
||||
repository: IfcOpenShell/build-outputs
|
||||
path: ifcopenshell_build
|
||||
ref: wasm
|
||||
lfs: true
|
||||
token: ${{ secrets.BUILD_REPO_TOKEN }}
|
||||
path: pyodide
|
||||
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
cd ifcopenshell_build
|
||||
python ../IfcOpenShell/pyodide/cache_dependencies.py unpack
|
||||
|
||||
- name: ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
VERSION=`cat IfcOpenShell/VERSION`
|
||||
sed -i s/0.8.0/$VERSION/g IfcOpenShell/pyodide/meta.yaml
|
||||
sed -i s/0.8.0/$VERSION/g IfcOpenShell/pyodide/setup.py
|
||||
echo '#!/usr/bin/bash' > script.sh
|
||||
echo 'cd pyodide' >> script.sh
|
||||
echo 'make' >> script.sh
|
||||
echo 'cd ..' >> script.sh
|
||||
echo 'mkdir -p packages/ifcopenshell' >> script.sh
|
||||
echo 'cp IfcOpenShell/pyodide/meta.yaml packages/ifcopenshell' >> script.sh
|
||||
echo 'PYODIDE_ROOT=/src/pyodide \' >> script.sh
|
||||
echo 'PATH=/src/pyodide/emsdk/emsdk:/src/pyodide/emsdk/emsdk/node/22.16.0_64bit/bin:/src/pyodide/emsdk/emsdk/upstream/emscripten:$PATH \' >> script.sh
|
||||
echo 'pyodide build-recipes ifcopenshell --install' >> script.sh
|
||||
chmod +x script.sh
|
||||
sed -i s/--tty// pyodide/run_docker
|
||||
pyodide/run_docker ./script.sh
|
||||
mv dist/ifcopenshell-$VERSION-py3-none-any.whl dist/ifcopenshell-$VERSION+${GITHUB_SHA:0:7}-cp313-cp313-emscripten_4_0_9_wasm32.whl
|
||||
./IfcOpenShell/pyodide/build_pyodide.sh
|
||||
FILE=`echo dist/ifcopenshell-*.whl`
|
||||
NEW_FILE=`echo $FILE | sed "s/-/+${GITHUB_SHA:0:7}-/2"`
|
||||
mv $FILE $NEW_FILE
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-logs-pyodide
|
||||
path: |
|
||||
ifcopenshell_build/*/*/logs/*.log
|
||||
retention-days: 30
|
||||
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
cd ifcopenshell_build
|
||||
python ../IfcOpenShell/pyodide/cache_dependencies.py pack
|
||||
|
||||
- name: Commit and Push Changes to Build Repository
|
||||
run: |
|
||||
cd ifcopenshell_build
|
||||
git config user.name "IfcOpenBot"
|
||||
git config user.email "ifcopenbot@ifcopenshell.org"
|
||||
git add */*/install/cache-*.tar.gz
|
||||
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
|
||||
git push || echo "Push failed"
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
|
||||
@@ -48,8 +48,8 @@ jobs:
|
||||
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
|
||||
|
||||
- name: ccache
|
||||
# Using fork to support Rocky.
|
||||
uses: Andrej730/ccache-action@main
|
||||
# Latest release (1.2.19) doesn't support Rocky, so using a specific commit.
|
||||
uses: hendrikmuhs/ccache-action@eca11c308176d48942455b9e5b1b70ff6950a778
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ jobs:
|
||||
[ -n "$install_root" ] && find "$install_root" -type f -name 'cache-*.tar.gz' -maxdepth 1 -exec tar -xzf {} -C "$install_root" \; || true
|
||||
|
||||
- name: ccache
|
||||
# Using fork to support Rocky.
|
||||
uses: Andrej730/ccache-action@main
|
||||
# Latest release (1.2.19) doesn't support Rocky, so using a specific commit.
|
||||
uses: hendrikmuhs/ccache-action@eca11c308176d48942455b9e5b1b70ff6950a778
|
||||
with:
|
||||
key: ubuntu-22.04-${{ runner.arch }}-rockylinux8
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python: ['3.9.11', '3.10.3', '3.11.8', '3.12.1', '3.13.0']
|
||||
arch: ['x64']
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
@@ -30,14 +29,6 @@ jobs:
|
||||
run: |
|
||||
choco install -y sed 7zip.install awscli
|
||||
|
||||
- name: Install Python
|
||||
run: |
|
||||
$installer = "python-${{ matrix.python }}-amd64.exe"
|
||||
$url = "https://www.python.org/ftp/python/${{ matrix.python }}/$installer"
|
||||
Invoke-WebRequest -Uri $url -OutFile $installer
|
||||
Start-Process -Wait -FilePath .\$installer -ArgumentList '/quiet InstallAllUsers=0 PrependPath=0 Include_test=0 TargetDir=C:\Python\${{ matrix.python }}'
|
||||
Remove-Item .\$installer
|
||||
|
||||
- name: Unpack Dependencies
|
||||
run: |
|
||||
cd _deps-vs2022-x64-installed
|
||||
@@ -45,21 +36,21 @@ jobs:
|
||||
7z x $_.FullName
|
||||
}
|
||||
|
||||
- name: Run Build Script
|
||||
- name: ccache
|
||||
# Use fork to resolve cache misses / duplicated cache entries on Windows.
|
||||
uses: Andrej730/ccache-action@main
|
||||
with:
|
||||
key: win-${{ matrix.arch }}
|
||||
# Windows ccache needs ~1GB
|
||||
# and with default 500MB some cache gets deleted, leading to misses.
|
||||
max-size: 5000MB
|
||||
|
||||
- name: Run Build Script And Pack .zip Archives
|
||||
shell: cmd
|
||||
run: |
|
||||
setlocal EnableDelayedExpansion
|
||||
SET PYTHON_VERSION=${{ matrix.python }}
|
||||
for /f "tokens=1,2,3 delims=." %%a in ("%PYTHON_VERSION%") do (
|
||||
set PY_VER_MAJOR_MINOR=%%a%%b
|
||||
)
|
||||
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
|
||||
SET IFCOS_INSTALL_PYTHON=FALSE
|
||||
cd win
|
||||
echo y | call build-deps.cmd vs2022-x64 Release
|
||||
SET PYTHONHOME=C:\Python\${{ matrix.python }}
|
||||
call run-cmake.bat vs2022-x64 -DENABLE_BUILD_OPTIMIZATIONS=On -DGLTF_SUPPORT=ON -DADD_COMMIT_SHA=ON -DVERSION_OVERRIDE=ON
|
||||
call install-ifcopenshell.bat vs2022-x64 Release
|
||||
python build-all-win.py
|
||||
|
||||
- name: Pack Dependencies
|
||||
run: |
|
||||
@@ -81,35 +72,6 @@ jobs:
|
||||
git commit -m "Update build artifacts [skip ci]" || echo "No changes to commit"
|
||||
git push || echo "Push failed"
|
||||
|
||||
- name: Package .zip Archives
|
||||
run: |
|
||||
$VERSION = 'v' + ((Get-Content VERSION).Trim())
|
||||
$SHA = ${env:GITHUB_SHA}.Substring(0, 7)
|
||||
$OUTPUT_DIR = "$env:USERPROFILE\output"
|
||||
New-Item -ItemType Directory -Force -Path $OUTPUT_DIR
|
||||
|
||||
if ("${{ matrix.python }}" -eq "3.9.11") {
|
||||
# only for the first python version the executables are assembled for upload
|
||||
cd _installed-vs2022-x64/bin
|
||||
Get-ChildItem -Path . | ForEach-Object {
|
||||
echo $_
|
||||
$exe = $_.Name
|
||||
$baseName = $exe.Substring(0, $exe.Length - 4)
|
||||
$zipName = "${baseName}-$VERSION-$SHA-win64.zip"
|
||||
7z a $zipName $exe
|
||||
}
|
||||
mv *.zip $OUTPUT_DIR
|
||||
}
|
||||
|
||||
$pyVersion = "${{ matrix.python }}"
|
||||
$pyVersionMajor = ($pyVersion -split '\.')[0..1] -join ''
|
||||
cd C:\Python\${{ matrix.python }}\Lib\site-packages
|
||||
Remove-Item -Recurse -Force ifcopenshell\__pycache__ -ErrorAction SilentlyContinue
|
||||
Get-ChildItem -Path ifcopenshell -Filter "*.pyc" -Recurse | Remove-Item -Force
|
||||
$zipName = "ifcopenshell-python-$pyVersionMajor-$VERSION-$SHA-win64.zip"
|
||||
7z a $zipName ifcopenshell
|
||||
mv $zipName $OUTPUT_DIR
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py39, py310, py311, py312, py313]
|
||||
pyver: [py39, py310, py311, py312, py313, py314]
|
||||
config:
|
||||
- {
|
||||
name: "Windows 64bit",
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pyver: [py39, py310, py311, py312, py313]
|
||||
pyver: [py39, py310, py311, py312, py313, py314]
|
||||
config:
|
||||
- {
|
||||
name: "Windows 64bit",
|
||||
|
||||
+19
-14
@@ -50,6 +50,7 @@ jobs:
|
||||
pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely
|
||||
pip install src/bcf --no-deps
|
||||
pip install git+https://github.com/zdhoward/aud
|
||||
pip install pytest-xdist==3.8.0
|
||||
|
||||
- name: Install C++ dependencies
|
||||
run: |
|
||||
@@ -113,6 +114,7 @@ jobs:
|
||||
-DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \
|
||||
-DPYTHON_LIBRARY:FILEPATH=${{ env.pythonLocation }}/lib/libpython3.11.so \
|
||||
-DCOLLADA_SUPPORT=Off \
|
||||
-DUSE_MMAP=On \
|
||||
"-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \
|
||||
-DGLTF_SUPPORT=On \
|
||||
-DWITH_ROCKSDB=On \
|
||||
@@ -120,20 +122,23 @@ jobs:
|
||||
sudo make -j $(nproc)
|
||||
sudo make install
|
||||
|
||||
- name: Run IfcConvert on Sample files
|
||||
run: |
|
||||
(find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \
|
||||
echo $i | tee -a log; \
|
||||
timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \
|
||||
echo $i $? >> statuses; \
|
||||
done) || true
|
||||
echo Failed
|
||||
grep -v 0$ statuses
|
||||
grep -v 0$ statuses | wc -l
|
||||
echo Succeeded
|
||||
grep 0$ statuses
|
||||
grep 0$ statuses | wc -l
|
||||
# - name: Run IfcConvert on Sample files
|
||||
# run: |
|
||||
# (find test/input src/bonsai/test/files -name '*.ifc' | while read i; do \
|
||||
# echo $i | tee -a log; \
|
||||
# timeout 1m "$(which IfcConvert)" -yv "$i" "$i.obj" --validate >> log 2>&1; \
|
||||
# echo $i $? >> statuses; \
|
||||
# done) || true
|
||||
# echo Failed
|
||||
# grep -v 0$ statuses
|
||||
# grep -v 0$ statuses | wc -l
|
||||
# echo Succeeded
|
||||
# grep 0$ statuses
|
||||
# grep 0$ statuses | wc -l
|
||||
|
||||
- name: Run IfcConvert on Sample file
|
||||
run: |
|
||||
IfcConvert test/input/acad2010_walls.ifc test/input/acad2010_walls.obj
|
||||
|
||||
- name: Test ifcopenshell-python
|
||||
run: |
|
||||
@@ -143,7 +148,7 @@ jobs:
|
||||
mv ifcopenshell ifcopenshell-local # Force testing on installed module
|
||||
pip install -e ../ifcpatch --no-deps # Needed for sql.py tests.
|
||||
ERROR=0
|
||||
make test || ERROR=1
|
||||
make test-parallel || ERROR=1
|
||||
cd ../bcf && make test || ERROR=1
|
||||
pip install requests
|
||||
cd ../bsdd && make test || ERROR=1
|
||||
|
||||
+4
-1
@@ -109,4 +109,7 @@ src/bonsai/bonsai/bim/schema/Brick.ttl
|
||||
bonsaiDecoratorForLoads.code-workspace
|
||||
dev_environment.bat
|
||||
|
||||
.pixi/
|
||||
.pixi/
|
||||
|
||||
src/ifcopenshell-python/ifcopenshell/express/*.exp
|
||||
src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
|
||||
|
||||
@@ -37,25 +37,25 @@ Contents
|
||||
|
||||
| Name | Description | License | Service |
|
||||
| ------------------------- | --------------------------------------------------------------------- | ------------------- | ------- |
|
||||
| bcf | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [](https://pypi.org/project/bcf-client/) [](https://anaconda.org/conda-forge/bcf-client) |
|
||||
| bonsai | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [](https://bonsaibim.org/download.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
|
||||
| bsdd | Library to query the bSDD API | LGPL-3.0-or-later | [](https://pypi.org/project/bsdd/) |
|
||||
| ifc2ca | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
|
||||
| ifc4d | Convert to and from IFC and project management software | LGPL-3.0-or-later | [](https://pypi.org/project/ifc4d/) |
|
||||
| ifc5d | Report and optimise cost information from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifc5d/) |
|
||||
| ifcbimtester | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later |
|
||||
| [bcf](https://docs.ifcopenshell.org/bcf.html) | Library to read and write BCF-XML and query OpenCDE BCF-API modules | LGPL-3.0-or-later | [](https://pypi.org/project/bcf-client/) [](https://anaconda.org/conda-forge/bcf-client) |
|
||||
| [bonsai](https://docs.ifcopenshell.org/bonsai.html) | Add-on to Blender providing a graphical native IFC authoring platform | GPL-3.0-or-later | [](https://bonsaibim.org/download.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=bonsai&expanded=true) [](https://community.chocolatey.org/packages/blenderbim-nightly/) |
|
||||
| [bsdd](https://docs.ifcopenshell.org/bsdd.html) | Library to query the bSDD API | LGPL-3.0-or-later | [](https://pypi.org/project/bsdd/) |
|
||||
| [ifc2ca](https://docs.ifcopenshell.org/ifc2ca.html) | Utility to convert IFC structural analysis models to Code_Aster | LGPL-3.0-or-later |
|
||||
| [ifc4d](https://docs.ifcopenshell.org/ifc4d.html) | Convert to and from IFC and project management software | LGPL-3.0-or-later | [](https://pypi.org/project/ifc4d/) |
|
||||
| [ifc5d](https://docs.ifcopenshell.org/ifc5d.html) | Report and optimise cost information from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifc5d/) |
|
||||
| [ifcbimtester](https://docs.ifcopenshell.org/bimtester.html) | Wrapper for Gherkin based unit testing for IFC models | LGPL-3.0-or-later |
|
||||
| ifcblender | Historic Blender IFC import add-on | LGPL-3.0-or-later\* |
|
||||
| ifccityjson | Convert CityJSON to IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccityjson/) |
|
||||
| ifcclash | Clash detection library and CLI app | LGPL-3.0-or-later | [](https://pypi.org/project/ifcclash/) |
|
||||
| ifcconvert | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcconvert/installation.html) [](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 | [](https://pypi.org/project/ifccsv/) |
|
||||
| ifcdiff | Compare changes between IFC models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcdiff/) |
|
||||
| ifcfm | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [](https://pypi.org/project/ifcfm/) |
|
||||
| ifcmax | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcmax.html)
|
||||
| ifcopenshell-python | Python library for IFC manipulation | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [](https://pypi.org/project/ifcopenshell/) [](https://anaconda.org/conda-forge/ifcopenshell) [](https://anaconda.org/ifcopenshell/ifcopenshell) [](https://hub.docker.com/r/aecgeeks/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell-git) |
|
||||
| ifcpatch | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [](https://pypi.org/project/ifcpatch/) |
|
||||
| ifcsverchok | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
|
||||
| ifctester | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [](https://pypi.org/project/ifctester/) |
|
||||
| [ifccityjson](https://docs.ifcopenshell.org/ifccityjson.html) | Convert CityJSON to IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccityjson/) |
|
||||
| [ifcclash](https://docs.ifcopenshell.org/ifcclash.html) | Clash detection library and CLI app | LGPL-3.0-or-later | [](https://pypi.org/project/ifcclash/) |
|
||||
| [ifcconvert](https://docs.ifcopenshell.org/ifcconvert.html) | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcconvert/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
|
||||
| [ifccsv](https://docs.ifcopenshell.org/ifccsv.html) | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [](https://pypi.org/project/ifccsv/) |
|
||||
| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [](https://pypi.org/project/ifcdiff/) |
|
||||
| [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [](https://pypi.org/project/ifcfm/) |
|
||||
| [ifcmax](https://docs.ifcopenshell.org/ifcmax.html) | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcmax.html)
|
||||
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [](https://pypi.org/project/ifcopenshell/) [](https://anaconda.org/conda-forge/ifcopenshell) [](https://anaconda.org/ifcopenshell/ifcopenshell) [](https://hub.docker.com/r/aecgeeks/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell) [](https://aur.archlinux.org/packages/ifcopenshell-git) |
|
||||
| [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [](https://pypi.org/project/ifcpatch/) |
|
||||
| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
|
||||
| [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [](https://pypi.org/project/ifctester/) |
|
||||
|
||||
The IfcOpenShell C++ codebase is split into multiple interal libraries:
|
||||
|
||||
|
||||
+15
-15
@@ -27,12 +27,6 @@ endif()
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON) # not necessary, but encouraged
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
find_program(CCACHE_FOUND ccache)
|
||||
if(CCACHE_FOUND)
|
||||
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
|
||||
message(STATUS "`ccache` is found, using it as a compiler launcher.")
|
||||
endif()
|
||||
|
||||
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
|
||||
|
||||
if(POLICY CMP0141) # 3.25+
|
||||
@@ -127,8 +121,10 @@ if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND(NOT BUILD_IFCGEOM)
|
||||
set(BUILD_IFCGEOM ON)
|
||||
endif()
|
||||
|
||||
find_program(CCACHE_FOUND ccache)
|
||||
if(CCACHE_FOUND)
|
||||
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
|
||||
message(STATUS "`ccache` is found, using it as a compiler launcher.")
|
||||
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_FOUND}")
|
||||
if(MSVC)
|
||||
# By default Visual Studio generators will use /Zi which is not compatible
|
||||
# with ccache, so tell Visual Studio to use /Z7 instead.
|
||||
@@ -212,6 +208,8 @@ if(NOT MINIMAL_BUILD)
|
||||
UNIFY_ENVVARS_AND_CACHE(LIBXML2_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(LIBXML2_LIBRARIES)
|
||||
UNIFY_ENVVARS_AND_CACHE(PCRE_LIBRARY_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(PYTHON_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(PYTHON_LIBRARY)
|
||||
UNIFY_ENVVARS_AND_CACHE(PYTHON_EXECUTABLE)
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_INCLUDE_DIR)
|
||||
UNIFY_ENVVARS_AND_CACHE(HDF5_LIBRARY_DIR)
|
||||
@@ -428,7 +426,7 @@ if(WASM_BUILD)
|
||||
else()
|
||||
# @todo review this, shouldn't this be all possible header-only now?
|
||||
# ... or rewritten using C++17 features?
|
||||
set(BOOST_COMPONENTS system program_options regex thread date_time)
|
||||
set(BOOST_COMPONENTS system program_options regex thread date_time iostreams)
|
||||
endif()
|
||||
|
||||
if(USE_MMAP)
|
||||
@@ -453,8 +451,11 @@ if(NOT MINIMAL_BUILD)
|
||||
clear_wasm_sysroot()
|
||||
if(IFCXML_SUPPORT)
|
||||
if((NOT LIBXML2_INCLUDE_DIR AND NOT LIBXML2_LIBRARIES))
|
||||
# First try config mode (probably works with vcpkg, Conan, macOS brew installs, but not on ubuntu 22.04?)
|
||||
find_package(LibXml2 QUIET CONFIG)
|
||||
# First try config mode (probably works with vcpkg, Conan, macOS brew installs, but not on ubuntu 22.04)
|
||||
# CONFIG is provided using root path, so no need to clear sysroot here.
|
||||
restore_wasm_sysroot()
|
||||
find_package(LibXml2 REQUIRED CONFIG)
|
||||
clear_wasm_sysroot()
|
||||
|
||||
if(NOT LibXml2_FOUND)
|
||||
# Fallback to CMake's builtin FindLibXml2 module (works on Ubuntu)
|
||||
@@ -1043,11 +1044,7 @@ if(BUILD_IFCGEOM)
|
||||
foreach(schema ${SCHEMA_VERSIONS})
|
||||
set(IFCGEOM_SCHEMA_LIBRARIES ${IFCGEOM_SCHEMA_LIBRARIES} geometry_mapping_ifc${schema})
|
||||
endforeach()
|
||||
if(WASM_BUILD)
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
|
||||
else()
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES} IfcGeom)
|
||||
endif()
|
||||
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} IfcGeom ${IFCGEOM_SCHEMA_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
|
||||
@@ -1177,6 +1174,9 @@ if(BUILD_IFCGEOM)
|
||||
add_library(geometry_mapping_ifc${schema} STATIC ${IFCGEOM_FILES})
|
||||
set_target_properties(geometry_mapping_ifc${schema} PROPERTIES COMPILE_FLAGS "-DIFC_GEOM_EXPORTS -DIfcSchema=Ifc${schema}")
|
||||
target_link_libraries(geometry_mapping_ifc${schema} IfcParse)
|
||||
if (NOT BUILD_SHARED_LIBS)
|
||||
target_link_libraries(geometry_mapping_ifc${schema} IfcGeom)
|
||||
endif()
|
||||
list(APPEND mapping_libraries geometry_mapping_ifc${schema})
|
||||
endforeach()
|
||||
|
||||
|
||||
+160
-110
@@ -18,6 +18,8 @@
|
||||
# #
|
||||
###############################################################################
|
||||
|
||||
# ruff: noqa: UP035
|
||||
|
||||
"""
|
||||
Example usage:
|
||||
# Build all targets by default.
|
||||
@@ -31,6 +33,7 @@ Available arguments:
|
||||
``-py-313`` - build for specific Python version
|
||||
(building for all supported Python version by default).
|
||||
``-wasm`` - compile for wasm
|
||||
``-without-xxx`` - do not build dependency ``xxx`` (e.g. ``--without-swig``)
|
||||
``-mac-cross-compile-intel`` - cross compile for Intel Mac on Apple Silicon host
|
||||
``-shared`` - build shared libraries. By default will build static.
|
||||
``-diskcleanup`` - clean up build directories after finishing building dependencies
|
||||
@@ -50,6 +53,13 @@ Used environment variables:
|
||||
- ``IFCOS_SCHEMAS`` - schemas to be built; defaults to cmake default (IFC2X3; IFC4; IFC4X3_ADD2) - to be supplied as `2x3;4`
|
||||
- ``USE_OCCT`` - whether to use official Open CASCADE instead of Community Edition
|
||||
(`true` by default, any other value is considered `false`)
|
||||
- ``WASM_PYTHON_PATH`` - path to WASM Python installation,
|
||||
used to deduce `PYVERSION` (e.g. '3.13.2'), `PYTHONINCLUDE`,
|
||||
`SIDE_MODULE_CFLAGS`, `SIDE_MODULE_LDFLAGS`.
|
||||
Allows to build wasm without pyodide build environment, which can be useful for debugging build issues.
|
||||
Example value: 'pyodide/cpython/installs/python-3.13.2'
|
||||
- ``WASM_TOOLCHAIN_FILE`` - path to emscripten toolchain file from pyodide ('Emscripten.cmake')
|
||||
needed only if ``WASM_PYTHON_PATH`` is provided.
|
||||
- ``ADD_COMMIT_SHA`` - if defined with any non-empty value then
|
||||
`ADD_COMMIT_SHA` and `VERSION_OVERRIDE` will be set to `ON` while configuring IfcOpenShell
|
||||
|
||||
@@ -133,7 +143,7 @@ PROJECT_NAME = "IfcOpenShell"
|
||||
USE_CURRENT_PYTHON_VERSION = os.getenv("USE_CURRENT_PYTHON_VERSION")
|
||||
ADD_COMMIT_SHA = os.getenv("ADD_COMMIT_SHA")
|
||||
|
||||
PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1", "3.13.6"]
|
||||
PYTHON_VERSIONS = ["3.9.11", "3.10.3", "3.11.8", "3.12.1", "3.13.6", "3.14.0"]
|
||||
JSON_VERSION = "3.11.3"
|
||||
OCE_VERSION = "0.18.3"
|
||||
OCCT_VERSION = "7.8.1"
|
||||
@@ -146,7 +156,7 @@ SWIG_VERSION = "4.1.0"
|
||||
OPENCOLLADA_VERSION = "v1.6.68"
|
||||
HDF5_VERSION = "1.13.1"
|
||||
|
||||
GMP_VERSION = "6.2.1"
|
||||
GMP_VERSION = "6.3.0"
|
||||
MPFR_VERSION = "3.1.6" # latest is 4.1.0
|
||||
CGAL_VERSION = "v5.6.3"
|
||||
USD_VERSION = "23.05"
|
||||
@@ -193,19 +203,40 @@ def cecho(message, color=NO_COLOR):
|
||||
logger.info(f"{color}{message}\033[0m")
|
||||
|
||||
|
||||
def which(cmd: str) -> Union[str, None]:
|
||||
PATH = os.getenv("PATH")
|
||||
assert PATH
|
||||
for path in PATH.split(":"):
|
||||
if os.path.exists(path) and cmd in os.listdir(path):
|
||||
return cmd
|
||||
return None
|
||||
|
||||
|
||||
# Flags.
|
||||
APPLE = platform.system() == "Darwin"
|
||||
MAC_CROSS_COMPILE_INTEL = "mac-cross-compile-intel" in flags
|
||||
assert platform.system() == "Darwin" or not MAC_CROSS_COMPILE_INTEL
|
||||
|
||||
WASM = "wasm" in flags
|
||||
"""Build WASM outside pyodide build environment."""
|
||||
WASM_DEBUG = False
|
||||
if WASM:
|
||||
if "WASM_PYTHON_PATH" in os.environ:
|
||||
wasm_python_path = os.environ["WASM_PYTHON_PATH"]
|
||||
# Deduce version from path, assuming format .../python-X.Y.Z
|
||||
version_match = re.search(r"python-(\d+)\.(\d+)\.(\d+)", wasm_python_path)
|
||||
assert version_match, f"Could not deduce python version from '{wasm_python_path}'"
|
||||
python_version = version_match.group(1)
|
||||
os.environ["PYVERSION"] = python_version
|
||||
os.environ["PYTHONINCLUDE"] = f"{wasm_python_path}/include/python{python_version.rpartition('.')[0]}"
|
||||
os.environ["SIDE_MODULE_CFLAGS"] = ""
|
||||
# Required, otherwise library will compile as .a, not .so.
|
||||
os.environ["SIDE_MODULE_LDFLAGS"] = "-s SIDE_MODULE=1"
|
||||
# We're outside pyodide build environment, so need to provide toolchain file ourselves.
|
||||
assert "WASM_TOOLCHAIN_FILE" in os.environ, "WASM_TOOLCHAIN_FILE must be set when WASM_PYTHON_PATH is provided"
|
||||
WASM_DEBUG = True
|
||||
required_vars = (
|
||||
# E.g. '3.13.2'.
|
||||
"PYVERSION",
|
||||
# 'include' folder in WASM-Python installation.
|
||||
# e.g. '/pyodide/cpython/installs/python-3.13.2/include/python3.13'
|
||||
"PYTHONINCLUDE",
|
||||
"SIDE_MODULE_CFLAGS",
|
||||
"SIDE_MODULE_LDFLAGS",
|
||||
)
|
||||
missing_vars = [v for v in required_vars if v not in os.environ]
|
||||
assert not missing_vars, f"Some variables required for WASM compilation are missing: {', '.join(missing_vars)}"
|
||||
|
||||
# Set defaults for missing empty environment variables
|
||||
|
||||
@@ -221,9 +252,11 @@ if platform.system() == "Darwin":
|
||||
|
||||
IFCOS_NUM_BUILD_PROCS = os.getenv("IFCOS_NUM_BUILD_PROCS", multiprocessing.cpu_count() + 1)
|
||||
|
||||
CMAKE_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "cmake"))
|
||||
SCRIPT_PATH = Path(__file__).parent
|
||||
REPO_PATH = SCRIPT_PATH.parent
|
||||
CMAKE_DIR = (REPO_PATH / "cmake").resolve().__str__()
|
||||
|
||||
build_dir = os.environ.get("BUILD_DIR", os.path.join(os.path.dirname(__file__), "..", "build"))
|
||||
BUILD_DIR = os.environ.get("BUILD_DIR", (REPO_PATH / "build").__str__())
|
||||
|
||||
|
||||
if WASM:
|
||||
@@ -232,7 +265,7 @@ elif MAC_CROSS_COMPILE_INTEL:
|
||||
arch = "x86_64"
|
||||
else:
|
||||
arch = platform.machine()
|
||||
DEFAULT_DEPS_DIR = Path(build_dir) / platform.system() / arch
|
||||
DEFAULT_DEPS_DIR = Path(BUILD_DIR) / platform.system() / arch
|
||||
|
||||
if TOOLSET:
|
||||
DEFAULT_DEPS_DIR = DEFAULT_DEPS_DIR / TOOLSET
|
||||
@@ -264,6 +297,7 @@ if USE_OCCT:
|
||||
cecho(" - Compiling against official Open Cascade")
|
||||
else:
|
||||
cecho(" - Compiling against Open Cascade Community Edition")
|
||||
cecho(f"* Build Directory = {BUILD_DIR}", MAGENTA)
|
||||
cecho(f"* Dependency Directory = {DEPS_DIR}", MAGENTA)
|
||||
cecho(f" - The directory where {PROJECT_NAME} dependencies are installed.")
|
||||
cecho(f"* Build Config Type = {BUILD_CFG}", MAGENTA)
|
||||
@@ -280,6 +314,11 @@ cecho(
|
||||
""" - How many compiler processes may be run in parallel.
|
||||
"""
|
||||
)
|
||||
cecho(f" * IFCOS_SCHEMAS = '{os.environ.get('IFCOS_SCHEMAS')}'", MAGENTA)
|
||||
cecho(
|
||||
""" - IFC Schemas to compile. If not provided, fallback to default provided in cmake.
|
||||
"""
|
||||
)
|
||||
|
||||
dependency_tree: "dict[str, tuple[str, ...]]" = {
|
||||
"IfcParse": ("boost", "libxml2", "hdf5", "rocksdb"),
|
||||
@@ -355,6 +394,23 @@ else:
|
||||
targets = set(dependency_tree.keys())
|
||||
|
||||
targets = set(t for t in targets if "without-%s" % t.lower() not in flags)
|
||||
if WASM:
|
||||
SKIP_TARGETS_FOR_WASM = {
|
||||
"hdf5",
|
||||
"rocksdb",
|
||||
"opencollada",
|
||||
"swig",
|
||||
"pcre",
|
||||
"pcre2",
|
||||
"IfcGeom",
|
||||
"IfcConvert",
|
||||
"IfcGeomServer",
|
||||
}
|
||||
SKIP_TARGETS_FOR_WASM = {t.lower() for t in SKIP_TARGETS_FOR_WASM}
|
||||
skip_targets = {t for t in targets if t.lower() in SKIP_TARGETS_FOR_WASM}
|
||||
if skip_targets:
|
||||
cecho(f"Skipping targets for wasm build: {', '.join(sorted(skip_targets))}", YELLOW)
|
||||
targets.difference_update(skip_targets)
|
||||
|
||||
print("Building:", *sorted(targets, key=lambda t: len(list(gather_dependencies(t)))))
|
||||
|
||||
@@ -363,16 +419,20 @@ yacc = "yacc" # Used during swig building process, installed with `bison` on De
|
||||
missing_commands: "list[str]" = []
|
||||
required_commands = [git, bunzip2, tar, cc, cplusplus, autoconf, automake, make, "patch", "cmake", yacc, xz]
|
||||
if "wasm" in flags:
|
||||
required_commands.remove(yacc) # yacc not needed for wasm builds
|
||||
# Skip swig build for WASM.
|
||||
required_commands.append("swig")
|
||||
required_commands.remove(yacc)
|
||||
|
||||
for cmd in required_commands:
|
||||
if which(cmd) is None:
|
||||
if shutil.which(cmd) is None:
|
||||
missing_commands.append(cmd)
|
||||
|
||||
if missing_commands:
|
||||
raise ValueError(f"Required tools not installed or not added to PATH: {', '.join(missing_commands)}")
|
||||
|
||||
MAC_INTEL_BIN_PATH = "/usr/local/bin"
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
brew = "/usr/local/bin/brew"
|
||||
brew = f"{MAC_INTEL_BIN_PATH}/brew"
|
||||
assert os.path.exists(brew), f"For intel cross compilation the brew path is expected to be '{brew}'."
|
||||
|
||||
# identifiers for the download tool (could be less memory consuming as ints, but are more verbose as strings)
|
||||
@@ -396,6 +456,13 @@ except:
|
||||
pass
|
||||
|
||||
|
||||
def restore_env(var_name: str, old_value: Union[str, None]) -> None:
|
||||
if old_value is None:
|
||||
del os.environ[var_name]
|
||||
else:
|
||||
os.environ[var_name] = old_value
|
||||
|
||||
|
||||
def run(cmds: "Sequence[str]", cwd: "Union[str, None]" = None, can_fail: bool = False) -> str:
|
||||
"""
|
||||
Wraps `subprocess.Popen.communicate()` and logs the command being executed,
|
||||
@@ -635,7 +702,7 @@ def build_dependency(
|
||||
if isinstance(patch, str):
|
||||
patch = [patch]
|
||||
for p in patch:
|
||||
patch_abs = os.path.abspath(os.path.join(os.path.dirname(__file__), p))
|
||||
patch_abs = (SCRIPT_PATH / p).absolute().__str__()
|
||||
if os.path.exists(patch_abs):
|
||||
try:
|
||||
run(["patch", "-p1", "--batch", "--forward", "-i", patch_abs], cwd=extract_dir)
|
||||
@@ -722,12 +789,13 @@ LDFLAGS = os.environ.get("LDFLAGS", "")
|
||||
|
||||
ADDITIONAL_ARGS_STR = " ".join(ADDITIONAL_ARGS)
|
||||
|
||||
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
if "wasm" in flags:
|
||||
CFLAGS_MINIMAL = CXXFLAGS_MINIMAL = CFLAGS = CXXFLAGS = os.environ["SIDE_MODULE_CFLAGS"]
|
||||
LDFLAGS = os.environ["SIDE_MODULE_LDFLAGS"]
|
||||
# WASM `SIDE_MODULE_` are absorbed by `emcmake` automatically.
|
||||
CXXFLAGS = CXXFLAGS_MINIMAL
|
||||
CFLAGS = CFLAGS_MINIMAL
|
||||
elif sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev/null"]) != 0:
|
||||
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
if BUILD_STATIC:
|
||||
CXXFLAGS = f"{CXXFLAGS} {PIC} -fdata-sections -ffunction-sections -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
|
||||
CFLAGS = f"{CFLAGS} {PIC} -fdata-sections -ffunction-sections -fvisibility=hidden {ADDITIONAL_ARGS_STR}"
|
||||
@@ -736,8 +804,6 @@ elif sp.call([bash, "-c", "ld --gc-sections 2>&1 | grep -- --gc-sections &> /dev
|
||||
CFLAGS = CFLAGS_MINIMAL
|
||||
LDFLAGS = f"{LDFLAGS} -Wl,--gc-sections {ADDITIONAL_ARGS_STR}"
|
||||
else:
|
||||
CXXFLAGS_MINIMAL = f"{CXXFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
CFLAGS_MINIMAL = f"{CFLAGS} {PIC} {ADDITIONAL_ARGS_STR}"
|
||||
if BUILD_STATIC:
|
||||
CXXFLAGS = f"{CXXFLAGS} {PIC} -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
|
||||
CFLAGS = f"{CFLAGS} {PIC} -fvisibility=hidden -fvisibility-inlines-hidden {ADDITIONAL_ARGS_STR}"
|
||||
@@ -817,9 +883,9 @@ if "eigen" in targets:
|
||||
)
|
||||
|
||||
if "pcre" in targets:
|
||||
OLD_CC, OLD_CCXX = None, None
|
||||
OLD_CC, OLD_CXX = None, None
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
OLD_CC, OLD_CCXX = os.environ.get("CC"), os.environ.get("CXX")
|
||||
OLD_CC, OLD_CXX = os.environ.get("CC"), os.environ.get("CXX")
|
||||
os.environ["CC"] = MAC_CROSS_COMPILE_INTEL_CC
|
||||
os.environ["CXX"] = MAC_CROSS_COMPILE_INTEL_CXX
|
||||
# Keep it autoconf as OpenCOLLADA is pretty old and might break
|
||||
@@ -832,14 +898,8 @@ if "pcre" in targets:
|
||||
download_name=f"pcre-{PCRE_VERSION}.tar.bz2",
|
||||
)
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
if OLD_CC is None:
|
||||
del os.environ["CC"]
|
||||
else:
|
||||
os.environ["CC"] = OLD_CC
|
||||
if OLD_CCXX is None:
|
||||
del os.environ["CXX"]
|
||||
else:
|
||||
os.environ["CXX"] = OLD_CCXX
|
||||
restore_env("CC", OLD_CC)
|
||||
restore_env("CXX", OLD_CXX)
|
||||
|
||||
if "pcre2" in targets:
|
||||
build_dependency(
|
||||
@@ -871,7 +931,7 @@ if "freetype" in targets:
|
||||
download_url="https://github.com/freetype/freetype",
|
||||
download_name="freetype2",
|
||||
download_tool=download_tool_git,
|
||||
revision="VER-2-11-1",
|
||||
revision="VER-2-14-0",
|
||||
)
|
||||
|
||||
if USE_OCCT and "occ" in targets:
|
||||
@@ -1010,6 +1070,7 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
|
||||
# On OSX a dynamic python library is built or it would not be compatible
|
||||
# with the system python because of some threading initialization
|
||||
PYTHON_CONFIGURE_ARGS: "list[str]" = []
|
||||
original_path = ""
|
||||
if platform.system() == "Darwin":
|
||||
PYTHON_CONFIGURE_ARGS = ["--enable-shared"]
|
||||
open_ssl_prefix = run([brew, "--prefix", "openssl@3"]).strip()
|
||||
@@ -1018,6 +1079,10 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
|
||||
PYTHON_CONFIGURE_ARGS.append(f"--with-openssl={open_ssl_prefix}")
|
||||
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
original_path = os.environ["PATH"]
|
||||
# Need to ensure python will pick up intel's `pkg-config`,
|
||||
# otherwise it might attempt to use ARM libraries (e.g. `zstd`) and fail.
|
||||
os.environ["PATH"] = f"{MAC_INTEL_BIN_PATH}{os.pathsep}{original_path}"
|
||||
PYTHON_CONFIGURE_ARGS.extend(["--with-universal-archs=intel-64", "--enable-universalsdk"])
|
||||
|
||||
for PYTHON_VERSION in PYTHON_VERSIONS:
|
||||
@@ -1039,6 +1104,9 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
|
||||
if not os.path.exists(os.path.join(DEPS_DIR, "install", f"python-{PYTHON_VERSION}")):
|
||||
raise e
|
||||
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
assert original_path
|
||||
os.environ["PATH"] = original_path
|
||||
os.environ["CPPFLAGS"] = OLD_CPP_FLAGS
|
||||
os.environ["CXXFLAGS"] = OLD_CXX_FLAGS
|
||||
os.environ["CFLAGS"] = OLD_C_FLAGS
|
||||
@@ -1084,8 +1152,15 @@ if "boost" in targets:
|
||||
if "cgal" in targets:
|
||||
gmp_args: "list[str]" = []
|
||||
mpfr_args: "list[str]" = []
|
||||
if "wasm" in flags:
|
||||
gmp_args.extend(("--disable-assembly", "--host", "none", "--enable-cxx"))
|
||||
|
||||
OLD_HOST_CC = None
|
||||
if WASM:
|
||||
if APPLE:
|
||||
# Override `HOST_CC`, otherwise `emcc` will try to use it's own `clang` which can only build
|
||||
# wasm executables and build will fail.
|
||||
os.environ["HOST_CC"] = "clang"
|
||||
# Disable assembly, otherwise `emcc -c conftest.s` will crash due to assembly mismatch.
|
||||
gmp_args.extend(("--disable-assembly", "--enable-cxx"))
|
||||
mpfr_args.extend(("--host", "none"))
|
||||
|
||||
OLD_CC = None
|
||||
@@ -1100,10 +1175,14 @@ if "cgal" in targets:
|
||||
pre_compile_subs=(
|
||||
[("build/config.h", "HAVE_OBSTACK_VPRINTF 1", "HAVE_OBSTACK_VPRINTF 0")] if "wasm" in flags else []
|
||||
),
|
||||
download_url="https://ftp.gnu.org/gnu/gmp/",
|
||||
# Sometimes ftp.gnu.org is very slow, use ftpmirror.gnu.org as a workaround.
|
||||
download_url="https://ftpmirror.gnu.org/gnu/gmp/",
|
||||
download_name=f"gmp-{GMP_VERSION}.tar.bz2",
|
||||
)
|
||||
|
||||
if WASM and APPLE:
|
||||
restore_env("HOST_CC", OLD_HOST_CC)
|
||||
|
||||
build_dependency(
|
||||
name=f"mpfr-{MPFR_VERSION}",
|
||||
mode="autoconf",
|
||||
@@ -1113,10 +1192,7 @@ if "cgal" in targets:
|
||||
)
|
||||
|
||||
if MAC_CROSS_COMPILE_INTEL:
|
||||
if OLD_CC is None:
|
||||
del os.environ["CC"]
|
||||
else:
|
||||
os.environ["CC"] = OLD_CC
|
||||
restore_env("CC", OLD_CC)
|
||||
|
||||
build_dependency(
|
||||
name=f"cgal-{CGAL_VERSION}",
|
||||
@@ -1247,15 +1323,19 @@ def get_cmake_args_prefix_path(additional_paths: "Sequence[str]" = ()) -> "list[
|
||||
args_prefix_path = cmake_args_prefix_path.copy()
|
||||
args_prefix_path.extend(additional_paths)
|
||||
prefix_path = ";".join(args_prefix_path)
|
||||
return [f"-DCMAKE_PREFIX_PATH={prefix_path}"]
|
||||
if WASM:
|
||||
# `emcmake` is disabling search in PATH, so we provide root paths instead.
|
||||
# Provide '/' to PATH, so it will be combined with provided root paths,
|
||||
# otherwise, depending on environment, it might not search the root path itself.
|
||||
return [f"-DCMAKE_FIND_ROOT_PATH={prefix_path}", "-DCMAKE_PREFIX_PATH=//"]
|
||||
else:
|
||||
return [f"-DCMAKE_PREFIX_PATH={prefix_path}"]
|
||||
|
||||
|
||||
if "wasm" in flags:
|
||||
# Boost is built by the build script so should not be found
|
||||
# inside of the sysroot set by the emscriptem toolchain
|
||||
cmake_args.append("-DWASM_BUILD=On")
|
||||
# set Eigen3 path for WASM to avoid find_package issues
|
||||
cmake_args.append(f"-DEIGEN_DIR={DEPS_DIR}/install/eigen-install-{EIGEN_VERSION}/include/eigen3")
|
||||
|
||||
schemas = os.environ.get("IFCOS_SCHEMAS")
|
||||
if schemas:
|
||||
@@ -1265,26 +1345,9 @@ if "cgal" in targets:
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/cgal-{CGAL_VERSION}")
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/gmp-{GMP_VERSION}")
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/mpfr-{MPFR_VERSION}")
|
||||
if "wasm" in flags:
|
||||
cmake_args.extend(
|
||||
[
|
||||
f"-DCGAL_INCLUDE_DIR={DEPS_DIR}/install/cgal-{CGAL_VERSION}/include",
|
||||
f"-DGMP_INCLUDE_DIR={DEPS_DIR}/install/gmp-{GMP_VERSION}/include",
|
||||
f"-DGMP_LIBRARY_DIR={DEPS_DIR}/install/gmp-{GMP_VERSION}/lib",
|
||||
f"-DMPFR_INCLUDE_DIR={DEPS_DIR}/install/mpfr-{MPFR_VERSION}/include",
|
||||
f"-DMPFR_LIBRARY_DIR={DEPS_DIR}/install/mpfr-{MPFR_VERSION}/lib",
|
||||
]
|
||||
)
|
||||
|
||||
if "occ" in targets and USE_OCCT:
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/occt-{OCCT_VERSION}")
|
||||
if "wasm" in flags:
|
||||
cmake_args.extend(
|
||||
[
|
||||
f"-DOCC_INCLUDE_DIR={DEPS_DIR}/install/occt-{OCCT_VERSION}/include/opencascade",
|
||||
f"-DOCC_LIBRARY_DIR={DEPS_DIR}/install/occt-{OCCT_VERSION}/lib",
|
||||
]
|
||||
)
|
||||
|
||||
elif "occ" in targets:
|
||||
# We don't support find_package for OCE.
|
||||
@@ -1305,13 +1368,6 @@ else:
|
||||
|
||||
if "libxml2" in targets:
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/libxml2-{LIBXML2_VERSION}")
|
||||
if "wasm" in flags:
|
||||
cmake_args.extend(
|
||||
[
|
||||
f"-DLIBXML2_INCLUDE_DIR={DEPS_DIR}/install/libxml2-{LIBXML2_VERSION}/include/libxml2",
|
||||
f"-DLIBXML2_LIBRARIES={DEPS_DIR}/install/libxml2-{LIBXML2_VERSION}/lib/libxml2.{LIBRARY_EXT}",
|
||||
]
|
||||
)
|
||||
|
||||
if "hdf5" in targets:
|
||||
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/hdf5-{HDF5_VERSION}")
|
||||
@@ -1341,7 +1397,7 @@ if "rocksdb" in targets:
|
||||
]
|
||||
)
|
||||
|
||||
if not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets):
|
||||
if not WASM and (not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets)):
|
||||
logger.info("\rConfiguring executables...")
|
||||
|
||||
exec_args = [
|
||||
@@ -1366,7 +1422,10 @@ if "IfcOpenShell-Python" in targets:
|
||||
ADDITIONAL_ARGS = "-Wl,-flat_namespace,-undefined,suppress"
|
||||
|
||||
if "wasm" in flags:
|
||||
ADDITIONAL_ARGS = f"-Wl,-undefined,suppress -sSIDE_MODULE=2 -sEXPORTED_FUNCTIONS=_PyInit__ifcopenshell_wrapper"
|
||||
ADDITIONAL_ARGS = "-Wl,-undefined,suppress"
|
||||
# Override CMAKE_TOOLCHAIN_FILE because by default emscripten doesn't support building shared binaries.
|
||||
if WASM_DEBUG:
|
||||
os.environ["CMAKE_TOOLCHAIN_FILE"] = os.environ["WASM_TOOLCHAIN_FILE"]
|
||||
|
||||
# NOTE: We don't use `CXXFLAGS` for wrappers, so wrapper is compiled with different flags
|
||||
# (e.g. ` -fdata-sections` is missing, which is set by default for executables)
|
||||
@@ -1380,40 +1439,47 @@ if "IfcOpenShell-Python" in targets:
|
||||
os.makedirs(python_dir, exist_ok=True)
|
||||
|
||||
def compile_python_wrapper(
|
||||
python_version: str, python_library: str, python_include: str, python_executable: Union[str, None]
|
||||
python_version: str,
|
||||
python_include: Union[str, None] = None,
|
||||
python_executable: Union[str, None] = None,
|
||||
python_path: Union[Path, None] = None,
|
||||
) -> Union[str, None]:
|
||||
"""
|
||||
:return: Path to module dir if ``python_executable`` was provided, otherwise ``None``.
|
||||
"""
|
||||
assert bool(python_path) ^ bool(python_include)
|
||||
|
||||
logger.info(f"\rConfiguring python {python_version} wrapper...")
|
||||
|
||||
cache_path = os.path.join(python_dir, "CMakeCache.txt")
|
||||
if os.path.exists(cache_path):
|
||||
os.remove(cache_path)
|
||||
|
||||
os.environ["PYTHON_LIBRARY_BASENAME"] = os.path.basename(python_library)
|
||||
|
||||
swig_prefix_paths: list[str] = []
|
||||
prefix_paths: list[str] = []
|
||||
if "swig" in targets:
|
||||
swig_prefix_paths.append(f"{DEPS_DIR}/install/swig")
|
||||
prefix_paths.append(f"{DEPS_DIR}/install/swig")
|
||||
if python_path:
|
||||
# We couldn't just prefix PATH and have to provide all variables explicitly,
|
||||
# see ifcwrap/cmake for the details.
|
||||
python_executable = (Path(python_path) / "bin" / "python3").__str__()
|
||||
python_include = run(
|
||||
[
|
||||
python_executable,
|
||||
"-c",
|
||||
"import sysconfig; print(sysconfig.get_config_var('INCLUDEPY'))",
|
||||
]
|
||||
)
|
||||
|
||||
assert python_include
|
||||
run_cmake(
|
||||
"",
|
||||
cmake_args
|
||||
+ get_cmake_args_prefix_path(swig_prefix_paths)
|
||||
+ get_cmake_args_prefix_path(prefix_paths)
|
||||
+ [
|
||||
"-DPYTHON_LIBRARY=" + python_library,
|
||||
*([f"-DPYTHON_EXECUTABLE={python_executable}"] if python_executable else []),
|
||||
# *([f"-DPYTHON_MODULE_INSTALL_DIR={os.environ['PYTHONPATH']}/ifcopenshell"] if "wasm" in flags else []),
|
||||
*(
|
||||
[
|
||||
"-DPYTHON_MODULE_INSTALL_DIR="
|
||||
+ os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "package"))
|
||||
]
|
||||
if "wasm" in flags
|
||||
else []
|
||||
),
|
||||
"-DPYTHON_INCLUDE_DIR=" + python_include,
|
||||
# Needed because pyodide is expecting setup.py to be in the root.
|
||||
*([f"-DPYTHON_MODULE_INSTALL_DIR={REPO_PATH}"] * WASM),
|
||||
f"-DPYTHON_INCLUDE_DIR={python_include}",
|
||||
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell/tmp",
|
||||
"-DUSERSPACE_PYTHON_PREFIX="
|
||||
+ ["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}],
|
||||
@@ -1450,33 +1516,17 @@ if "IfcOpenShell-Python" in targets:
|
||||
return module_dir
|
||||
|
||||
if "wasm" in flags:
|
||||
compile_python_wrapper(
|
||||
f"{os.environ['PYMAJOR']}.{os.environ['PYMINOR']}.{os.environ['PYMICRO']}",
|
||||
f"{os.environ['TARGETINSTALLDIR']}/lib/libpython{os.environ['PYMAJOR']}.{os.environ['PYMINOR']}.a",
|
||||
os.environ["PYTHONINCLUDE"],
|
||||
None,
|
||||
)
|
||||
compile_python_wrapper(os.environ["PYVERSION"], os.environ["PYTHONINCLUDE"])
|
||||
# Copy setup.py where pyodide build system expects it.
|
||||
shutil.copy(REPO_PATH / "pyodide" / "setup.py", REPO_PATH)
|
||||
|
||||
elif USE_CURRENT_PYTHON_VERSION:
|
||||
python_info = sysconfig.get_paths()
|
||||
|
||||
py_path_components = [sysconfig.get_config_var("LIBDIR"), sysconfig.get_config_var("INSTSONAME")]
|
||||
|
||||
if sysconfig.get_config_var("multiarchsubdir"):
|
||||
py_path_components.insert(1, sysconfig.get_config_var("multiarchsubdir").replace("/", ""))
|
||||
|
||||
python_lib = os.path.join(*py_path_components)
|
||||
|
||||
compile_python_wrapper(platform.python_version(), python_lib, python_info["include"], sys.executable)
|
||||
compile_python_wrapper(platform.python_version(), python_info["include"], sys.executable)
|
||||
else:
|
||||
for python_version in PYTHON_VERSIONS:
|
||||
python_library = run([bash, "-c", f"ls {DEPS_DIR}/install/python-{python_version}/lib/libpython*.*"])
|
||||
python_include = run([bash, "-c", f"ls -d {DEPS_DIR}/install/python-{python_version}/include/python*"])
|
||||
python_executable = os.path.join(
|
||||
DEPS_DIR, "install", f"python-{python_version}", "bin", f"python{python_version[0]}"
|
||||
)
|
||||
|
||||
module_dir = compile_python_wrapper(python_version, python_library, python_include, python_executable)
|
||||
python_path = Path(DEPS_DIR) / "install" / f"python-{python_version}"
|
||||
module_dir = compile_python_wrapper(python_version, python_path=python_path)
|
||||
assert module_dir
|
||||
# Not sure why, but added after reading this in the logs
|
||||
# cp: /Users/runner/work/IfcOpenShell/IfcOpenShell/build/Darwin/x86_64/10.15/install/ifcopenshell/python-3.9.11: No such file or directory
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
There are two ways to build pyodide ifcopenshell Python wrapper wheel.
|
||||
|
||||
1. Using pyodide build system (`build_pyodide.yml` does it):
|
||||
|
||||
- install prebuilt pyodide build and emscripten environment (see `build_pyodide.sh`)
|
||||
- clone IfcOpenShell to `IfcOpenShell` folder
|
||||
- create `packages/ifcopenshell` folder that will be used by pyodide build system
|
||||
- from `IfcOpenShell` move building recipe `pyodide/meta.yaml` to `packages/ifcopenshell`
|
||||
- run `pyodide build-recipes ifcopenshell --install`, it will
|
||||
- execute `meta.yaml` recipe - it will:
|
||||
- copy IfcOpenShell source to build folder `packages/ifcopenhell/build/ifcopenshell-0.8.0`
|
||||
- build ifcopenshell and its dependencies
|
||||
- note that rerunning `pyodide build-recipes` will remove previous build folder and rebuild all dependencies.
|
||||
The way to avoid it, if build fails, is to use `pyodide build-recipes-no-deps ifcopenshell --continue` instead.
|
||||
- run `setup.py` in `IfcOpenShell` root, producing a wheel in `IfcOpenShell/dist`
|
||||
- copy that wheel to `packages/ifcopenshell/dist`
|
||||
- `--install` it to current build envrionment
|
||||
- copy the wheel next to `dist` folder (in root directory, next to `packages`)
|
||||
- add wheel to `dist/pyodide-lock.json`
|
||||
|
||||
2. Build it outside of pyodide build system.
|
||||
|
||||
Building inside pyodide build system should be preferred, option to build it outside is useful for debugging purposes,
|
||||
since it's pure cmake without any additional moving parts.
|
||||
|
||||
- setup pyodide environment, see above
|
||||
- clone IfcOpenShell repo next to it to `IfcOpenShell` folder
|
||||
- setup debug build environment using `source pyodide/debug_build_env.sh /path/to/pyodide_root`
|
||||
- run `python nix/build-all.py -wasm -py-313` in `IfcOpenShell`
|
||||
- it will produce Python package in `IfcOpenShell/ifcopenshell`
|
||||
- run `pyodide build`
|
||||
- it will produce a wheel in `IfcOpenShell/dist`
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/bash
|
||||
set -ex
|
||||
|
||||
# Install uv.
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
uv venv --python 3.13
|
||||
source .venv/bin/activate
|
||||
|
||||
# Install pyodide cross build environment.
|
||||
# Instructions: https://pyodide.org/en/stable/development/building-packages.html
|
||||
uv pip install pyodide-build
|
||||
# `uv run` is required, so xbuildenv would skip using `pip`.
|
||||
uv run pyodide xbuildenv install
|
||||
|
||||
# Emscripten doesn't come with xbuildenv.
|
||||
git clone https://github.com/emscripten-core/emsdk
|
||||
pushd emsdk
|
||||
PYODIDE_EMSCRIPTEN_VERSION=$(pyodide config get emscripten_version)
|
||||
./emsdk install ${PYODIDE_EMSCRIPTEN_VERSION}
|
||||
./emsdk activate ${PYODIDE_EMSCRIPTEN_VERSION}
|
||||
source emsdk_env.sh
|
||||
which emcc
|
||||
popd
|
||||
|
||||
mkdir -p packages/ifcopenshell
|
||||
VERSION=`cat IfcOpenShell/VERSION`
|
||||
cp IfcOpenShell/pyodide/meta.yaml packages/ifcopenshell
|
||||
sed -i s/0.8.0/$VERSION/g packages/ifcopenshell/meta.yaml
|
||||
|
||||
# Use custom build ifcopenshell directory in build-all to make caching simpler
|
||||
# Otherwise pyodide build path typically includes package version, so cached cmake configs might break.
|
||||
export BUILD_DIR=`readlink -f ifcopenshell_build`
|
||||
|
||||
# Use build-recipes-no-deps first, so logs would be printed to stdout.
|
||||
pyodide build-recipes-no-deps ifcopenshell
|
||||
pyodide build-recipes ifcopenshell --install
|
||||
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Cache built dependencies for builds.
|
||||
|
||||
This script is finding common install directory and either
|
||||
packs each folder into a tar.gz archive, if it wasn't packed before,
|
||||
or unpacks existing archives.
|
||||
|
||||
Usage: python cache_dependencies.py [pack|unpack]
|
||||
"""
|
||||
|
||||
import tarfile
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
|
||||
CACHE_PREFIX = "cache-"
|
||||
|
||||
|
||||
def get_install_dir() -> Path:
|
||||
for data in Path.cwd().glob("*/*/install"):
|
||||
return data
|
||||
raise Exception("No install dir found")
|
||||
|
||||
|
||||
def pack_dependencies(install_dir: Path) -> None:
|
||||
# Process each install_dir
|
||||
for dependency_path in install_dir.iterdir():
|
||||
if not dependency_path.is_dir():
|
||||
continue
|
||||
dependency_name = dependency_path.name
|
||||
tar_path = install_dir / f"{CACHE_PREFIX}{dependency_name}.tar.gz"
|
||||
if tar_path.exists():
|
||||
print(f"Skipping existing cache: '{tar_path}'")
|
||||
else:
|
||||
with tarfile.open(tar_path, "w:gz") as tar:
|
||||
tar.add(dependency_path, arcname=dependency_path.name)
|
||||
print(f"Created cache: '{tar_path}'")
|
||||
|
||||
|
||||
def unpack_dependencies(install_dir: Path) -> None:
|
||||
# `filter` argument was fully introduced in 3.12
|
||||
# and results in deprecation warnings in 3.12-3.13, if not provided.
|
||||
tar_filter: "dict[Literal['filter'], Literal['data']]" = (
|
||||
{"filter": "data"} if bool(sys.version_info >= (3, 12)) else {}
|
||||
)
|
||||
for tar_path in install_dir.glob(f"{CACHE_PREFIX}*.tar.gz"):
|
||||
with tarfile.open(tar_path, "r:gz") as tar:
|
||||
tar.extractall(path=install_dir, **tar_filter)
|
||||
print(f"Extracted cache: '{tar_path.name}'.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2 or (action := sys.argv[1].lower()) not in ("pack", "unpack"):
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
install_dir = get_install_dir()
|
||||
print(f"Found install dir: '{install_dir}'")
|
||||
|
||||
if action == "pack":
|
||||
pack_dependencies(install_dir)
|
||||
else:
|
||||
unpack_dependencies(install_dir)
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $0 <pyodide_root>"
|
||||
exit 1
|
||||
fi
|
||||
export PYODIDE_ROOT=$(readlink -f "$1")
|
||||
export PATH="$PYODIDE_ROOT/emsdk/emsdk:$PYODIDE_ROOT/emsdk/emsdk/node/22.16.0_64bit/bin:$PYODIDE_ROOT/emsdk/emsdk/upstream/emscripten:$PATH"
|
||||
# build-all.py vars.
|
||||
export WASM_PYTHON_PATH="$PYODIDE_ROOT/cpython/installs/python-3.13.2"
|
||||
export WASM_TOOLCHAIN_FILE="$PYODIDE_ROOT/pyodide-build/pyodide_build/tools/cmake/Modules/Platform/Emscripten.cmake"
|
||||
+2
-3
@@ -3,13 +3,12 @@ package:
|
||||
version: 0.8.0
|
||||
|
||||
source:
|
||||
# meta.yaml is placed as `packages/ifcopenshell/meta.yaml`.
|
||||
path: ../../IfcOpenShell
|
||||
|
||||
build:
|
||||
script: |
|
||||
BUILD_CFG=Release python nix/build-all.py --without-rocksdb --without-hdf5 --without-opencollada --without-swig --without-pcre -v --wasm --py313 IfcOpenShell-Python
|
||||
mv package/ifcopenshell .
|
||||
cp pyodide/setup.py .
|
||||
BUILD_CFG=Release python nix/build-all.py -v --wasm --py313
|
||||
|
||||
about:
|
||||
home: http://ifcopenshell.org
|
||||
|
||||
+46
-9
@@ -1,11 +1,48 @@
|
||||
from setuptools import setup, find_packages
|
||||
# setup.py is getting deprecated, but we still use it,
|
||||
# because `tool.setuptools.ext-modules` is still experimental in pyproject.toml
|
||||
# and we need it to get the wheel suffix right.
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
setup(name='ifcopenshell',
|
||||
version='0.8.0',
|
||||
description='IfcOpenShell is an open source (LGPL) software library for working with the Industry Foundation Classes (IFC) file format.',
|
||||
author='Thomas Krijnen',
|
||||
author_email='thomas@aecgeeks.com',
|
||||
url='http://ifcopenshell.org',
|
||||
packages=find_packages(),
|
||||
package_data={'ifcopenshell': ['util/schema/*.json', 'util/schema/*.ifc'], '': ['*.so', '*.json', '*.ifc']},
|
||||
import tomllib
|
||||
from setuptools import Extension, setup
|
||||
|
||||
|
||||
REPO_FOLDER = Path(__file__).parent
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
if "PKG_VERSION" in os.environ:
|
||||
# Inside pyodide build environment.
|
||||
return os.environ["PKG_VERSION"]
|
||||
return (REPO_FOLDER / "VERSION").read_text().strip()
|
||||
|
||||
|
||||
# Read dependencies from pyproject.toml
|
||||
def get_dependencies() -> list[str]:
|
||||
pyproject_toml = REPO_FOLDER / "src" / "ifcopenshell-python" / "pyproject.toml"
|
||||
pyproject_data = tomllib.loads(pyproject_toml.read_text())
|
||||
dependencies = pyproject_data["project"]["dependencies"]
|
||||
return dependencies
|
||||
|
||||
|
||||
setup(
|
||||
name="ifcopenshell",
|
||||
version=get_version(),
|
||||
description=(
|
||||
"IfcOpenShell is an open source (LGPL) software library "
|
||||
"for working with the Industry Foundation Classes (IFC) file format."
|
||||
),
|
||||
author="Thomas Krijnen",
|
||||
author_email="thomas@aecgeeks.com",
|
||||
url="https://ifcopenshell.org",
|
||||
install_requires=get_dependencies(),
|
||||
packages=["ifcopenshell"],
|
||||
package_data={
|
||||
# "*.so" is needed to include prebuilt binary extension. Otherwise it would try to build it and fail.
|
||||
"ifcopenshell": ["util/schema/*.json", "util/schema/*.ifc", "*.so"],
|
||||
"": ["*.json", "*.ifc"],
|
||||
},
|
||||
# Has to provide extension to get the correct wheel suffix.
|
||||
ext_modules=[Extension("ifcopenshell._ifcopenshell_wrapper", sources=[])],
|
||||
)
|
||||
|
||||
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.);
|
||||
#7=IFCSIMPLEPROPERTYTEMPLATE('2j2ZEZR8X5tONm7kli5hM6',$,'HasLinework','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#8=IFCSIMPLEPROPERTYTEMPLATE('1ttChRysH9UuEX2FeMj5Hu',$,'HasAnnotation','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#9=IFCSIMPLEPROPERTYTEMPLATE('2NPPxuABv1huDTVh32TFgw',$,'GlobalReferencing','Whether or not this drawing can be referenced in other drawings.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#10=IFCSIMPLEPROPERTYTEMPLATE('10hT_1zrzEbRRKMXYAWvtD',$,'Metadata','Comma separated list of selector expressions to evaluate for each drawing elementand add results to their ''class'' attribute.\X2\000A\X0\E.g. ''Name, id'' would add to ''class'' value similar to ''Name-Wall id-1220''.\X2\000A\X0\Then it can be used to applied css styles based on the resulting class.\X2\000A\X0\If attribute is not present on the element, then it won''t be added to it''s ''class''.',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude ifc elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#11=IFCSIMPLEPROPERTYTEMPLATE('3Z0BXPSG5CWgtI33ioV7aj',$,'Include','Selector expression to include elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#12=IFCSIMPLEPROPERTYTEMPLATE('1RVts_g3PAw98PJA2yL3bO',$,'Exclude','Selector expression to exclude elements in the drawing',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#13=IFCSIMPLEPROPERTYTEMPLATE('0c1$8NpYDEaBiJrj16jHIo',$,'Stylesheet','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#14=IFCSIMPLEPROPERTYTEMPLATE('3mRF52q81FQB$h4oTh7M45',$,'Markers','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#15=IFCSIMPLEPROPERTYTEMPLATE('1rhr_0N3LDtuORcEJP0KXM',$,'Symbols','',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
|
||||
@@ -36,6 +36,7 @@ from bonsai.bim.ifc import IfcStore
|
||||
from mathutils import Vector
|
||||
from typing import Union
|
||||
from logging import Logger
|
||||
from math import radians
|
||||
|
||||
|
||||
class IfcExporter:
|
||||
@@ -104,7 +105,16 @@ class IfcExporter:
|
||||
|
||||
def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
|
||||
element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id)
|
||||
if tool.Geometry.is_scaled(obj):
|
||||
# Handle camera scales specially
|
||||
if obj.type == "CAMERA":
|
||||
# Check if this is a reflected ceiling plan camera
|
||||
camera = tool.Ifc.get_entity(obj)
|
||||
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
|
||||
# Ensure reflected ceiling cameras have the correct scale
|
||||
if obj.scale != (-1, -1, -1):
|
||||
obj.scale = (-1, -1, -1)
|
||||
# Skip all other scale handling for cameras
|
||||
elif tool.Geometry.is_scaled(obj):
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
# update_representation might not apply scale if the object has openings
|
||||
# reset it, so let user know that the scale wasn't saved.
|
||||
|
||||
@@ -222,8 +222,6 @@ def refresh_ui_data():
|
||||
if isinstance(ifc_file := tool.Ifc.get(), ifcopenshell.sqlite):
|
||||
ifc_file.clear_cache()
|
||||
|
||||
props = tool.Drawing.get_document_props()
|
||||
props.should_draw_decorations = props.should_draw_decorations
|
||||
if tool.Web.get_web_props().is_connected:
|
||||
tool.Web.send_webui_data()
|
||||
|
||||
|
||||
@@ -952,6 +952,9 @@ class IfcImporter:
|
||||
if not props.ifc_file:
|
||||
props.ifc_file = self.ifc_import_settings.input_file
|
||||
self.file = tool.Ifc.get()
|
||||
# IFC4 Reference View shall have no booleans https://github.com/BuildingSMART/IFC4-CV/issues/14
|
||||
if self.file.schema == "IFC4" and "ReferenceView" in str(self.file.header.file_description.description):
|
||||
self.ifc_import_settings.void_limit = 0
|
||||
|
||||
def calculate_unit_scale(self):
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
|
||||
|
||||
@@ -94,6 +94,9 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
aggregates_to_check = set()
|
||||
|
||||
# First pass: unassign all parts and track their aggregates
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
@@ -101,6 +104,10 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if not aggregate:
|
||||
continue
|
||||
|
||||
# Track this aggregate for later checking
|
||||
aggregates_to_check.add(aggregate)
|
||||
|
||||
core.unassign_object(
|
||||
tool.Ifc,
|
||||
tool.Aggregate,
|
||||
@@ -116,6 +123,29 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
|
||||
pset = tool.Ifc.get().by_id(pset["id"])
|
||||
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
|
||||
|
||||
# Second pass: delete aggregates that now have no parts
|
||||
deleted_aggregates = []
|
||||
for aggregate in aggregates_to_check:
|
||||
related_objects = ifcopenshell.util.element.get_parts(aggregate)
|
||||
if len(related_objects) == 0:
|
||||
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
|
||||
deleted_aggregates.append(aggregate_name)
|
||||
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
if aggregate_obj:
|
||||
ifcopenshell.api.root.remove_product(tool.Ifc.get(), product=aggregate)
|
||||
bpy.data.objects.remove(aggregate_obj, do_unlink=True)
|
||||
|
||||
# Show info message if aggregates were deleted
|
||||
if deleted_aggregates:
|
||||
if len(deleted_aggregates) == 1:
|
||||
self.report(
|
||||
{"INFO"}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts"
|
||||
)
|
||||
else:
|
||||
aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates)
|
||||
self.report({"INFO"}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts")
|
||||
|
||||
|
||||
class BIM_OT_enable_editing_aggregate(bpy.types.Operator):
|
||||
"""Enable editing aggregation relationship"""
|
||||
|
||||
@@ -282,7 +282,11 @@ class BIM_PT_classification_references(Panel, ReferenceUI):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return bool((obj := context.active_object) and tool.Ifc.get_entity(obj))
|
||||
return (
|
||||
(obj := tool.Blender.get_active_object())
|
||||
and (element := tool.Ifc.get_entity(obj))
|
||||
and element.is_a("IfcObjectDefinition")
|
||||
)
|
||||
|
||||
def get_object_name(self, context: bpy.types.Context) -> str:
|
||||
assert (obj := context.active_object)
|
||||
|
||||
@@ -818,7 +818,12 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.merge_identical_objects"
|
||||
bl_label = "Merge Identical Objects"
|
||||
bl_description = "For materials currently only IfcMaterials are supported"
|
||||
bl_description = (
|
||||
"Merge identical IFC objects (that match all attributes).\n"
|
||||
"\n"
|
||||
"SHIFT + CLICK to merge by name/identification attribute only.\n"
|
||||
"Merges names with number suffix, as well (ex: foo, foo.001, foo.002)\n"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
|
||||
@@ -826,18 +831,36 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
|
||||
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
|
||||
)
|
||||
|
||||
by_name_or_identification_only: bpy.props.BoolProperty(
|
||||
name="By Name/Identification Only",
|
||||
description="Merge based only on Name or Identification attribute, ignoring other properties",
|
||||
default=False,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
object_type: tool.Debug.PurgeMergeObjectType
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Check if shift key is pressed
|
||||
if event.shift:
|
||||
self.by_name_or_identification_only = True
|
||||
else:
|
||||
self.by_name_or_identification_only = False
|
||||
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
object_type: str = self.object_type
|
||||
if object_type in ("PROFILE", "TYPE"):
|
||||
self.report({"ERROR"}, f"Unsupported object type {object_type}.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
merged_data = tool.Debug.merge_identical_objects(object_type)
|
||||
merged_data = tool.Debug.merge_identical_objects(
|
||||
object_type, by_name_or_identification_only=self.by_name_or_identification_only
|
||||
)
|
||||
plural_object_type = f"{object_type.lower().replace('_', ' ')}s"
|
||||
if merged_data:
|
||||
merge_mode = " by name/identification" if self.by_name_or_identification_only else ""
|
||||
for element_type, element_names in merged_data.items():
|
||||
print(f"- {element_type}:")
|
||||
for name in element_names:
|
||||
@@ -846,7 +869,8 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
|
||||
merged = sum(len(v) for v in merged_data.values())
|
||||
|
||||
msg = " See system console for details." if merged else ""
|
||||
self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged.{msg}")
|
||||
merge_mode = " (by name/identification)" if self.by_name_or_identification_only else ""
|
||||
self.report({"INFO"}, f"{merged} identical {plural_object_type} were merged{merge_mode}.{msg}")
|
||||
|
||||
if merged == 0:
|
||||
return
|
||||
|
||||
@@ -61,6 +61,7 @@ classes = (
|
||||
operator.EnableEditingAssignedProduct,
|
||||
operator.EnableEditingElementFilter,
|
||||
operator.EnableEditingText,
|
||||
operator.ExcludeAnnotation,
|
||||
operator.ExpandSheet,
|
||||
operator.LoadDrawings,
|
||||
operator.LoadReferences,
|
||||
|
||||
@@ -235,13 +235,18 @@ class DecoratorData:
|
||||
cut_cache = {}
|
||||
slice_cache = {}
|
||||
fill_cache = {}
|
||||
camera_location_checksum = None
|
||||
camera_rotation_checksum = None
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls):
|
||||
cls.cut_cache = {}
|
||||
cls.layerset_cache = {}
|
||||
cls.fill_cache = {}
|
||||
|
||||
@classmethod
|
||||
def load(cls, handler):
|
||||
cls.is_loaded = True
|
||||
cls.cut_cache = {}
|
||||
cls.layerset_cache = {}
|
||||
cls.fill_cache = {}
|
||||
|
||||
text = {}
|
||||
dimension = {}
|
||||
|
||||
@@ -22,6 +22,7 @@ import blf
|
||||
import math
|
||||
import bmesh
|
||||
import shapely
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
@@ -1625,6 +1626,7 @@ class CutDecorator:
|
||||
if cls.installed:
|
||||
cls.uninstall()
|
||||
handler = cls()
|
||||
handler.cache_camera_matrix()
|
||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
|
||||
|
||||
@classmethod
|
||||
@@ -1727,6 +1729,30 @@ class CutDecorator:
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def cache_camera_matrix(self):
|
||||
obj = bpy.context.scene.camera
|
||||
DecoratorData.camera_location_checksum = repr(np.array(obj.matrix_world.translation).tobytes())
|
||||
DecoratorData.camera_rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes())
|
||||
|
||||
def is_camera_moved(self):
|
||||
if not DecoratorData.camera_location_checksum:
|
||||
self.cache_camera_matrix()
|
||||
return True # Let's be conservative
|
||||
obj = bpy.context.scene.camera
|
||||
loc_check = np.frombuffer(eval(DecoratorData.camera_location_checksum))
|
||||
loc_real = np.array(obj.matrix_world.translation).flatten()
|
||||
if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm
|
||||
self.cache_camera_matrix()
|
||||
return True
|
||||
rot_check = np.frombuffer(eval(DecoratorData.camera_rotation_checksum)).reshape(3, 3)
|
||||
rot_real = np.array(obj.matrix_world.to_3x3())
|
||||
rot_dot = np.dot(rot_check, rot_real.T)
|
||||
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
|
||||
if angle_rad > 0.0017453292519943296: # 0.1 degrees
|
||||
self.cache_camera_matrix()
|
||||
return True
|
||||
return False
|
||||
|
||||
def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
has_cut_cache = element.id() in DecoratorData.cut_cache
|
||||
has_fill_cache = element.id() in DecoratorData.fill_cache
|
||||
@@ -1734,9 +1760,9 @@ class CutDecorator:
|
||||
# Currently selected objects must be recalculated as they may be being moved / edited.
|
||||
# If the camera is selected, we also recalculate as the user may be moving the camera.
|
||||
|
||||
if not has_cut_cache or obj.select_get() or context.scene.camera.select_get():
|
||||
if not has_cut_cache or obj.select_get() or self.is_camera_moved():
|
||||
self.recalculate_cut(context, obj, element)
|
||||
if not has_fill_cache or obj.select_get() or context.scene.camera.select_get():
|
||||
if not has_fill_cache or obj.select_get() or self.is_camera_moved():
|
||||
self.recalculate_fill(context, obj, element)
|
||||
|
||||
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
@@ -1933,8 +1959,7 @@ class DecorationsHandler:
|
||||
# NOTE: we USE POST_PIXEL here so that we can use both POLYLINE_UNIFORM_COLOR
|
||||
# and drawing text in the same handler. BUT this means that we supply coordinates in WINSPACE
|
||||
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_PIXEL")
|
||||
if not DecoratorData.is_loaded:
|
||||
DecoratorData.load(handler)
|
||||
DecoratorData.clear_cache()
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
@@ -1961,5 +1986,8 @@ class DecorationsHandler:
|
||||
if not DrawingsData.is_loaded:
|
||||
DrawingsData.load()
|
||||
|
||||
if not DecoratorData.is_loaded:
|
||||
DecoratorData.load(self)
|
||||
|
||||
for obj, decorator in DecoratorData.data["object_decorators"]:
|
||||
decorator.decorate(context, obj)
|
||||
|
||||
@@ -210,12 +210,6 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
should_duplicate_annotations=self.should_duplicate_annotations,
|
||||
)
|
||||
|
||||
# TODO: Why need to resync active drawing, if it wasn't changed.
|
||||
drawing = props.get_active_drawing()
|
||||
if drawing is None:
|
||||
return
|
||||
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
|
||||
|
||||
|
||||
class CreateDrawing(bpy.types.Operator):
|
||||
"""Creates/refreshes a .svg drawing
|
||||
@@ -442,6 +436,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
context.scene.render.filepath = str(Path(svg_path).with_suffix(".png"))
|
||||
assert (drawing_style := self.cprops.get_active_drawing_style())
|
||||
|
||||
tool.Blender.sync_render_visibility()
|
||||
|
||||
if drawing_style.render_type == "DEFAULT":
|
||||
bpy.ops.render.render(write_still=True)
|
||||
else:
|
||||
@@ -625,7 +621,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
path.attrib["d"] = d
|
||||
group.append(g)
|
||||
|
||||
def generate_wall_layers(self, context: bpy.types.Context, root) -> None:
|
||||
def generate_material_layers(self, context: bpy.types.Context, root) -> None:
|
||||
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
|
||||
if "projection" in el.get("class", "").split():
|
||||
continue
|
||||
@@ -794,8 +790,9 @@ class CreateDrawing(bpy.types.Operator):
|
||||
edge_bm.to_mesh(edge_mesh)
|
||||
edge_bm.free()
|
||||
|
||||
actual_path = svg_path[0:-4] + "0001.svg"
|
||||
freestyle_svg_exporter = tool.Blender.get_addon("freestyle_svg_exporter")
|
||||
context.scene.render.filepath = svg_path[0:-4]
|
||||
actual_path = freestyle_svg_exporter.create_path(bpy.context.scene)
|
||||
bpy.ops.render.render(write_still=False)
|
||||
|
||||
os.replace(actual_path, svg_path)
|
||||
@@ -839,7 +836,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
if tool.Drawing.is_camera_orthographic():
|
||||
self.generate_bisect_linework(context, root)
|
||||
self.generate_wall_layers(context, root)
|
||||
if self.cprops.generate_material_layers:
|
||||
self.generate_material_layers(context, root)
|
||||
self.merge_linework_and_add_metadata(root)
|
||||
self.move_elements_to_top(root)
|
||||
|
||||
@@ -937,12 +935,14 @@ class CreateDrawing(bpy.types.Operator):
|
||||
if self.cprops.cut_mode == "BISECT":
|
||||
self.remove_cut_linework(root)
|
||||
self.generate_bisect_linework(context, root)
|
||||
self.generate_wall_layers(context, root)
|
||||
if self.cprops.generate_material_layers:
|
||||
self.generate_material_layers(context, root)
|
||||
self.merge_linework_and_add_metadata(root)
|
||||
self.move_elements_to_top(root)
|
||||
elif self.cprops.cut_mode == "OPENCASCADE":
|
||||
self.move_projection_to_bottom(root)
|
||||
self.generate_wall_layers(context, root)
|
||||
if self.cprops.generate_material_layers:
|
||||
self.generate_material_layers(context, root)
|
||||
self.merge_linework_and_add_metadata(root)
|
||||
self.move_elements_to_top(root)
|
||||
|
||||
@@ -1252,6 +1252,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
|
||||
def get_svg_classes(self, element, layer=None):
|
||||
classes = [element.is_a()]
|
||||
|
||||
# ─── Material ──────────────────────────────────────────────
|
||||
material = ifcopenshell.util.element.get_material(element, should_skip_usage=True)
|
||||
material_name = ""
|
||||
if material:
|
||||
@@ -1264,6 +1266,7 @@ class CreateDrawing(bpy.types.Operator):
|
||||
else:
|
||||
classes.append("material-null")
|
||||
|
||||
# ─── Layer ─────────────────────────────────────────────────
|
||||
if layer:
|
||||
classes.append(layer.is_a())
|
||||
layer_material = layer.Material
|
||||
@@ -1276,12 +1279,19 @@ class CreateDrawing(bpy.types.Operator):
|
||||
layer_material_category = tool.Drawing.canonicalise_class_name(layer_material.Category)
|
||||
classes.append(f"layer-material-category-{layer_material_category}")
|
||||
|
||||
# ─── Metadata ──────────────────────────────────────────────
|
||||
for key in self.metadata:
|
||||
value = ifcopenshell.util.selector.get_element_value(element, key)
|
||||
if value:
|
||||
classes.append(
|
||||
tool.Drawing.canonicalise_class_name(key) + "-" + tool.Drawing.canonicalise_class_name(str(value))
|
||||
)
|
||||
|
||||
# ─── Target View ───────────────────────────────────────────
|
||||
if getattr(self.cprops, "target_view", None):
|
||||
target_view_class = tool.Drawing.canonicalise_class_name(str(self.cprops.target_view))
|
||||
classes.append(f"target-view-{target_view_class}")
|
||||
|
||||
return classes
|
||||
|
||||
def is_manifold(self, obj) -> bool:
|
||||
@@ -1348,7 +1358,13 @@ class CreateDrawing(bpy.types.Operator):
|
||||
join_criteria = join_criteria.split(",")
|
||||
else:
|
||||
# Drawing convention states that same objects classes with the same material are merged when cut.
|
||||
join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status", "Material.Name"]
|
||||
join_criteria = [
|
||||
"class",
|
||||
"material.Name",
|
||||
"/Pset_.*Common/.Status",
|
||||
"EPset_Status.Status",
|
||||
"EPset_Status.UserDefinedStatus",
|
||||
]
|
||||
|
||||
group = root.find("{http://www.w3.org/2000/svg}g")
|
||||
joined_paths = {}
|
||||
@@ -1477,11 +1493,10 @@ class CreateDrawing(bpy.types.Operator):
|
||||
joined_paths.setdefault(hash_keys, []).append(el)
|
||||
|
||||
for key, els in joined_paths.items():
|
||||
polygons = []
|
||||
classes = set()
|
||||
queue = []
|
||||
|
||||
for el in els:
|
||||
classes.update(el.attrib["class"].split())
|
||||
classes = set(el.attrib["class"].split())
|
||||
classes.add(el.attrib["{http://www.ifcopenshell.org/ns}guid"])
|
||||
is_closed_polygon = False
|
||||
for path in el.findall("{http://www.w3.org/2000/svg}path"):
|
||||
@@ -1496,31 +1511,30 @@ class CreateDrawing(bpy.types.Operator):
|
||||
coords.append(coords[0])
|
||||
if len(coords) > 2 and coords[0] == coords[-1]:
|
||||
is_closed_polygon = True
|
||||
polygons.append(shapely.Polygon(coords))
|
||||
queue.append((shapely.Polygon(coords), classes))
|
||||
if is_closed_polygon:
|
||||
el.getparent().remove(el)
|
||||
|
||||
try:
|
||||
merged_polygons = shapely.ops.unary_union(polygons)
|
||||
except:
|
||||
print("Warning. Portions of the merge failed. Please report a bug!", polygons)
|
||||
merged_polygons = polygons
|
||||
while queue:
|
||||
polygon, polygon_classes = queue.pop()
|
||||
for polygon2, polygon2_classes in queue[:]:
|
||||
try:
|
||||
merged_polygon = shapely.union(polygon, polygon2)
|
||||
except:
|
||||
print("Warning. Portions of the merge failed. Please report a bug!", polygon, polygon2)
|
||||
continue
|
||||
if type(merged_polygon) == shapely.Polygon:
|
||||
polygon = merged_polygon
|
||||
polygon_classes.update(polygon2_classes)
|
||||
queue.remove((polygon2, polygon2_classes))
|
||||
|
||||
if type(merged_polygons) == shapely.MultiPolygon:
|
||||
merged_polygons = merged_polygons.geoms
|
||||
elif type(merged_polygons) == shapely.Polygon:
|
||||
merged_polygons = [merged_polygons]
|
||||
else:
|
||||
merged_polygons = []
|
||||
|
||||
for polygon in merged_polygons:
|
||||
g = etree.Element("g")
|
||||
path = etree.SubElement(g, "path")
|
||||
d = "M" + " L".join([",".join([str(o) for o in co]) for co in polygon.exterior.coords[0:-1]]) + " Z"
|
||||
for interior in polygon.interiors:
|
||||
d += " M" + " L".join([",".join([str(o) for o in co]) for co in interior.coords[0:-1]]) + " Z"
|
||||
path.attrib["d"] = d
|
||||
g.set("class", " ".join(list(classes)))
|
||||
g.set("class", " ".join(list(polygon_classes)))
|
||||
group.append(g)
|
||||
|
||||
def drawing_to_model_co(self, x: float, y: float) -> Vector:
|
||||
@@ -1862,13 +1876,15 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
props = tool.Drawing.get_document_props()
|
||||
# Won't be visible in UI anyway.
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
if not props.sheets or not prefs.data_dir:
|
||||
return False
|
||||
if not tool.Drawing.get_active_drawing_item():
|
||||
cls.poll_message_set("No drawing selected.")
|
||||
return False
|
||||
if not props.sheets:
|
||||
cls.poll_message_set("No sheets available.")
|
||||
return False
|
||||
if not tool.Blender.get_user_data_dir():
|
||||
cls.poll_message_set("BIM data directory not set.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -2222,6 +2238,7 @@ class ActivateModel(bpy.types.Operator):
|
||||
)
|
||||
|
||||
tool.Blender.reset_object_visibility()
|
||||
tool.Drawing.hide_all_drawing_collections()
|
||||
tool.Blender.update_viewport()
|
||||
bonsai.bim.handler.refresh_ui_data()
|
||||
|
||||
@@ -2318,9 +2335,8 @@ class ActivateDrawingBase(tool.Ifc.Operator):
|
||||
|
||||
dprops.active_drawing_id = self.drawing
|
||||
dprops.drawing_styles.clear()
|
||||
if ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "HasUnderlay"):
|
||||
bpy.ops.bim.reload_drawing_styles()
|
||||
bpy.ops.bim.activate_drawing_style()
|
||||
bpy.ops.bim.reload_drawing_styles()
|
||||
bpy.ops.bim.activate_drawing_style()
|
||||
|
||||
if tool.Drawing.is_camera_orthographic():
|
||||
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=tool.Ifc.get().by_id(self.drawing))
|
||||
@@ -2333,10 +2349,22 @@ class ActivateDrawingBase(tool.Ifc.Operator):
|
||||
camera = context.scene.camera
|
||||
assert camera
|
||||
camera_props = tool.Drawing.get_camera_props(camera)
|
||||
# Check if this is a reflected ceiling camera and preserve its scale
|
||||
camera_element = tool.Ifc.get_entity(camera)
|
||||
is_reflected = False
|
||||
if camera_element:
|
||||
is_reflected = (
|
||||
ifcopenshell.util.element.get_pset(camera_element, "EPset_Drawing", "TargetView")
|
||||
== "REFLECTED_PLAN_VIEW"
|
||||
)
|
||||
if is_reflected and camera.scale != (-1, -1, -1):
|
||||
camera.scale = (-1, -1, -1)
|
||||
|
||||
if camera_props.update_representation(camera.matrix_world):
|
||||
bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="")
|
||||
# See 6452 and 6478.
|
||||
# bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT")
|
||||
# Restore the scale after update if needed
|
||||
if is_reflected:
|
||||
camera.scale = (-1, -1, -1)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -2812,8 +2840,13 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not props.schedules:
|
||||
cls.poll_message_set("No schedule selected.")
|
||||
return False
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
return props.schedules and props.sheets and prefs.data_dir
|
||||
if not props.sheets:
|
||||
cls.poll_message_set("No sheets available.")
|
||||
return False
|
||||
if not tool.Blender.get_user_data_dir():
|
||||
cls.poll_message_set("BIM data directory not set.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Drawing.get_document_props()
|
||||
@@ -2880,8 +2913,13 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if not props.references:
|
||||
cls.poll_message_set("No reference selected.")
|
||||
return False
|
||||
bim_props = tool.Blender.get_bim_props()
|
||||
return props.references and props.sheets and bim_props.data_dir
|
||||
if not props.sheets:
|
||||
cls.poll_message_set("No sheets available.")
|
||||
return False
|
||||
if not tool.Blender.get_user_data_dir():
|
||||
cls.poll_message_set("BIM data directory not set.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _execute(self, context):
|
||||
props = tool.Drawing.get_document_props()
|
||||
@@ -3730,3 +3768,19 @@ class OpenDocumentationWebUi(bpy.types.Operator):
|
||||
else:
|
||||
bpy.ops.bim.open_web_browser(page="documentation")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ExcludeAnnotation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.exclude_annotation"
|
||||
bl_label = "Exclude Annotation"
|
||||
bl_description = "Excludes the automatic annotation reference from the drawing"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
if not (obj := bpy.context.scene.camera) or not (drawing := tool.Ifc.get_entity(obj)):
|
||||
return
|
||||
for obj in tool.Blender.get_selected_objects(include_active=False):
|
||||
if (element := tool.Ifc.get_entity(obj)) and tool.Drawing.is_auto_annotation(element):
|
||||
if referenced_element := tool.Drawing.get_annotation_element(element):
|
||||
tool.Drawing.exclude_annotation_from_drawing(referenced_element, drawing)
|
||||
core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing)
|
||||
|
||||
@@ -498,6 +498,9 @@ class BIMCameraProperties(PropertyGroup):
|
||||
name="Linework Mode",
|
||||
update=get_update_layer_callback("linework_mode", "LineworkMode"),
|
||||
)
|
||||
generate_material_layers: bpy.props.BoolProperty(
|
||||
name="Generate Material Layers", description="Generate material layer linework in drawings", default=True
|
||||
)
|
||||
fill_mode: EnumProperty(
|
||||
items=[
|
||||
("NONE", "None", "Disable filling areas seen in projection"),
|
||||
@@ -859,6 +862,19 @@ class BIMAnnotationProperties(PropertyGroup):
|
||||
)
|
||||
is_adding_type: bpy.props.BoolProperty(default=False)
|
||||
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
|
||||
tag_rotation_mode: bpy.props.EnumProperty(
|
||||
name="Tag Rotation Mode",
|
||||
description="How to orient the tag relative to the tagged object",
|
||||
items=[
|
||||
("NONE", "No Rotation", "Keep tag in default orientation"),
|
||||
("LOCAL_X", "Local X Axis", "Align tag with object's local X axis"),
|
||||
("LOCAL_Y", "Local Y Axis", "Align tag with object's local Y axis"),
|
||||
("LOCAL_Z", "Local Z Axis", "Align tag with object's local Z axis"),
|
||||
("CAMERA_Horizontal", "Camera Horizontal", "Align tag with camera X axis"),
|
||||
("CAMERA_Vertical", "Camera Vertical", "Align tag with camera Y axis"),
|
||||
],
|
||||
default="NONE",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
object_type: str
|
||||
|
||||
@@ -99,12 +99,13 @@ class BIM_PT_camera(Panel):
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "linework_mode")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "generate_material_layers")
|
||||
if props.linework_mode == "OPENCASCADE":
|
||||
row = self.layout.row()
|
||||
row.prop(props, "fill_mode")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "cut_mode")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "width")
|
||||
row = self.layout.row()
|
||||
@@ -193,6 +194,7 @@ class BIM_PT_element_filters(Panel):
|
||||
text = "Exclude Filter" if ElementFiltersData.data["has_exclude_filter"] else "No Exclude Filter Found"
|
||||
icon = "GREASEPENCIL" if ElementFiltersData.data["has_exclude_filter"] else "ADD"
|
||||
row.label(text=text, icon="FILTER")
|
||||
row.operator("bim.exclude_annotation", icon="REMOVE", text="")
|
||||
row.operator("bim.enable_editing_element_filter", icon=icon, text="").filter_mode = "EXCLUDE"
|
||||
|
||||
|
||||
|
||||
@@ -228,7 +228,6 @@ class AnnotationToolUI:
|
||||
def draw_type_selection_interface(cls):
|
||||
# shared by both sidebar and header
|
||||
object_type = cls.props.object_type
|
||||
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="FILE_VOLUME")
|
||||
prop_with_search(row, cls.props, "object_type", text="")
|
||||
@@ -248,6 +247,9 @@ class AnnotationToolUI:
|
||||
add_layout_hotkey_operator(cls.layout, "Add", "S_A", "Create a new annotation")
|
||||
|
||||
if object_type in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP:
|
||||
row = cls.layout.row(align=True)
|
||||
row.label(text="", icon="DRIVER_ROTATIONAL_DIFFERENCE")
|
||||
row.prop(cls.props, "tag_rotation_mode", text="")
|
||||
add_layout_hotkey_operator(
|
||||
cls.layout,
|
||||
"Bulk Tag",
|
||||
@@ -299,6 +301,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return
|
||||
|
||||
related_objects = bpy.context.selected_objects
|
||||
created_objects = []
|
||||
|
||||
for related_object in related_objects:
|
||||
obj = core.add_annotation(
|
||||
tool.Ifc,
|
||||
@@ -311,7 +315,15 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
),
|
||||
enable_editing=False,
|
||||
)
|
||||
tool.Drawing.setup_annotation_object(obj, object_type, related_object)
|
||||
tool.Drawing.setup_annotation_object(obj, object_type, related_object, props.tag_rotation_mode)
|
||||
created_objects.append(obj)
|
||||
|
||||
# Select the created annotation objects
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for obj in created_objects:
|
||||
obj.select_set(True)
|
||||
if created_objects:
|
||||
bpy.context.view_layer.objects.active = created_objects[-1]
|
||||
|
||||
def hotkey_S_A(self):
|
||||
if bpy.ops.bim.add_annotation.poll():
|
||||
@@ -341,4 +353,5 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
continue
|
||||
related_object = tool.Ifc.get_object(related_product)
|
||||
|
||||
tool.Drawing.setup_annotation_object(obj, annotation_type, related_object)
|
||||
rotation_mode = tool.Drawing.get_annotation_props().tag_rotation_mode
|
||||
tool.Drawing.setup_annotation_object(obj, annotation_type, related_object, rotation_mode)
|
||||
|
||||
@@ -109,8 +109,9 @@ def block_scale(scene: bpy.types.Scene) -> None:
|
||||
if obj.type == "CAMERA":
|
||||
camera = tool.Ifc.get_entity(obj)
|
||||
if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW":
|
||||
obj.scale = (-1, -1, -1)
|
||||
obj.rotation_euler = (0.0, 0.0, math.radians(180))
|
||||
# Only update if scale isn't already (-1, -1, -1)
|
||||
if obj.scale != (-1, -1, -1):
|
||||
obj.scale = (-1, -1, -1)
|
||||
else:
|
||||
if obj.scale != (1, 1, 1):
|
||||
obj.scale = (1, 1, 1)
|
||||
|
||||
@@ -858,6 +858,10 @@ class OverrideDelete(bpy.types.Operator):
|
||||
objects_to_remove = context.selected_objects
|
||||
|
||||
self.process_arrays(context)
|
||||
|
||||
# Track aggregates before deleting their parts
|
||||
aggregates_to_check = self.track_aggregates(objects_to_remove)
|
||||
|
||||
clear_active_object = True
|
||||
|
||||
for i, obj in enumerate(objects_to_remove, 1):
|
||||
@@ -882,13 +886,28 @@ class OverrideDelete(bpy.types.Operator):
|
||||
continue
|
||||
if ifcopenshell.util.element.get_pset(element, "BBIM_Array"):
|
||||
self.report({"INFO"}, "Elements that are part of an array cannot be deleted.")
|
||||
return {"FINISHED"}
|
||||
continue
|
||||
if element.is_a("IfcGridAxis"):
|
||||
# Deleting the last W axis is OK
|
||||
if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
|
||||
(grid := element.PartOfV) and len(grid[0].VAxes) == 1
|
||||
):
|
||||
self.report(
|
||||
{"INFO"}, "The last grid axis of a grid cannot be deleted. Delete the grid instead."
|
||||
)
|
||||
continue
|
||||
if tool.Drawing.is_auto_annotation(element):
|
||||
self.report({"INFO"}, "References cannot be deleted. Exclude the referenced element instead.")
|
||||
continue
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
elif tool.Geometry.is_representation_item(obj):
|
||||
tool.Geometry.delete_ifc_item(obj)
|
||||
else:
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
# Delete empty aggregates after deleting their parts
|
||||
self.delete_empty_aggregates(aggregates_to_check)
|
||||
|
||||
for opening in tool.Model.get_model_props().openings:
|
||||
if opening.obj is not None and not tool.Ifc.get_entity(opening.obj):
|
||||
bpy.data.objects.remove(opening.obj)
|
||||
@@ -921,6 +940,49 @@ class OverrideDelete(bpy.types.Operator):
|
||||
data["old_file"].redo()
|
||||
tool.Ifc.set(data["new_file"])
|
||||
|
||||
def track_aggregates(self, objects_to_remove):
|
||||
"""Track aggregates that contain objects being deleted"""
|
||||
aggregates_to_check = set()
|
||||
for obj in objects_to_remove:
|
||||
if not tool.Blender.is_valid_data_block(obj):
|
||||
continue
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if aggregate:
|
||||
aggregates_to_check.add(aggregate)
|
||||
return aggregates_to_check
|
||||
|
||||
def delete_empty_aggregates(self, aggregates_to_check):
|
||||
"""Delete aggregates that now have no parts"""
|
||||
deleted_aggregates = []
|
||||
for aggregate in aggregates_to_check:
|
||||
# Check if aggregate still exists (might have been deleted already)
|
||||
try:
|
||||
aggregate.id()
|
||||
except:
|
||||
continue
|
||||
|
||||
related_objects = ifcopenshell.util.element.get_parts(aggregate)
|
||||
if len(related_objects) == 0:
|
||||
aggregate_name = aggregate.Name or f"{aggregate.is_a()} #{aggregate.id()}"
|
||||
deleted_aggregates.append(aggregate_name)
|
||||
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
if aggregate_obj and tool.Blender.is_valid_data_block(aggregate_obj):
|
||||
tool.Geometry.delete_ifc_object(aggregate_obj)
|
||||
|
||||
# Show info message if aggregates were deleted
|
||||
if deleted_aggregates:
|
||||
if len(deleted_aggregates) == 1:
|
||||
self.report(
|
||||
{"INFO"}, f"Aggregate '{deleted_aggregates[0]}' was deleted because it had no remaining parts"
|
||||
)
|
||||
else:
|
||||
aggregate_list = ", ".join(f"'{name}'" for name in deleted_aggregates)
|
||||
self.report({"INFO"}, f"Aggregates {aggregate_list} were deleted because they had no remaining parts")
|
||||
|
||||
def process_arrays(self, context: bpy.types.Context) -> None:
|
||||
ifc_file = tool.Ifc.get()
|
||||
selected_objects = set(context.selected_objects)
|
||||
@@ -2973,6 +3035,7 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
|
||||
item_mesh = bpy.data.meshes.new("tmp")
|
||||
tool.Ifc.link(item, item_mesh)
|
||||
item_obj = bpy.data.objects.new("tmp", item_mesh)
|
||||
tool.Geometry.lock_scale(item_obj)
|
||||
tool.Geometry.name_item_object(item_obj, item)
|
||||
item_obj.matrix_world = obj.matrix_world
|
||||
bpy.context.collection.objects.link(item_obj)
|
||||
|
||||
@@ -41,7 +41,7 @@ class SolarData:
|
||||
|
||||
@classmethod
|
||||
def sun_position(cls):
|
||||
return tool.Blender.get_sun_position_addon()
|
||||
return tool.Blender.get_addon("sun_position")
|
||||
|
||||
@classmethod
|
||||
def sites(cls):
|
||||
|
||||
@@ -202,7 +202,7 @@ class RadianceRender(bpy.types.Operator):
|
||||
print(f"Camera position: {camera_position}")
|
||||
print(f"Camera direction: {camera_direction}")
|
||||
|
||||
# sun_position = tool.Blender.get_sun_position_addon()
|
||||
# sun_position = tool.Blender.get_addon("sun_position")
|
||||
# azimuth, elevation = sun_position.sun_calc.get_sun_coordinates(
|
||||
# sun_pos_props.time,
|
||||
# sun_pos_props.latitude,
|
||||
|
||||
@@ -40,7 +40,7 @@ from bpy.types import PropertyGroup
|
||||
from bonsai.bim.module.light.data import SolarData
|
||||
from bonsai.bim.module.light.decorator import SolarDecorator
|
||||
|
||||
sun_position = tool.Blender.get_sun_position_addon()
|
||||
sun_position = tool.Blender.get_addon("sun_position")
|
||||
now = datetime.datetime.now()
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "spectraldb.json"), "r") as f:
|
||||
|
||||
@@ -373,7 +373,12 @@ class BIMStairProperties(PropertyGroup):
|
||||
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
|
||||
self["nosing_length"] = 0
|
||||
|
||||
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type")
|
||||
def update_custom_tread_lock(self, context: bpy.types.Context) -> None:
|
||||
"""When lock is enabled, sync custom treads with tread_run"""
|
||||
if self.custom_tread_lock:
|
||||
self["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
|
||||
|
||||
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type", "custom_tread_lock")
|
||||
|
||||
is_editing: bpy.props.BoolProperty(default=False)
|
||||
width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE")
|
||||
@@ -407,6 +412,12 @@ class BIMStairProperties(PropertyGroup):
|
||||
default="CONCRETE",
|
||||
update=validate_nosing_value,
|
||||
)
|
||||
custom_tread_lock: bpy.props.BoolProperty(
|
||||
name="Lock First/Last Treads to Tread Run",
|
||||
description="When enabled, first and last treads automatically use the Tread Run value",
|
||||
default=True,
|
||||
update=update_custom_tread_lock,
|
||||
)
|
||||
custom_first_last_tread_run: bpy.props.FloatVectorProperty(
|
||||
name="Custom First / Last Treads Widths",
|
||||
description='Specify custom first / last treads widths, different from the general "Tread Run". Leave 0 to disable.',
|
||||
@@ -442,6 +453,7 @@ class BIMStairProperties(PropertyGroup):
|
||||
top_slab_depth: float
|
||||
has_top_nib: bool
|
||||
stair_type: str
|
||||
custom_tread_lock: bool
|
||||
custom_first_last_tread_run: tuple[float, float]
|
||||
nosing_length: float
|
||||
nosing_depth: float
|
||||
@@ -480,8 +492,11 @@ class BIMStairProperties(PropertyGroup):
|
||||
}
|
||||
stair_kwargs.update(generic_props)
|
||||
|
||||
# defined here to appear last in UI
|
||||
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
|
||||
# If locked, use tread_run for both first and last treads
|
||||
if self.custom_tread_lock:
|
||||
stair_kwargs["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
|
||||
else:
|
||||
stair_kwargs["custom_first_last_tread_run"] = self.custom_first_last_tread_run
|
||||
|
||||
if not convert_to_project_units:
|
||||
return stair_kwargs
|
||||
@@ -489,8 +504,24 @@ class BIMStairProperties(PropertyGroup):
|
||||
stair_kwargs = tool.Model.convert_data_to_project_units(stair_kwargs, self.non_si_units_props)
|
||||
return stair_kwargs
|
||||
|
||||
def get_props_kwargs_for_ifc_export(self, convert_to_project_units=False, stair_type=None):
|
||||
"""Get props including custom_tread_lock for saving to IFC"""
|
||||
stair_kwargs = self.get_props_kwargs(convert_to_project_units, stair_type)
|
||||
# Add the lock state for IFC storage (after getting base kwargs to avoid passing to generate function)
|
||||
stair_kwargs["custom_tread_lock"] = self.custom_tread_lock
|
||||
return stair_kwargs
|
||||
|
||||
def set_props_kwargs_from_ifc_data(self, kwargs):
|
||||
kwargs = tool.Model.convert_data_to_si_units(kwargs, self.non_si_units_props)
|
||||
|
||||
# Determine lock state based on whether custom treads match tread_run
|
||||
# If custom_tread_lock wasn't saved (old files), infer it from the data
|
||||
if "custom_tread_lock" not in kwargs:
|
||||
custom_treads = kwargs.get("custom_first_last_tread_run", (0.0, 0.0))
|
||||
tread_run = kwargs.get("tread_run", 0.3)
|
||||
# Lock is off if either custom tread differs from tread_run and is not 0
|
||||
kwargs["custom_tread_lock"] = not any(ct != 0.0 and ct != tread_run for ct in custom_treads)
|
||||
|
||||
for prop_name in kwargs:
|
||||
setattr(self, prop_name, kwargs[prop_name])
|
||||
|
||||
|
||||
@@ -188,7 +188,8 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
ifc_file = tool.Ifc.get()
|
||||
|
||||
stair_data = props.get_props_kwargs(convert_to_project_units=True)
|
||||
# Use the special method that includes custom_tread_lock for IFC storage
|
||||
stair_data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True)
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Stair")
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=element, name="BBIM_Stair")
|
||||
@@ -241,7 +242,8 @@ class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
assert element
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
|
||||
data = props.get_props_kwargs(convert_to_project_units=True)
|
||||
# Use the special method that includes custom_tread_lock for IFC storage
|
||||
data = props.get_props_kwargs_for_ifc_export(convert_to_project_units=True)
|
||||
props.is_editing = False
|
||||
regenerate_stair_mesh(obj)
|
||||
tool.Model.add_body_representation(obj)
|
||||
|
||||
@@ -302,13 +302,38 @@ class BIM_PT_stair(bpy.types.Panel):
|
||||
row.operator("bim.cancel_editing_stair", icon="CANCEL", text="")
|
||||
row = self.layout.row(align=True)
|
||||
for prop_name in props.get_props_kwargs():
|
||||
# Skip custom_tread_lock as it's handled with custom_first_last_tread_run
|
||||
if prop_name == "custom_tread_lock":
|
||||
continue
|
||||
|
||||
prop_value = getattr(props, prop_name)
|
||||
if isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
|
||||
|
||||
# Special handling for custom_first_last_tread_run
|
||||
if prop_name == "custom_first_last_tread_run":
|
||||
# Draw the lock toggle
|
||||
row_lock = self.layout.row(align=True)
|
||||
lock_text = (
|
||||
"Lock First/Last Treads" if not props.custom_tread_lock else "Unlock First/Last Treads"
|
||||
)
|
||||
row_lock.prop(
|
||||
props,
|
||||
"custom_tread_lock",
|
||||
text=lock_text,
|
||||
icon="LOCKED" if props.custom_tread_lock else "UNLOCKED",
|
||||
)
|
||||
|
||||
# Only show the custom values input if unlocked
|
||||
if not props.custom_tread_lock:
|
||||
prop_readable_name = props.bl_rna.properties[prop_name].name
|
||||
self.layout.label(text=f"{prop_readable_name}:")
|
||||
self.layout.prop(props, prop_name, text="")
|
||||
elif isinstance(prop_value, Iterable) and not isinstance(prop_value, str):
|
||||
prop_readable_name = props.bl_rna.properties[prop_name].name
|
||||
self.layout.label(text=f"{prop_readable_name}:")
|
||||
self.layout.prop(props, prop_name, text="")
|
||||
else:
|
||||
self.layout.prop(props, prop_name)
|
||||
|
||||
if prop_name == "height": # Weak but we just want to insert this inside props drawing
|
||||
row_length = self.layout.row(align=True)
|
||||
row_length.prop(props, "total_length_target")
|
||||
|
||||
@@ -2522,6 +2522,7 @@ class FlipClippingPlane(bpy.types.Operator):
|
||||
obj = context.active_object
|
||||
if obj in tool.Project.get_project_props().clipping_planes_objs:
|
||||
obj.rotation_euler[0] += radians(180)
|
||||
obj.rotation_euler[0] %= radians(360)
|
||||
context.view_layer.update()
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -450,9 +450,9 @@ class BIMProjectProperties(PropertyGroup):
|
||||
self.mvd = header_data.mvd
|
||||
self.author_name = header_data.author_name
|
||||
self.author_email = header_data.author_email
|
||||
self.organisation_name = header_data.organization_name
|
||||
self.organisation_email = header_data.organization_email
|
||||
self.authorisation = header_data.authorization
|
||||
self.organisation_name = header_data.organisation_name
|
||||
self.organisation_email = header_data.organisation_email
|
||||
self.authorisation = header_data.authorisation
|
||||
|
||||
if TYPE_CHECKING:
|
||||
is_editing: bool
|
||||
|
||||
@@ -653,4 +653,5 @@ class BIM_PT_purge(Panel):
|
||||
row = layout.row(align=True)
|
||||
row.label(text=f"{object_type.replace('_', ' ').capitalize()}:")
|
||||
row.operator("bim.purge_unused_objects", text="Purge Unused").object_type = object_type
|
||||
row.operator("bim.merge_identical_objects", text="Merge Identical").object_type = object_type
|
||||
merge_op = row.operator("bim.merge_identical_objects", text="Merge Identical")
|
||||
merge_op.object_type = object_type
|
||||
|
||||
@@ -242,14 +242,11 @@ class BIM_PT_object_psets(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
ifc_id = tool.Blender.get_ifc_definition_id(obj)
|
||||
if not ifc_id:
|
||||
return False
|
||||
if not tool.Ifc.get_object_by_identifier(ifc_id):
|
||||
return False
|
||||
return True
|
||||
return (
|
||||
(obj := tool.Blender.get_active_object())
|
||||
and (element := tool.Ifc.get_entity(obj))
|
||||
and element.is_a("IfcObjectDefinition")
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
if not ObjectPsetsData.is_loaded:
|
||||
@@ -388,14 +385,11 @@ class BIM_PT_object_qtos(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not (obj := context.active_object):
|
||||
return False
|
||||
ifc_id = tool.Blender.get_ifc_definition_id(obj)
|
||||
if not ifc_id:
|
||||
return False
|
||||
if not tool.Ifc.get_object_by_identifier(ifc_id):
|
||||
return False
|
||||
return True
|
||||
return (
|
||||
(obj := tool.Blender.get_active_object())
|
||||
and (element := tool.Ifc.get_entity(obj))
|
||||
and element.is_a("IfcObjectDefinition")
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
if not ObjectQtosData.is_loaded:
|
||||
|
||||
@@ -317,6 +317,7 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
f"Mesh '{obj.data.name}' has loose geometry, loose geometry will be ignored to save mesh to IFC as a tessellation.",
|
||||
)
|
||||
|
||||
representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context)
|
||||
element = core.assign_class(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
@@ -326,13 +327,9 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
predefined_type=predefined_type,
|
||||
should_add_representation=False,
|
||||
)
|
||||
representation = tool.Geometry.export_mesh_to_tessellation(obj, ifc_context)
|
||||
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=representation,
|
||||
tool.Ifc, tool.Geometry, obj=obj, representation=representation
|
||||
)
|
||||
else:
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ class ColourByPropertyData:
|
||||
elif pset.endswith("BaseQuantities"):
|
||||
keys.extend([f'/.*BaseQuantities/."{name}"' for name in properties.keys() if name != "id"])
|
||||
else:
|
||||
keys.extend([f"{pset}.{name}" for name in properties.keys() if name != "id"])
|
||||
keys.extend([f'"{pset}"."{name}"' for name in properties.keys() if name != "id"])
|
||||
results = [(k, k, "") for k in keys]
|
||||
return default + results
|
||||
|
||||
|
||||
@@ -523,8 +523,12 @@ class BIMWorkScheduleProperties(PropertyGroup):
|
||||
active_task_input_index: IntProperty(name="Active Task Input Index")
|
||||
task_outputs: CollectionProperty(name="Task Outputs", type=TaskProduct)
|
||||
active_task_output_index: IntProperty(name="Active Task Output Index")
|
||||
show_nested_outputs: BoolProperty(name="Show Nested Task Elements", default=False, update=update_active_task_outputs)
|
||||
show_nested_resources: BoolProperty(name="Show Nested Task Elements", default=False, update=update_active_task_resources)
|
||||
show_nested_outputs: BoolProperty(
|
||||
name="Show Nested Task Elements", default=False, update=update_active_task_outputs
|
||||
)
|
||||
show_nested_resources: BoolProperty(
|
||||
name="Show Nested Task Elements", default=False, update=update_active_task_resources
|
||||
)
|
||||
show_nested_inputs: BoolProperty(name="Show Nested Task Elements", default=False, update=update_active_task_inputs)
|
||||
product_input_tasks: CollectionProperty(name="Product Task Inputs", type=TaskProduct)
|
||||
product_output_tasks: CollectionProperty(name="Product Task Outputs", type=TaskProduct)
|
||||
|
||||
@@ -34,14 +34,11 @@ class BIM_PT_type(Panel):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if not context.active_object:
|
||||
return False
|
||||
element = tool.Ifc.get_entity(context.active_object)
|
||||
if not element:
|
||||
return False
|
||||
if not element.is_a("IfcProduct") and not element.is_a("IfcTypeProduct"):
|
||||
return True
|
||||
return True
|
||||
return (
|
||||
(obj := tool.Blender.get_active_object())
|
||||
and (element := tool.Ifc.get_entity(obj))
|
||||
and (element.is_a("IfcObject") or element.is_a("IfcTypeObject"))
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
if not TypeData.is_loaded:
|
||||
|
||||
@@ -108,10 +108,6 @@ def add_sheet(ifc: type[tool.Ifc], drawing: type[tool.Drawing], titleblock: ifco
|
||||
def regenerate_sheet(
|
||||
drawing: type[tool.Drawing], sheet: ifcopenshell.entity_instance
|
||||
) -> Union[list[tool.Drawing.SheetWarningType], None]:
|
||||
warnings = drawing.validate_sheet_files(sheet)
|
||||
if warnings:
|
||||
return warnings
|
||||
|
||||
titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK")
|
||||
assert titleblock_uri
|
||||
|
||||
@@ -320,6 +316,7 @@ def duplicate_drawing(
|
||||
) -> ifcopenshell.entity_instance:
|
||||
drawing_name = drawing_tool.ensure_unique_drawing_name(drawing_tool.get_name(drawing))
|
||||
new_drawing = ifc.run("root.copy_class", product=drawing)
|
||||
drawing_tool.clear_annotation_relationships(new_drawing)
|
||||
drawing_tool.copy_representation(drawing, new_drawing)
|
||||
drawing_tool.set_name(new_drawing, drawing_name)
|
||||
group = drawing_tool.get_drawing_group(new_drawing)
|
||||
@@ -475,46 +472,44 @@ def sync_references(
|
||||
if not drawing_tool.has_annotation(drawing):
|
||||
return
|
||||
|
||||
context = drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))
|
||||
if not context:
|
||||
if not (context := drawing_tool.get_annotation_context(drawing_tool.get_drawing_target_view(drawing))):
|
||||
return
|
||||
|
||||
group = drawing_tool.get_drawing_group(drawing)
|
||||
for reference_element in drawing_tool.get_potential_reference_elements(drawing):
|
||||
potential_reference_elements = drawing_tool.get_potential_reference_elements(drawing)
|
||||
|
||||
for element in potential_reference_elements:
|
||||
if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
|
||||
drawing_tool.sync_object_placement(obj)
|
||||
|
||||
for element in drawing_tool.get_group_elements(group):
|
||||
if not drawing_tool.is_auto_annotation(element):
|
||||
continue
|
||||
if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
|
||||
drawing_tool.sync_object_placement(obj)
|
||||
if not (reference_element := drawing_tool.get_assigned_product(element)):
|
||||
if obj := ifc.get_object(element):
|
||||
drawing_tool.delete_object(obj)
|
||||
ifc.run("root.remove_product", product=element)
|
||||
continue
|
||||
reference_obj = ifc.get_object(reference_element)
|
||||
annotation = drawing_tool.get_drawing_reference_annotation(drawing, reference_element)
|
||||
if reference_element not in potential_reference_elements:
|
||||
# It was auto created, so it makes sense to auto delete
|
||||
if obj := ifc.get_object(element):
|
||||
drawing_tool.delete_object(obj)
|
||||
ifc.run("root.remove_product", product=element)
|
||||
elif not drawing_tool.regenerate_reference_annotation(drawing, element, reference_element, context):
|
||||
if obj := ifc.get_object(element):
|
||||
drawing_tool.delete_object(obj)
|
||||
ifc.run("root.remove_product", product=element)
|
||||
|
||||
should_delete_existing_annotation = False
|
||||
should_create_annotation = False
|
||||
|
||||
# remove annotation only if the reference object was changed
|
||||
# otherwise we rely on the existing annotation
|
||||
if annotation:
|
||||
if reference_obj and (ifc.is_moved(reference_obj) or ifc.is_edited(reference_obj)):
|
||||
should_delete_existing_annotation = True
|
||||
|
||||
if should_delete_existing_annotation or not annotation:
|
||||
should_create_annotation = True
|
||||
|
||||
if should_delete_existing_annotation:
|
||||
annotation_obj = ifc.get_object(annotation)
|
||||
if annotation_obj:
|
||||
drawing_tool.delete_object(annotation_obj)
|
||||
ifc.run("root.remove_product", product=annotation)
|
||||
|
||||
if should_create_annotation:
|
||||
annotation = drawing_tool.generate_reference_annotation(drawing, reference_element, context)
|
||||
if annotation:
|
||||
for reference_element in potential_reference_elements:
|
||||
if not drawing_tool.get_drawing_reference_annotation(drawing, reference_element):
|
||||
if annotation := drawing_tool.generate_reference_annotation(drawing, reference_element, context):
|
||||
ifc.run("drawing.assign_product", relating_product=reference_element, related_object=annotation)
|
||||
ifc.run("group.assign_group", group=group, products=[annotation])
|
||||
collector.assign(ifc.get_object(annotation))
|
||||
|
||||
if reference_obj and ifc.is_moved(reference_obj):
|
||||
drawing_tool.sync_object_placement(reference_obj)
|
||||
|
||||
if reference_obj and ifc.is_edited(reference_obj):
|
||||
drawing_tool.sync_object_representation(reference_obj)
|
||||
|
||||
|
||||
def select_assigned_product(drawing: type[tool.Drawing], context: bpy.types.Context) -> None:
|
||||
drawing.select_assigned_product(context)
|
||||
|
||||
@@ -1506,28 +1506,21 @@ class Blender(bonsai.core.tool.Blender):
|
||||
return bpy.context.preferences.addons[blender_package_name].preferences
|
||||
|
||||
@classmethod
|
||||
def get_sun_position_addon(cls) -> Union[types.ModuleType, None]:
|
||||
# Check if it's installed as legacy Blender addon.
|
||||
def get_addon(cls, name: str) -> Union[types.ModuleType, None]:
|
||||
import importlib
|
||||
|
||||
try:
|
||||
sun_position = importlib.import_module("sun_position")
|
||||
return importlib.import_module(name) # Legacy Blender addon
|
||||
except ImportError:
|
||||
sun_position = None
|
||||
|
||||
if sun_position:
|
||||
return sun_position
|
||||
pass
|
||||
|
||||
for package_name in bpy.context.preferences.addons.keys():
|
||||
if package_name.endswith(".sun_position"):
|
||||
if package_name.endswith(f".{name}"):
|
||||
try:
|
||||
sun_position = importlib.import_module(package_name)
|
||||
return sun_position
|
||||
return importlib.import_module(package_name)
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
return sun_position
|
||||
|
||||
@classmethod
|
||||
def get_sun_props(cls) -> Union[SunPosProperties, None]:
|
||||
assert (scene := bpy.context.scene)
|
||||
@@ -1889,6 +1882,24 @@ class Blender(bonsai.core.tool.Blender):
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = previously_active
|
||||
|
||||
@classmethod
|
||||
def sync_render_visibility(cls):
|
||||
# Doing bpy.ops.object.hide_render_clear_all() or
|
||||
# bpy.ops.object.isolate_type_render() is extremely slow.
|
||||
# Hopefully this doesn't crash on Windows, it doesn't crash on Linux.
|
||||
should_hides = [0 if obj.visible_get() else 1 for obj in bpy.data.objects]
|
||||
should_hides = np.fromiter(should_hides, dtype=np.uint8, count=len(should_hides))
|
||||
bpy.data.objects.foreach_set("hide_render", should_hides)
|
||||
return # Otherwise...
|
||||
# for obj in bpy.data.objects:
|
||||
# if not obj.data:
|
||||
# continue
|
||||
# # For speed, check equality prior to change to prevent needless updates
|
||||
# if (is_visible := obj.visible_get()) and obj.hide_render is True:
|
||||
# obj.hide_render = False
|
||||
# elif not is_visible and obj.hide_render is False:
|
||||
# obj.hide_render = True
|
||||
|
||||
@classmethod
|
||||
def hide_objects(cls, objs):
|
||||
previously_selected = {o.name for o in bpy.context.selected_objects}
|
||||
|
||||
@@ -51,9 +51,13 @@ class Collector(bonsai.core.tool.Collector):
|
||||
if tool.Geometry.is_locked(element):
|
||||
tool.Geometry.lock_object(obj)
|
||||
element = (element.PartOfU or element.PartOfV or element.PartOfW)[0]
|
||||
if not tool.Spatial.get_grid_props().is_visible:
|
||||
obj.hide_viewport = True
|
||||
elif element.is_a("IfcGrid"):
|
||||
if tool.Geometry.is_locked(element):
|
||||
tool.Geometry.lock_object(obj)
|
||||
if not tool.Spatial.get_grid_props().is_visible:
|
||||
obj.hide_viewport = True
|
||||
|
||||
if element.is_a("IfcProject"):
|
||||
if tool.Geometry.is_locked(element):
|
||||
@@ -69,7 +73,8 @@ class Collector(bonsai.core.tool.Collector):
|
||||
tool.Geometry.lock_object(obj)
|
||||
collection = cls._create_project_child_collection("IfcSpace")
|
||||
cls.link_collection_object_safe(collection, obj)
|
||||
obj.hide_viewport = True
|
||||
if not tool.Spatial.get_spatial_props().is_visible:
|
||||
obj.hide_viewport = True
|
||||
elif element.is_a("IfcStructuralItem"):
|
||||
collection = cls._create_project_child_collection("IfcStructuralItem")
|
||||
cls.link_collection_object_safe(collection, obj)
|
||||
@@ -93,7 +98,8 @@ class Collector(bonsai.core.tool.Collector):
|
||||
cls.link_collection_object_safe(collection, obj)
|
||||
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
|
||||
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
|
||||
obj.hide_viewport = True
|
||||
if not tool.Spatial.get_spatial_props().is_visible:
|
||||
obj.hide_viewport = True
|
||||
elif (
|
||||
tool.Ifc.get_schema() != "IFC2X3"
|
||||
and element.is_a("IfcSpatialElement")
|
||||
@@ -105,7 +111,8 @@ class Collector(bonsai.core.tool.Collector):
|
||||
cls.link_collection_object_safe(collection, obj)
|
||||
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
|
||||
cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection)
|
||||
obj.hide_viewport = True
|
||||
if not tool.Spatial.get_spatial_props().is_visible:
|
||||
obj.hide_viewport = True
|
||||
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
||||
if collection := cls._create_own_collection(obj):
|
||||
cls.link_collection_object_safe(collection, obj)
|
||||
|
||||
@@ -273,18 +273,16 @@ class Cost(bonsai.core.tool.Cost):
|
||||
return
|
||||
props = cls.get_cost_props()
|
||||
props.cost_item_type_products.clear()
|
||||
# TODO implement process and resource types
|
||||
# props.cost_item_processes.clear()
|
||||
# props.cost_item_resources.clear()
|
||||
props.cost_item_processes.clear()
|
||||
props.cost_item_resources.clear()
|
||||
for rel in cost_item.Controls or []:
|
||||
for related_object in rel.RelatedObjects:
|
||||
if related_object.is_a("IfcTypeProduct"):
|
||||
new = props.cost_item_type_products.add()
|
||||
# TODO implement process and resource types
|
||||
# elif related_object.is_a("IfcProcess"):
|
||||
# new = props.cost_item_processes.add()
|
||||
# elif related_object.is_a("IfcResource"):
|
||||
# new = props.cost_item_resources.add()
|
||||
elif related_object.is_a("IfcProcess"):
|
||||
new = props.cost_item_processes.add()
|
||||
elif related_object.is_a("IfcResource"):
|
||||
new = props.cost_item_resources.add()
|
||||
new.ifc_definition_id = related_object.id()
|
||||
new.name = related_object.Name or "Unnamed"
|
||||
|
||||
|
||||
+104
-17
@@ -18,6 +18,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import bmesh
|
||||
import bpy
|
||||
@@ -33,7 +34,7 @@ import bonsai.tool as tool
|
||||
from bonsai.bim.ifc import IfcStore
|
||||
from mathutils import Vector
|
||||
from collections import defaultdict
|
||||
from typing import Literal, TYPE_CHECKING, assert_never
|
||||
from typing import Literal, TYPE_CHECKING, assert_never, Union
|
||||
from collections.abc import Iterable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -136,12 +137,30 @@ class Debug(bonsai.core.tool.Debug):
|
||||
"PERSON",
|
||||
"PERSON_AND_ORGANIZATION",
|
||||
],
|
||||
by_name_or_identification_only: bool = False,
|
||||
) -> dict[str, list[str]]:
|
||||
"""Merge identical objects.
|
||||
|
||||
Note that Styles UI (or other UI) should be updated manually after using this method.
|
||||
|
||||
Args:
|
||||
object_type: The type of object to merge
|
||||
by_name_or_identification_only: If True, merge based only on Name attribute (or equivalent identifier).
|
||||
Strips .XXX suffix patterns (e.g., 'foo.001' matches 'foo', 'foo.002').
|
||||
For PERSON, uses Identification. For APPLICATION, uses ApplicationFullName.
|
||||
For PERSON_AND_ORGANIZATION, uses combination of person and organization identifiers.
|
||||
"""
|
||||
|
||||
def normalize_name(name: str) -> str:
|
||||
"""Remove .XXX suffix pattern from names (e.g., 'foo.001' -> 'foo')"""
|
||||
if not name:
|
||||
return ""
|
||||
# Match pattern: name ending with .digits
|
||||
match = re.match(r"^(.+)\.\d+$", name)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return name
|
||||
|
||||
def get_hash(element: ifcopenshell.entity_instance) -> int:
|
||||
data = element.get_info_2(include_identifier=False, recursive=True)
|
||||
if object_type == "APPLICATION":
|
||||
@@ -152,6 +171,49 @@ class Debug(bonsai.core.tool.Debug):
|
||||
data["TheOrganization"] = element.TheOrganization.id()
|
||||
return hash(json.dumps(data, sort_keys=True))
|
||||
|
||||
def get_name_key(element: ifcopenshell.entity_instance) -> str:
|
||||
"""Get key based on name/identifier attribute for the given object type"""
|
||||
if object_type == "STYLE":
|
||||
name = element.Name if element.Name else ""
|
||||
return normalize_name(name)
|
||||
elif object_type == "MATERIAL":
|
||||
name = element.Name if element.Name else ""
|
||||
return normalize_name(name)
|
||||
elif object_type == "ORGANIZATION":
|
||||
name = element.Name if element.Name else ""
|
||||
return normalize_name(name)
|
||||
elif object_type == "APPLICATION":
|
||||
name = element.ApplicationFullName if element.ApplicationFullName else ""
|
||||
return normalize_name(name)
|
||||
elif object_type == "PERSON":
|
||||
ident = element.Identification if element.Identification else ""
|
||||
return normalize_name(ident)
|
||||
elif object_type == "PERSON_AND_ORGANIZATION":
|
||||
person_id = element.ThePerson.Identification if element.ThePerson.Identification else ""
|
||||
org_name = element.TheOrganization.Name if element.TheOrganization.Name else ""
|
||||
return f"{normalize_name(person_id)}|{normalize_name(org_name)}"
|
||||
else:
|
||||
assert_never(object_type)
|
||||
|
||||
def get_element_name(element: ifcopenshell.entity_instance) -> str:
|
||||
"""Get the actual name/identifier from element for sorting purposes"""
|
||||
if object_type == "STYLE":
|
||||
return element.Name if element.Name else ""
|
||||
elif object_type == "MATERIAL":
|
||||
return element.Name if element.Name else ""
|
||||
elif object_type == "ORGANIZATION":
|
||||
return element.Name if element.Name else ""
|
||||
elif object_type == "APPLICATION":
|
||||
return element.ApplicationFullName if element.ApplicationFullName else ""
|
||||
elif object_type == "PERSON":
|
||||
return element.Identification if element.Identification else ""
|
||||
elif object_type == "PERSON_AND_ORGANIZATION":
|
||||
person_id = element.ThePerson.Identification if element.ThePerson.Identification else ""
|
||||
org_name = element.TheOrganization.Name if element.TheOrganization.Name else ""
|
||||
return f"{person_id}|{org_name}"
|
||||
else:
|
||||
assert_never(object_type)
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
merged_element_types: dict[str, list[str]] = {}
|
||||
|
||||
@@ -179,22 +241,47 @@ class Debug(bonsai.core.tool.Debug):
|
||||
for element_type in element_types:
|
||||
elements = ifc_file.by_type(element_type, include_subtypes=False)
|
||||
|
||||
# Calculate hashes.
|
||||
hash_to_elements: defaultdict[int, list[ifcopenshell.entity_instance]] = defaultdict(list)
|
||||
for element in elements:
|
||||
# Except for styles, ignore unnamed elements as they may be not safe to merge
|
||||
merge_optional_names = ("STYLE", "PERSON")
|
||||
not_optional_name = ("APPLICATION", "ORGANIZATION")
|
||||
has_no_name = ("PERSON_AND_ORGANIZATION",)
|
||||
if (
|
||||
object_type not in merge_optional_names
|
||||
and object_type not in not_optional_name
|
||||
and object_type not in has_no_name
|
||||
and not element.Name
|
||||
):
|
||||
continue
|
||||
element_hash = get_hash(element)
|
||||
hash_to_elements[element_hash].append(element)
|
||||
# Calculate hashes or name keys.
|
||||
hash_to_elements: defaultdict[Union[int, str], list[ifcopenshell.entity_instance]]
|
||||
|
||||
if by_name_or_identification_only:
|
||||
# Group by name/identifier only (with .XXX suffix normalization)
|
||||
hash_to_elements = defaultdict(list)
|
||||
for element in elements:
|
||||
name_key = get_name_key(element)
|
||||
# Skip elements without a valid identifier
|
||||
if not name_key:
|
||||
continue
|
||||
hash_to_elements[name_key].append(element)
|
||||
|
||||
# Sort elements within each group to keep the one without suffix (or lowest suffix)
|
||||
for name_key in hash_to_elements:
|
||||
# Sort by: 1) prefer names without .XXX suffix, 2) then by original name
|
||||
def sort_key(el):
|
||||
name = get_element_name(el)
|
||||
# Check if name has .XXX suffix
|
||||
has_suffix = bool(re.match(r"^.+\.\d+$", name))
|
||||
# Return tuple: (has_suffix, name) - sort by no suffix first, then alphabetically
|
||||
return (has_suffix, name)
|
||||
|
||||
hash_to_elements[name_key].sort(key=sort_key)
|
||||
else:
|
||||
# Group by full hash
|
||||
hash_to_elements = defaultdict(list)
|
||||
for element in elements:
|
||||
# Except for styles, ignore unnamed elements as they may be not safe to merge
|
||||
merge_optional_names = ("STYLE", "PERSON")
|
||||
not_optional_name = ("APPLICATION", "ORGANIZATION")
|
||||
has_no_name = ("PERSON_AND_ORGANIZATION",)
|
||||
if (
|
||||
object_type not in merge_optional_names
|
||||
and object_type not in not_optional_name
|
||||
and object_type not in has_no_name
|
||||
and not element.Name
|
||||
):
|
||||
continue
|
||||
element_hash = get_hash(element)
|
||||
hash_to_elements[element_hash].append(element)
|
||||
|
||||
merged_elements_names: list[str] = []
|
||||
# Merge elements.
|
||||
|
||||
+498
-280
@@ -208,6 +208,27 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def get_annotation_drawing(cls, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance | None:
|
||||
for rel in element.HasAssignments:
|
||||
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.ObjectType == "DRAWING":
|
||||
for e in rel.RelatedObjects:
|
||||
if e.ObjectType == "DRAWING":
|
||||
return e
|
||||
|
||||
@classmethod
|
||||
def exclude_annotation_from_drawing(
|
||||
cls, element: ifcopenshell.entity_instance, drawing: ifcopenshell.entity_instance
|
||||
) -> None:
|
||||
pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing")
|
||||
if not pset:
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc_file, product=drawing, name="EPset_Drawing")
|
||||
exclude = ifcopenshell.util.element.get_property_definition(pset, prop="Exclude") or ""
|
||||
if exclude:
|
||||
exclude += "+"
|
||||
exclude += element.GlobalId
|
||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Exclude": exclude})
|
||||
|
||||
@classmethod
|
||||
def ensure_annotation_in_drawing_plane(
|
||||
cls, obj: bpy.types.Object, camera: Optional[bpy.types.Object] = None
|
||||
@@ -218,11 +239,7 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
entity = tool.Ifc.get_entity(obj)
|
||||
if not entity:
|
||||
return
|
||||
for rel in entity.HasAssignments:
|
||||
if rel.is_a("IfcRelAssignsToGroup"):
|
||||
for e in rel.RelatedObjects:
|
||||
if e.ObjectType == "DRAWING":
|
||||
return tool.Ifc.get_object(e)
|
||||
return tool.Ifc.get_object(cls.get_annotation_drawing(entity))
|
||||
|
||||
if not camera:
|
||||
camera = get_camera_from_annotation_object(obj) or bpy.context.scene.camera
|
||||
@@ -236,7 +253,11 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
|
||||
@classmethod
|
||||
def setup_annotation_object(
|
||||
cls, obj: bpy.types.Object, object_type: str, related_object: Optional[bpy.types.Object] = None
|
||||
cls,
|
||||
obj: bpy.types.Object,
|
||||
object_type: str,
|
||||
related_object: Optional[bpy.types.Object] = None,
|
||||
rotation_mode: str = "NONE",
|
||||
) -> None:
|
||||
"""Finish object's adjustments after both object and entity are created"""
|
||||
|
||||
@@ -308,6 +329,55 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
ifc_file, relating_product=related_entity, related_object=obj_entity
|
||||
)
|
||||
|
||||
if rotation_mode != "NONE" and related_object:
|
||||
cls.apply_annotation_rotation(obj, related_object, rotation_mode)
|
||||
|
||||
@classmethod
|
||||
def apply_annotation_rotation(
|
||||
cls, tag_obj: bpy.types.Object, related_object: bpy.types.Object, rotation_mode: str
|
||||
) -> None:
|
||||
"""Apply rotation to annotation based on the selected rotation mode"""
|
||||
camera = bpy.context.scene.camera
|
||||
camera_right = camera.matrix_world.to_3x3() @ mathutils.Vector((1, 0, 0))
|
||||
|
||||
if rotation_mode == "CAMERA_Horizontal":
|
||||
location = tag_obj.location.copy()
|
||||
camera_matrix = camera.matrix_world.copy()
|
||||
camera_matrix.translation = location
|
||||
tag_obj.matrix_world = camera_matrix
|
||||
|
||||
elif rotation_mode == "CAMERA_Vertical":
|
||||
location = tag_obj.location.copy()
|
||||
camera_matrix = camera.matrix_world.copy()
|
||||
camera_matrix.translation = location
|
||||
rotation_90z = mathutils.Matrix.Rotation(math.pi / 2, 4, "Z")
|
||||
camera_matrix = camera_matrix @ rotation_90z
|
||||
tag_obj.matrix_world = camera_matrix
|
||||
|
||||
elif rotation_mode == "LOCAL_X":
|
||||
local_x = related_object.matrix_world.to_3x3() @ mathutils.Vector((1, 0, 0))
|
||||
local_x = local_x.normalized()
|
||||
if local_x.dot(camera_right) < 0:
|
||||
local_x = -local_x
|
||||
|
||||
tag_obj.rotation_euler = local_x.to_track_quat("X", "Z").to_euler()
|
||||
|
||||
elif rotation_mode == "LOCAL_Y":
|
||||
local_y = related_object.matrix_world.to_3x3() @ mathutils.Vector((0, 1, 0))
|
||||
local_y = local_y.normalized()
|
||||
if local_y.dot(camera_right) < 0:
|
||||
local_y = -local_y
|
||||
|
||||
tag_obj.rotation_euler = local_y.to_track_quat("X", "Z").to_euler()
|
||||
|
||||
elif rotation_mode == "LOCAL_Z":
|
||||
local_z = related_object.matrix_world.to_3x3() @ mathutils.Vector((0, 0, 1))
|
||||
local_z = local_z.normalized()
|
||||
if local_z.dot(camera_right) < 0:
|
||||
local_z = -local_z
|
||||
|
||||
tag_obj.rotation_euler = local_z.to_track_quat("X", "Z").to_euler()
|
||||
|
||||
@classmethod
|
||||
def is_annotation_object_type(
|
||||
cls, element: ifcopenshell.entity_instance, object_types: Union[str, Sequence[str]]
|
||||
@@ -815,7 +885,13 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
def get_assigned_product(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
|
||||
for rel in element.HasAssignments:
|
||||
if rel.is_a("IfcRelAssignsToProduct"):
|
||||
return rel.RelatingProduct
|
||||
product = rel.RelatingProduct
|
||||
if product.is_a("IfcGrid") and rel.Name:
|
||||
for attribute in ("UAxes", "VAxes", "WAxes"):
|
||||
for axis in getattr(product, attribute) or []:
|
||||
if axis.AxisTag == rel.Name:
|
||||
return axis
|
||||
return product
|
||||
|
||||
@classmethod
|
||||
def import_annotations_in_group(cls, group: ifcopenshell.entity_instance) -> None:
|
||||
@@ -1401,7 +1477,10 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
cls, drawing: ifcopenshell.entity_instance
|
||||
) -> list[ifcopenshell.entity_instance]:
|
||||
elements = []
|
||||
existing_references = cls.get_group_elements(cls.get_drawing_group(drawing))
|
||||
existing_references = set(cls.get_group_elements(cls.get_drawing_group(drawing)))
|
||||
if exclude := ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing", "Exclude"):
|
||||
existing_references.update(ifcopenshell.util.selector.filter_elements(tool.Ifc.get(), exclude))
|
||||
|
||||
for element in tool.Ifc.get().by_type("IfcAnnotation"):
|
||||
if element in existing_references or element == drawing:
|
||||
continue
|
||||
@@ -1411,14 +1490,25 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
"GlobalReferencing", False
|
||||
):
|
||||
elements.append(element)
|
||||
for element in tool.Ifc.get().by_type("IfcGridAxis"):
|
||||
elements.append(element)
|
||||
for element in tool.Ifc.get().by_type("IfcGrid"):
|
||||
if element in existing_references:
|
||||
continue
|
||||
for axis in element.UAxes + element.VAxes + (element.WAxes or tuple()):
|
||||
if axis in existing_references:
|
||||
continue
|
||||
elements.append(axis)
|
||||
target_view = tool.Drawing.get_drawing_target_view(drawing)
|
||||
if target_view in ("SECTION_VIEW", "ELEVATION_VIEW"):
|
||||
for element in tool.Ifc.get().by_type("IfcBuildingStorey"):
|
||||
if element in existing_references:
|
||||
continue
|
||||
elements.append(element)
|
||||
return elements
|
||||
|
||||
@classmethod
|
||||
def is_auto_annotation(cls, element: ifcopenshell.entity_instance):
|
||||
return element.is_a("IfcAnnotation") and element.ObjectType in ("GRID", "SECTION", "ELEVATION", "SECTION_LEVEL")
|
||||
|
||||
@classmethod
|
||||
def get_drawing_reference_annotation(
|
||||
cls, drawing: ifcopenshell.entity_instance, reference_element: ifcopenshell.entity_instance
|
||||
@@ -1430,7 +1520,7 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
# IfcRelAssignsToProduct.RelatingProduct = IfcGrid
|
||||
# IfcRelAssignsToProduct.Name = IfcGridAxis.AxisTag
|
||||
grid = None
|
||||
for attribute in ("PartOfW", "PartOfV", "PartOfU"):
|
||||
for attribute in ("PartOfU", "PartOfV", "PartOfW"):
|
||||
if getattr(reference_element, attribute, None):
|
||||
grid = getattr(reference_element, attribute)[0]
|
||||
break
|
||||
@@ -1452,6 +1542,26 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct == reference_element:
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def regenerate_reference_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
annotation: ifcopenshell.entity_instance,
|
||||
reference_element: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
if reference_element.is_a("IfcGridAxis"):
|
||||
return cls.regenerate_grid_axis_reference_annotation(drawing, annotation, reference_element, context)
|
||||
elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING":
|
||||
target_view = ifcopenshell.util.element.get_pset(reference_element, "EPset_Drawing", "TargetView")
|
||||
if target_view == "ELEVATION_VIEW":
|
||||
return cls.regenerate_elevation_reference_annotation(drawing, annotation, reference_element, context)
|
||||
elif target_view == "SECTION_VIEW":
|
||||
return cls.regenerate_section_reference_annotation(drawing, annotation, reference_element, context)
|
||||
elif reference_element.is_a("IfcBuildingStorey"):
|
||||
return cls.regenerate_storey_annotation(drawing, annotation, reference_element, context)
|
||||
return annotation
|
||||
|
||||
@classmethod
|
||||
def generate_reference_annotation(
|
||||
cls,
|
||||
@@ -1461,333 +1571,410 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
) -> ifcopenshell.entity_instance:
|
||||
if reference_element.is_a("IfcGridAxis"):
|
||||
return cls.generate_grid_axis_reference_annotation(drawing, reference_element, context)
|
||||
|
||||
elif reference_element.is_a("IfcAnnotation") and reference_element.ObjectType == "DRAWING":
|
||||
|
||||
def ensure_referenced_drawing_obj_exists(drawing: ifcopenshell.entity_instance):
|
||||
obj = tool.Ifc.get_object(drawing)
|
||||
if obj is None:
|
||||
# Annotations in that drawing are lazy loaded as needed
|
||||
obj = cls.import_drawing(drawing)
|
||||
tool.Blender.get_layer_collection(obj.users_collection[0]).hide_viewport = True
|
||||
|
||||
target_view = ifcopenshell.util.element.get_pset(reference_element, "EPset_Drawing", "TargetView")
|
||||
if target_view == "ELEVATION_VIEW":
|
||||
ensure_referenced_drawing_obj_exists(reference_element)
|
||||
return cls.generate_elevation_reference_annotation(drawing, reference_element, context)
|
||||
elif target_view == "SECTION_VIEW":
|
||||
ensure_referenced_drawing_obj_exists(reference_element)
|
||||
return cls.generate_section_reference_annotation(drawing, reference_element, context)
|
||||
|
||||
elif reference_element.is_a("IfcBuildingStorey"):
|
||||
return cls.generate_storey_annotation(drawing, reference_element, context)
|
||||
|
||||
@classmethod
|
||||
def generate_storey_points(
|
||||
cls, drawing: ifcopenshell.entity_instance, storey: ifcopenshell.entity_instance
|
||||
) -> list | None:
|
||||
import bonsai.bim.module.drawing.helper as helper
|
||||
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
if camera.data.type != "ORTHO":
|
||||
return
|
||||
if not cls.is_matrix_perpendicular(camera.matrix_world, Matrix()):
|
||||
return
|
||||
|
||||
xmin, xmax, ymin, ymax = helper.ortho_view_frame(camera.data)[:4]
|
||||
rl = ifcopenshell.util.placement.get_local_placement(storey.ObjectPlacement)[2][3]
|
||||
y = (camera.matrix_world.inverted() @ Vector((0.0, 0.0, rl))).y
|
||||
if y < ymin or y > ymax:
|
||||
return
|
||||
|
||||
return (Vector((xmax, y, 0.0)), Vector((xmin, y, 0.0)))
|
||||
|
||||
@classmethod
|
||||
def generate_storey_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
reference_element: ifcopenshell.entity_instance,
|
||||
storey: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
if not (points := cls.generate_storey_points(drawing, storey)):
|
||||
return
|
||||
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
mesh = bpy.data.meshes.new("Mesh")
|
||||
obj = bpy.data.objects.new(storey.Name or "Unnamed", mesh)
|
||||
obj.matrix_world = cls.get_default_annotation_matrix(camera)
|
||||
element = cls.run_root_assign_class(
|
||||
obj=obj, ifc_class="IfcAnnotation", predefined_type="SECTION_LEVEL", should_add_representation=False
|
||||
)
|
||||
tool.Geometry.run_edit_object_placement(obj)
|
||||
element.Name = storey.Name or "Unnamed"
|
||||
builder = ShapeBuilder(tool.Ifc.get())
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
points = [p / unit_scale for p in points]
|
||||
representation = builder.get_representation(context, [builder.polyline(points)])
|
||||
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
|
||||
bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def regenerate_storey_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
annotation: ifcopenshell.entity_instance,
|
||||
storey: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
if not (points := cls.generate_storey_points(drawing, storey)):
|
||||
return
|
||||
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, annotation)
|
||||
m = ifcopenshell.util.shape.get_shape_matrix(shape)
|
||||
mw = cls.get_default_annotation_matrix(camera)
|
||||
existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
|
||||
|
||||
new_points = None
|
||||
if not np.allclose(m, np.array(mw), atol=1e-4):
|
||||
new_points = points
|
||||
elif len(existing_verts) != 2:
|
||||
new_points = points
|
||||
else:
|
||||
existing_verts = sorted(existing_verts, key=lambda v: v.x)
|
||||
xmin, xmax = [v.x for v in existing_verts]
|
||||
y = points[0].y
|
||||
if not tool.Cad.is_x(y, existing_verts[0].y) or not tool.Cad.is_x(y, existing_verts[1].y):
|
||||
new_points = (Vector((xmax, y, 0.0)), Vector((xmin, y, 0.0)))
|
||||
|
||||
if new_points:
|
||||
if representation := ifcopenshell.util.representation.get_representation(annotation, context):
|
||||
ifcopenshell.api.geometry.unassign_representation(
|
||||
tool.Ifc.get(), product=annotation, representation=representation
|
||||
)
|
||||
builder = ShapeBuilder(tool.Ifc.get())
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
new_points = [p / unit_scale for p in new_points]
|
||||
representation = builder.get_representation(context, [builder.polyline(new_points)])
|
||||
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation)
|
||||
|
||||
if obj := tool.Ifc.get_object(annotation):
|
||||
obj.matrix_world = mw
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc, tool.Geometry, obj=obj, representation=representation
|
||||
)
|
||||
else:
|
||||
ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw))
|
||||
|
||||
annotation.Name = storey.Name or "Unnamed"
|
||||
return annotation
|
||||
|
||||
@classmethod
|
||||
def generate_section_reference_points(
|
||||
cls, drawing: ifcopenshell.entity_instance, section: ifcopenshell.entity_instance
|
||||
) -> list | None:
|
||||
import bonsai.bim.module.drawing.helper as helper
|
||||
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
assert isinstance(camera, bpy.types.Object)
|
||||
assert isinstance((camera_data := camera.data), bpy.types.Camera)
|
||||
props = tool.Drawing.get_camera_props(camera_data)
|
||||
if camera.data.type != "ORTHO":
|
||||
return
|
||||
settings = ifcopenshell.geom.settings()
|
||||
shape = ifcopenshell.geom.create_shape(settings, section)
|
||||
m = ifcopenshell.util.shape.get_shape_matrix(shape)
|
||||
if not cls.is_matrix_perpendicular(camera.matrix_world, Matrix(m)):
|
||||
return
|
||||
if not cls.does_shape_intersect_camera(shape, camera):
|
||||
return
|
||||
|
||||
bounds = helper.ortho_view_frame(camera_data) if camera_data.type == "ORTHO" else None
|
||||
reference_obj = tool.Ifc.get_object(reference_element)
|
||||
assert isinstance(reference_obj, bpy.types.Object)
|
||||
# Get cutting plane as a line
|
||||
verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
|
||||
cutting_plane_verts = sorted(verts, key=lambda v: v[2])[-4:]
|
||||
v1, *_, v2 = sorted(cutting_plane_verts, key=lambda v: v[0]) # Cut is in +X direction
|
||||
im = camera.matrix_world.inverted()
|
||||
v1, v2 = [im @ Vector((m @ np.append(v, 1.0))[:3]) for v in [v1, v2]]
|
||||
|
||||
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
|
||||
mat = reference_obj.matrix_world.copy()
|
||||
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
|
||||
xyz[2] = 0
|
||||
xyz = camera.matrix_world @ xyz
|
||||
mat.translation = xyz
|
||||
annotation_offset = mathutils.Vector((0, 0, -camera_data.clip_start - 0.05))
|
||||
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
|
||||
mat.translation += annotation_offset
|
||||
return mat
|
||||
|
||||
def project_point_onto_camera(point: Vector, camera: bpy.types.Object) -> Vector:
|
||||
projection = camera.matrix_world.to_quaternion() @ mathutils.Vector((0, 0, -1))
|
||||
return camera.matrix_world.inverted() @ mathutils.geometry.intersect_line_plane(
|
||||
point.xyz, point.xyz - projection, camera.location, projection
|
||||
)
|
||||
|
||||
obj_matrix = to_camera_coords(camera, reference_obj)
|
||||
|
||||
if props.raster_x > props.raster_y:
|
||||
width = camera_data.ortho_scale
|
||||
height = width / props.raster_x * props.raster_y
|
||||
else:
|
||||
height = camera_data.ortho_scale
|
||||
width = height / props.raster_y * props.raster_x
|
||||
|
||||
projection = project_point_onto_camera(reference_obj.location, camera)
|
||||
co1 = camera.matrix_world @ mathutils.Vector((width / 2, projection[1], -1))
|
||||
co2 = camera.matrix_world @ mathutils.Vector((-(width / 2), projection[1], -1))
|
||||
co1 = obj_matrix.inverted() @ co1
|
||||
co2 = obj_matrix.inverted() @ co2
|
||||
|
||||
data = bpy.data.curves.new("Annotation", type="CURVE")
|
||||
data.dimensions = "3D"
|
||||
data.resolution_u = 2
|
||||
|
||||
polyline = data.splines.new("POLY")
|
||||
polyline.points.add(1)
|
||||
polyline.points[-2].co = list(co1) + [1]
|
||||
polyline.points[-1].co = list(co2) + [1]
|
||||
|
||||
obj = bpy.data.objects.new(reference_obj.name, data)
|
||||
obj.matrix_world = obj_matrix
|
||||
|
||||
element = cls.run_root_assign_class(
|
||||
obj=obj,
|
||||
ifc_class="IfcAnnotation",
|
||||
predefined_type="SECTION_LEVEL",
|
||||
should_add_representation=True,
|
||||
context=context,
|
||||
ifc_representation_class=None,
|
||||
)
|
||||
if representation := ifcopenshell.util.representation.get_representation(element, context):
|
||||
cls.reload_representation(obj=obj, representation=representation)
|
||||
bpy.data.curves.remove(data)
|
||||
|
||||
return element
|
||||
bounds = helper.ortho_view_frame(camera.data)
|
||||
if not (points := helper.clip_segment(bounds, [v1, v2])):
|
||||
return
|
||||
for v in points:
|
||||
v.z = 0
|
||||
return points
|
||||
|
||||
@classmethod
|
||||
def generate_section_reference_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
reference_element: ifcopenshell.entity_instance,
|
||||
section: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
import bonsai.bim.module.drawing.helper as helper
|
||||
if not (points := cls.generate_section_reference_points(drawing, section)):
|
||||
return
|
||||
|
||||
reference_obj = tool.Ifc.get_object(reference_element)
|
||||
reference_obj.matrix_world
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
bounds = helper.ortho_view_frame(camera.data) if camera.data.type == "ORTHO" else None
|
||||
mesh = bpy.data.meshes.new("Mesh")
|
||||
obj = bpy.data.objects.new(section.Name or "Unnamed", mesh)
|
||||
obj.matrix_world = cls.get_default_annotation_matrix(camera)
|
||||
element = cls.run_root_assign_class(
|
||||
obj=obj, ifc_class="IfcAnnotation", predefined_type="SECTION", should_add_representation=False
|
||||
)
|
||||
element.Name = section.Name or "Unnamed"
|
||||
builder = ShapeBuilder(tool.Ifc.get())
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
points = [p / unit_scale for p in points]
|
||||
representation = builder.get_representation(context, [builder.polyline(points)])
|
||||
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
|
||||
bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
|
||||
return element
|
||||
|
||||
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
|
||||
mat = reference_obj.matrix_world.copy()
|
||||
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
|
||||
xyz[2] = 0
|
||||
xyz = camera.matrix_world @ xyz
|
||||
mat.translation = xyz
|
||||
annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
|
||||
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
|
||||
mat.translation += annotation_offset
|
||||
return mat
|
||||
@classmethod
|
||||
def regenerate_section_reference_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
annotation: ifcopenshell.entity_instance,
|
||||
section: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
if not (points := cls.generate_section_reference_points(drawing, section)):
|
||||
return
|
||||
|
||||
def clip_to_camera_boundary(
|
||||
mesh: bpy.types.Mesh, bounds: tuple[float, float, float, float, float, float]
|
||||
) -> Union[bpy.types.Mesh, None]:
|
||||
mesh.verts.ensure_lookup_table()
|
||||
points = [v.co for v in mesh.verts[0:2]]
|
||||
points = helper.clip_segment(bounds, points)
|
||||
if points is None:
|
||||
return None
|
||||
mesh.verts[0].co = points[0]
|
||||
mesh.verts[1].co = points[1]
|
||||
return mesh
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, annotation)
|
||||
m = ifcopenshell.util.shape.get_shape_matrix(shape)
|
||||
mw = cls.get_default_annotation_matrix(camera)
|
||||
existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
|
||||
|
||||
if cls.is_perpendicular(camera, reference_obj) and cls.is_intersecting(camera, reference_obj):
|
||||
reference_mesh = cls.get_camera_block(reference_obj)
|
||||
obj_matrix = to_camera_coords(camera, reference_obj)
|
||||
new_points = None
|
||||
if not np.allclose(m, np.array(mw), atol=1e-4):
|
||||
new_points = points
|
||||
elif len(existing_verts) != 2:
|
||||
new_points = points
|
||||
else:
|
||||
if not tool.Cad.are_edges_collinear(existing_verts, points):
|
||||
# Attempt to update the section line by projecting existing verts onto the new line
|
||||
v1 = tool.Cad.point_on_edge(existing_verts[0], points)
|
||||
v2 = tool.Cad.point_on_edge(existing_verts[1], points)
|
||||
existing_length = (existing_verts[0] - existing_verts[1]).length
|
||||
new_length = (v2 - v1).length
|
||||
if abs((existing_length - new_length) / existing_length) <= 0.10:
|
||||
# If the projected line is within 10% of the previous length ...
|
||||
new_points = (v1, v2)
|
||||
else:
|
||||
new_points = points
|
||||
|
||||
# The reference mesh vertices represent a view cube. To convert
|
||||
# this into a section line we:
|
||||
# 1. Select the 4 +Z vertices local to the reference element. This
|
||||
# is the cutting plane.
|
||||
verts_local_to_reference = [reference_obj.matrix_world.inverted() @ v for v in reference_mesh["verts"]]
|
||||
cutting_plane_verts = sorted(verts_local_to_reference, key=lambda x: x.z)[-4:]
|
||||
global_cutting_plane_verts = [reference_obj.matrix_world @ v for v in cutting_plane_verts]
|
||||
# 2. Project the cutting plane onto our viewing camera.
|
||||
verts_local_to_camera = [camera.matrix_world.inverted() @ v for v in global_cutting_plane_verts]
|
||||
# 3. Collapse verts with the same XY coords, and set Z to be just
|
||||
# below the clip_start so it's visible
|
||||
collapsed_verts = []
|
||||
for vert in verts_local_to_camera:
|
||||
if not [True for v in collapsed_verts if (vert.xy - v.xy).length < 1e-2]:
|
||||
collapsed_verts.append(mathutils.Vector((vert.x, vert.y, -camera.data.clip_start - 0.05)))
|
||||
# 4. The first two vertices is the section line
|
||||
section_line = collapsed_verts[0:2]
|
||||
# 5. Sort the vertices in the +X direction so that the vertices are
|
||||
# ordered to "point" in the direction of the section cut.
|
||||
section_line = sorted(
|
||||
section_line, key=lambda co: (reference_obj.matrix_world.inverted() @ camera.matrix_world @ co).x
|
||||
)
|
||||
global_section_line = [camera.matrix_world @ v for v in section_line]
|
||||
local_section_line = [obj_matrix.inverted() @ v for v in global_section_line]
|
||||
|
||||
mesh = bpy.data.meshes.new(name="Annotation")
|
||||
mesh.from_pydata(local_section_line, [(0, 1)], [])
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bm = clip_to_camera_boundary(bm, bounds)
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
|
||||
obj = bpy.data.objects.new(reference_obj.name, mesh)
|
||||
obj.matrix_world = obj_matrix
|
||||
|
||||
element = cls.run_root_assign_class(
|
||||
obj=obj,
|
||||
ifc_class="IfcAnnotation",
|
||||
predefined_type="SECTION",
|
||||
should_add_representation=True,
|
||||
context=context,
|
||||
ifc_representation_class=None,
|
||||
)
|
||||
if representation := ifcopenshell.util.representation.get_representation(element, context):
|
||||
cls.reload_representation(obj=obj, representation=representation)
|
||||
return element
|
||||
if new_points:
|
||||
if representation := ifcopenshell.util.representation.get_representation(annotation, context):
|
||||
ifcopenshell.api.geometry.unassign_representation(
|
||||
tool.Ifc.get(), product=annotation, representation=representation
|
||||
)
|
||||
builder = ShapeBuilder(tool.Ifc.get())
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
new_points = [p / unit_scale for p in new_points]
|
||||
representation = builder.get_representation(context, [builder.polyline(new_points)])
|
||||
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation)
|
||||
if obj := tool.Ifc.get_object(annotation):
|
||||
obj.matrix_world = mw
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc, tool.Geometry, obj=obj, representation=representation
|
||||
)
|
||||
else:
|
||||
ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw))
|
||||
annotation.Name = section.Name or "Unnamed"
|
||||
return annotation
|
||||
|
||||
@classmethod
|
||||
def generate_elevation_reference_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
reference_element: ifcopenshell.entity_instance,
|
||||
elevation: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
reference_obj = tool.Ifc.get_object(reference_element)
|
||||
reference_obj.matrix_world
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
|
||||
def to_camera_coords(camera: bpy.types.Object, reference_obj: bpy.types.Object) -> Matrix:
|
||||
mat = reference_obj.matrix_world.copy()
|
||||
xyz = camera.matrix_world.inverted() @ reference_obj.matrix_world.translation
|
||||
xyz[2] = 0
|
||||
xyz = camera.matrix_world @ xyz
|
||||
mat.translation = xyz
|
||||
annotation_offset = mathutils.Vector((0, 0, -camera.data.clip_start - 0.05))
|
||||
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
|
||||
mat.translation += annotation_offset
|
||||
return mat
|
||||
|
||||
if cls.is_perpendicular(camera, reference_obj) and cls.is_intersecting(camera, reference_obj):
|
||||
obj = bpy.data.objects.new(reference_obj.name, None)
|
||||
if camera.data.type != "ORTHO":
|
||||
return
|
||||
settings = ifcopenshell.geom.settings()
|
||||
shape = ifcopenshell.geom.create_shape(settings, elevation)
|
||||
m = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
|
||||
if cls.is_matrix_perpendicular(camera.matrix_world, m) and cls.does_shape_intersect_camera(shape, camera):
|
||||
obj = bpy.data.objects.new(elevation.Name or "Unnamed", None)
|
||||
obj.empty_display_size = 0.1
|
||||
obj.matrix_world = to_camera_coords(camera, reference_obj)
|
||||
|
||||
obj.matrix_world = cls.get_default_annotation_matrix(camera, matrix_world=m)
|
||||
element = cls.run_root_assign_class(
|
||||
obj=obj,
|
||||
ifc_class="IfcAnnotation",
|
||||
predefined_type="ELEVATION",
|
||||
should_add_representation=False,
|
||||
context=context,
|
||||
ifc_representation_class=None,
|
||||
obj=obj, ifc_class="IfcAnnotation", predefined_type="ELEVATION", should_add_representation=False
|
||||
)
|
||||
if representation := ifcopenshell.util.representation.get_representation(element, context):
|
||||
cls.reload_representation(obj=obj, representation=representation)
|
||||
element.Name = elevation.Name or "Unnamed"
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def regenerate_elevation_reference_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
annotation: ifcopenshell.entity_instance,
|
||||
elevation: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> ifcopenshell.entity_instance:
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
if camera.data.type != "ORTHO":
|
||||
return
|
||||
settings = ifcopenshell.geom.settings()
|
||||
shape = ifcopenshell.geom.create_shape(settings, elevation)
|
||||
m = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
|
||||
if cls.is_matrix_perpendicular(camera.matrix_world, m) and cls.does_shape_intersect_camera(shape, camera):
|
||||
existing_matrix = Matrix(ifcopenshell.util.placement.get_local_placement(annotation.ObjectPlacement))
|
||||
# The user is allowed to shift the elevation, but not rotate it
|
||||
if not np.allclose(np.array(m.to_3x3()), np.array(existing_matrix.to_3x3()), atol=1e-4):
|
||||
mw = cls.get_default_annotation_matrix(camera, matrix_world=m)
|
||||
if obj := tool.Ifc.get_object(annotation):
|
||||
obj.matrix_world = mw
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
else:
|
||||
ifcopenshell.api.geometry.edit_object_placement(
|
||||
tool.Ifc.get(), product=annotation, matrix=np.array(mw)
|
||||
)
|
||||
annotation.Name = elevation.Name or "Unnamed"
|
||||
return annotation
|
||||
|
||||
@classmethod
|
||||
def generate_grid_axis_reference_points(
|
||||
cls, drawing: ifcopenshell.entity_instance, axis: ifcopenshell.entity_instance
|
||||
) -> list | None:
|
||||
import bonsai.bim.module.drawing.helper as helper
|
||||
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
if camera.data.type != "ORTHO":
|
||||
return
|
||||
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
geometry = ifcopenshell.geom.create_shape(settings, axis.AxisCurve)
|
||||
verts = ifcopenshell.util.shape.get_vertices(geometry)
|
||||
grid = (axis.PartOfU or axis.PartOfV or axis.PartOfW)[0]
|
||||
m = ifcopenshell.util.placement.get_local_placement(grid.ObjectPlacement)
|
||||
im = camera.matrix_world.inverted()
|
||||
v1, v2 = [im @ Vector((m @ np.append(v, 1.0))[:3]) for v in verts[:2]]
|
||||
|
||||
target_view = tool.Drawing.get_drawing_target_view(drawing)
|
||||
if target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW"):
|
||||
bounds = helper.ortho_view_frame(camera.data)
|
||||
if not (points := helper.clip_segment(bounds, [v1, v2])):
|
||||
return
|
||||
elif target_view in ("ELEVATION_VIEW", "SECTION_VIEW"):
|
||||
bounds = helper.ortho_view_frame(camera.data)
|
||||
if not (points := helper.elevate_segment(bounds, [v1, v2])):
|
||||
return
|
||||
else:
|
||||
return
|
||||
for v in points:
|
||||
v.z = 0
|
||||
return points
|
||||
|
||||
@classmethod
|
||||
def generate_grid_axis_reference_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
reference_element: ifcopenshell.entity_instance,
|
||||
axis: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
import bonsai.bim.module.drawing.helper as helper
|
||||
|
||||
target_view = tool.Drawing.get_drawing_target_view(drawing)
|
||||
if not (points := cls.generate_grid_axis_reference_points(drawing, axis)):
|
||||
return
|
||||
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
assert isinstance(camera, bpy.types.Object)
|
||||
assert isinstance(camera_data := camera.data, bpy.types.Camera)
|
||||
|
||||
is_ortho = camera_data.type == "ORTHO"
|
||||
bounds = helper.ortho_view_frame(camera_data) if is_ortho else None
|
||||
clipping = is_ortho and target_view in ("PLAN_VIEW", "REFLECTED_PLAN_VIEW")
|
||||
elevating = is_ortho and target_view in ("ELEVATION_VIEW", "SECTION_VIEW")
|
||||
|
||||
def clone(src: bpy.types.Object) -> bpy.types.Object:
|
||||
dst = src.copy()
|
||||
assert isinstance(dst.data, bpy.types.Mesh)
|
||||
dst.data = dst.data.copy()
|
||||
dst.name = dst.name.replace("IfcGridAxis/", "")
|
||||
tool.Blender.get_object_bim_props(dst).ifc_definition_id = 0
|
||||
tool.Geometry.get_mesh_props(dst.data).ifc_definition_id = 0
|
||||
return dst
|
||||
|
||||
def disassemble(obj: bpy.types.Object) -> tuple[bpy.types.Object, bmesh.types.BMesh]:
|
||||
assert isinstance(obj.data, bpy.types.Mesh)
|
||||
mesh = bmesh.new()
|
||||
mesh.verts.ensure_lookup_table()
|
||||
mesh.from_mesh(obj.data)
|
||||
return obj, mesh
|
||||
|
||||
def assemble(obj: bpy.types.Object, mesh: bmesh.types.BMesh) -> bpy.types.Object:
|
||||
assert isinstance(obj.data, bpy.types.Mesh)
|
||||
mesh.to_mesh(obj.data)
|
||||
return obj
|
||||
|
||||
def to_camera_coords(
|
||||
obj: bpy.types.Object, mesh: bmesh.types.BMesh
|
||||
) -> tuple[bpy.types.Object, bmesh.types.BMesh]:
|
||||
mesh.transform(camera.matrix_world.inverted() @ obj.matrix_world)
|
||||
obj.matrix_world = camera.matrix_world
|
||||
annotation_offset = mathutils.Vector((0, 0, -camera_data.clip_start - 0.05))
|
||||
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
|
||||
obj.matrix_world.translation += annotation_offset
|
||||
return obj, mesh
|
||||
|
||||
def clip_to_camera_boundary(mesh: bmesh.types.BMesh) -> bmesh.types.BMesh:
|
||||
mesh.verts.ensure_lookup_table()
|
||||
points = [v.co for v in mesh.verts[0:2]]
|
||||
points = helper.clip_segment(bounds, points)
|
||||
if points is None:
|
||||
return None
|
||||
mesh.verts[0].co = points[0]
|
||||
mesh.verts[1].co = points[1]
|
||||
return mesh
|
||||
|
||||
def draw_grids_vertically(mesh: bmesh.types.BMesh) -> bmesh.types.BMesh:
|
||||
mesh.verts.ensure_lookup_table()
|
||||
points = [v.co for v in mesh.verts[0:2]]
|
||||
points = helper.elevate_segment(bounds, points)
|
||||
if points is None:
|
||||
return None
|
||||
points = helper.clip_segment(bounds, points)
|
||||
if points is None:
|
||||
return None
|
||||
mesh.verts[0].co = points[0]
|
||||
mesh.verts[1].co = points[1]
|
||||
return mesh
|
||||
|
||||
obj = tool.Ifc.get_object(reference_element)
|
||||
if not obj:
|
||||
return
|
||||
assert isinstance(obj, bpy.types.Object)
|
||||
obj, mesh = to_camera_coords(*disassemble(clone(obj)))
|
||||
|
||||
if clipping:
|
||||
mesh = clip_to_camera_boundary(mesh)
|
||||
elif elevating:
|
||||
mesh = draw_grids_vertically(mesh)
|
||||
|
||||
if mesh is None:
|
||||
return
|
||||
|
||||
assemble(obj, mesh)
|
||||
|
||||
mesh = bpy.data.meshes.new("Mesh")
|
||||
obj = bpy.data.objects.new(axis.AxisTag or "-", mesh)
|
||||
obj.matrix_world = cls.get_default_annotation_matrix(camera)
|
||||
element = cls.run_root_assign_class(
|
||||
obj=obj,
|
||||
ifc_class="IfcAnnotation",
|
||||
predefined_type="GRID",
|
||||
should_add_representation=True,
|
||||
context=context,
|
||||
ifc_representation_class=None,
|
||||
obj=obj, ifc_class="IfcAnnotation", predefined_type="GRID", should_add_representation=False
|
||||
)
|
||||
if representation := ifcopenshell.util.representation.get_representation(element, context):
|
||||
cls.reload_representation(obj=obj, representation=representation)
|
||||
element.Name = axis.AxisTag or "-"
|
||||
builder = ShapeBuilder(tool.Ifc.get())
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
points = [p / unit_scale for p in points]
|
||||
representation = builder.get_representation(context, [builder.polyline(points)])
|
||||
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
|
||||
bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def regenerate_grid_axis_reference_annotation(
|
||||
cls,
|
||||
drawing: ifcopenshell.entity_instance,
|
||||
annotation: ifcopenshell.entity_instance,
|
||||
axis: ifcopenshell.entity_instance,
|
||||
context: ifcopenshell.entity_instance,
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
if not (points := cls.generate_grid_axis_reference_points(drawing, axis)):
|
||||
return
|
||||
|
||||
camera = tool.Ifc.get_object(drawing)
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, annotation)
|
||||
m = ifcopenshell.util.shape.get_shape_matrix(shape)
|
||||
mw = cls.get_default_annotation_matrix(camera)
|
||||
existing_verts = [Vector(v) for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
|
||||
|
||||
new_points = None
|
||||
if not np.allclose(m, np.array(mw), atol=1e-4):
|
||||
new_points = points
|
||||
elif len(existing_verts) != 2:
|
||||
new_points = points
|
||||
else:
|
||||
if not tool.Cad.are_edges_collinear(existing_verts, points):
|
||||
# Attempt to update the section line by projecting existing verts onto the new line
|
||||
v1 = tool.Cad.point_on_edge(existing_verts[0], points)
|
||||
v2 = tool.Cad.point_on_edge(existing_verts[1], points)
|
||||
existing_length = (existing_verts[0] - existing_verts[1]).length
|
||||
new_length = (v2 - v1).length
|
||||
if abs((existing_length - new_length) / existing_length) <= 0.10:
|
||||
# If the projected line is within 10% of the previous length ...
|
||||
new_points = (v1, v2)
|
||||
else:
|
||||
new_points = points
|
||||
|
||||
if new_points:
|
||||
if representation := ifcopenshell.util.representation.get_representation(annotation, context):
|
||||
ifcopenshell.api.geometry.unassign_representation(
|
||||
tool.Ifc.get(), product=annotation, representation=representation
|
||||
)
|
||||
builder = ShapeBuilder(tool.Ifc.get())
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
new_points = [p / unit_scale for p in new_points]
|
||||
representation = builder.get_representation(context, [builder.polyline(new_points)])
|
||||
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), annotation, representation)
|
||||
if obj := tool.Ifc.get_object(annotation):
|
||||
obj.matrix_world = mw
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc, tool.Geometry, obj=obj, representation=representation
|
||||
)
|
||||
else:
|
||||
ifcopenshell.api.geometry.edit_object_placement(tool.Ifc.get(), product=annotation, matrix=np.array(mw))
|
||||
annotation.Name = axis.AxisTag or "-"
|
||||
return annotation
|
||||
|
||||
@classmethod
|
||||
def get_default_annotation_matrix(cls, camera, matrix_world=None):
|
||||
if matrix_world is None:
|
||||
matrix_world = camera.matrix_world.copy()
|
||||
annotation_offset = Vector((0, 0, -camera.data.clip_start - 0.05))
|
||||
annotation_offset = camera.matrix_world.to_quaternion() @ annotation_offset
|
||||
matrix_world.translation += annotation_offset
|
||||
return matrix_world
|
||||
|
||||
@classmethod
|
||||
def is_perpendicular(cls, a: bpy.types.Object, b: bpy.types.Object) -> bool:
|
||||
axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))]
|
||||
@@ -1798,6 +1985,16 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def is_matrix_perpendicular(cls, a: Matrix, b: Matrix) -> bool:
|
||||
axes = [mathutils.Vector((1, 0, 0)), mathutils.Vector((0, 1, 0)), mathutils.Vector((0, 0, 1))]
|
||||
a_quaternion = a.to_quaternion()
|
||||
b_quaternion = b.to_quaternion()
|
||||
for axis in axes:
|
||||
if abs((a_quaternion @ axis).angle(b_quaternion @ axis) - (math.pi / 2)) < 1e-5:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_camera_block(cls, obj: bpy.types.Object) -> dict:
|
||||
assert isinstance(camera := obj.data, bpy.types.Camera)
|
||||
@@ -1841,6 +2038,16 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
b_tree = mathutils.bvhtree.BVHTree.FromPolygons(b_block["verts"], b_block["faces"])
|
||||
return bool(a_tree.overlap(b_tree))
|
||||
|
||||
@classmethod
|
||||
def does_shape_intersect_camera(cls, shape, camera) -> bool:
|
||||
a_block = cls.get_camera_block(camera)
|
||||
a_tree = mathutils.bvhtree.BVHTree.FromPolygons(a_block["verts"], a_block["faces"])
|
||||
m = ifcopenshell.util.shape.get_shape_matrix(shape)
|
||||
verts = [(m @ np.append(v, 1.0))[:3] for v in ifcopenshell.util.shape.get_vertices(shape.geometry)]
|
||||
faces = ifcopenshell.util.shape.get_faces(shape.geometry)
|
||||
b_tree = mathutils.bvhtree.BVHTree.FromPolygons(verts, faces)
|
||||
return bool(a_tree.overlap(b_tree))
|
||||
|
||||
@classmethod
|
||||
def replace_text_literal_variables(
|
||||
cls,
|
||||
@@ -2568,3 +2775,14 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
ifcopenshell.api.document.remove_reference(tool.Ifc.get(), reference=reference)
|
||||
|
||||
tool.Drawing.import_sheets()
|
||||
|
||||
@classmethod
|
||||
def hide_all_drawing_collections(cls) -> None:
|
||||
for element in tool.Ifc.get().by_type("IfcAnnotation"):
|
||||
if element.ObjectType == "DRAWING" and (obj := tool.Ifc.get_object(element)):
|
||||
tool.Blender.get_layer_collection(obj.users_collection[0]).hide_viewport = True
|
||||
|
||||
@classmethod
|
||||
def clear_annotation_relationships(cls, drawing: ifcopenshell.entity_instance) -> None:
|
||||
for rel in drawing.ReferencedBy:
|
||||
tool.Ifc.get().remove(rel)
|
||||
|
||||
@@ -233,20 +233,20 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
return
|
||||
elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
||||
return bonsai.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
|
||||
elif element.is_a("IfcAnnotation"):
|
||||
if element.ObjectType == "DRAWING":
|
||||
return bonsai.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
|
||||
elif tool.Drawing.is_auto_annotation(element):
|
||||
return # For now, these are special referenced objects and cannot be deleted. Exclude instead.
|
||||
elif element.is_a("IfcRelSpaceBoundary"):
|
||||
ifcopenshell.api.boundary.remove_boundary(ifc_file, boundary=element)
|
||||
tool.Boundary.undecorate_boundary(obj)
|
||||
return bpy.data.objects.remove(obj)
|
||||
elif element.is_a("IfcGridAxis"):
|
||||
is_last_axis = False
|
||||
# Deleting the last W axis is OK
|
||||
if ((grid := element.PartOfU) and len(grid[0].UAxes) == 1) or (
|
||||
(grid := element.PartOfV) and len(grid[0].VAxes) == 1
|
||||
):
|
||||
is_last_axis = True
|
||||
if is_last_axis:
|
||||
return
|
||||
ifcopenshell.api.grid.remove_grid_axis(ifc_file, axis=element)
|
||||
return bpy.data.objects.remove(obj)
|
||||
|
||||
@@ -786,17 +786,21 @@ class Model(bonsai.core.tool.Model):
|
||||
elif material.is_a("IfcMaterialLayerSet"):
|
||||
axis = ifcopenshell.util.element.get_pset(element, "EPset_Parametric", "LayerSetDirection")
|
||||
if axis is None:
|
||||
if element.is_a() in [
|
||||
if element.is_a() in (
|
||||
"IfcSlabType",
|
||||
"IfcRoofType",
|
||||
"IfcRampType",
|
||||
"IfcPlateType",
|
||||
"IfcCovering",
|
||||
"IfcFurniture",
|
||||
]:
|
||||
"IfcSlab",
|
||||
"IfcRoof",
|
||||
"IfcRamp",
|
||||
"IfcPlate",
|
||||
):
|
||||
axis = "AXIS3"
|
||||
else:
|
||||
elif element.is_a() in ("IfcWallType", "IfcWall"):
|
||||
axis = "AXIS2"
|
||||
else:
|
||||
return
|
||||
return f"LAYER{axis[-1]}"
|
||||
elif material.is_a("IfcMaterialProfileSetUsage"):
|
||||
# TODO: remove after we support editing profile usages with IfcRevolvedAreaSolid.
|
||||
@@ -1399,7 +1403,6 @@ class Model(bonsai.core.tool.Model):
|
||||
|
||||
number_of_risers = number_of_treads + 1
|
||||
tread_rise = height / number_of_risers
|
||||
custom_tread_run = any(run != 0 for run in custom_first_last_tread_run)
|
||||
nosing_overlap = max(nosing_length, 0)
|
||||
nosing_tread_gap = -min(nosing_length, 0)
|
||||
nosing_overlap_offset = -V_(nosing_overlap, 0)
|
||||
@@ -1430,19 +1433,26 @@ class Model(bonsai.core.tool.Model):
|
||||
default_tread_offset = Vector([tread_run, tread_rise])
|
||||
|
||||
def get_tread_data(i):
|
||||
if custom_tread_run:
|
||||
current_tread_run = None
|
||||
if i == 0:
|
||||
current_tread_run = custom_first_last_tread_run[0]
|
||||
elif i == number_of_risers - 1:
|
||||
current_tread_run = custom_first_last_tread_run[1]
|
||||
# Check if this is first or last tread with custom run
|
||||
current_tread_run = None
|
||||
if i == 0 and custom_first_last_tread_run[0] is not None:
|
||||
current_tread_run = custom_first_last_tread_run[0]
|
||||
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] is not None:
|
||||
current_tread_run = custom_first_last_tread_run[1]
|
||||
|
||||
if current_tread_run is not None:
|
||||
tread_offset = default_tread_offset.copy()
|
||||
tread_offset.x = current_tread_run
|
||||
|
||||
# Handle zero-width treads
|
||||
if current_tread_run == 0:
|
||||
# For zero width, just return vertical offset with no horizontal tread
|
||||
return tread_offset, ()
|
||||
|
||||
tread_verts = deepcopy(default_tread_verts)
|
||||
tread_verts[-1].x = current_tread_run
|
||||
return tread_offset, tread_verts
|
||||
|
||||
if current_tread_run:
|
||||
tread_offset = default_tread_offset.copy()
|
||||
tread_offset.x = current_tread_run
|
||||
tread_verts = deepcopy(default_tread_verts)
|
||||
tread_verts[-1].x = current_tread_run
|
||||
return tread_offset, tread_verts
|
||||
return default_tread_offset, default_tread_verts
|
||||
|
||||
# treads
|
||||
@@ -1450,9 +1460,13 @@ class Model(bonsai.core.tool.Model):
|
||||
for i in range(number_of_risers):
|
||||
last_vert_i = len(vertices) - 1
|
||||
tread_offset, tread_verts = get_tread_data(i)
|
||||
current_tread_verts = [v + current_offset for v in tread_verts]
|
||||
edges.extend(default_tread_edges + last_vert_i)
|
||||
vertices.extend(current_tread_verts)
|
||||
|
||||
# Skip adding vertices/edges for zero-width treads
|
||||
if tread_verts:
|
||||
current_tread_verts = [v + current_offset for v in tread_verts]
|
||||
edges.extend(default_tread_edges + last_vert_i)
|
||||
vertices.extend(current_tread_verts)
|
||||
|
||||
current_offset += tread_offset
|
||||
|
||||
if stair_type == "WOOD/STEEL":
|
||||
@@ -1467,35 +1481,47 @@ class Model(bonsai.core.tool.Model):
|
||||
default_tread_offset = V_(tread_run + nosing_tread_gap, tread_rise)
|
||||
|
||||
def get_tread_data(i):
|
||||
if custom_tread_run:
|
||||
current_tread_run = None
|
||||
if i == 0 and custom_first_last_tread_run[0] != 0:
|
||||
current_tread_run = custom_first_last_tread_run[0]
|
||||
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] != 0:
|
||||
current_tread_run = custom_first_last_tread_run[1]
|
||||
# Check if this is first or last tread with custom run
|
||||
current_tread_run = None
|
||||
if i == 0 and custom_first_last_tread_run[0] is not None:
|
||||
current_tread_run = custom_first_last_tread_run[0]
|
||||
elif i == number_of_risers - 1 and custom_first_last_tread_run[1] is not None:
|
||||
current_tread_run = custom_first_last_tread_run[1]
|
||||
|
||||
if current_tread_run is not None:
|
||||
tread_offset = default_tread_offset.copy()
|
||||
tread_offset.x = current_tread_run + nosing_tread_gap
|
||||
|
||||
# Handle zero-width treads
|
||||
if current_tread_run == 0:
|
||||
return tread_offset, ()
|
||||
|
||||
tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth))
|
||||
return tread_offset, tread_verts
|
||||
|
||||
if current_tread_run:
|
||||
tread_offset = default_tread_offset.copy()
|
||||
tread_offset.x = current_tread_run + nosing_tread_gap
|
||||
tread_verts = get_tread_verts(size=V_(current_tread_run + nosing_overlap, tread_depth))
|
||||
return tread_offset, tread_verts
|
||||
return default_tread_offset, default_tread_verts
|
||||
|
||||
# each tread is a separate shape
|
||||
cur_offset = V_(0, 0)
|
||||
tread_index = 0
|
||||
for i in range(number_of_risers):
|
||||
tread_offset, tread_verts = get_tread_data(i)
|
||||
cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts]
|
||||
vertices.extend(cur_trade_shape)
|
||||
|
||||
cur_vertex = i * 4
|
||||
verts_to_add = (
|
||||
(cur_vertex, cur_vertex + 1),
|
||||
(cur_vertex + 1, cur_vertex + 2),
|
||||
(cur_vertex + 2, cur_vertex + 3),
|
||||
(cur_vertex + 3, cur_vertex),
|
||||
)
|
||||
edges.extend(verts_to_add)
|
||||
# Skip adding vertices/edges for zero-width treads
|
||||
if tread_verts:
|
||||
cur_trade_shape = [v + cur_offset + nosing_overlap_offset for v in tread_verts]
|
||||
vertices.extend(cur_trade_shape)
|
||||
|
||||
cur_vertex = tread_index * 4
|
||||
verts_to_add = (
|
||||
(cur_vertex, cur_vertex + 1),
|
||||
(cur_vertex + 1, cur_vertex + 2),
|
||||
(cur_vertex + 2, cur_vertex + 3),
|
||||
(cur_vertex + 3, cur_vertex),
|
||||
)
|
||||
edges.extend(verts_to_add)
|
||||
tread_index += 1
|
||||
|
||||
cur_offset += tread_offset
|
||||
|
||||
elif stair_type == "GENERIC":
|
||||
|
||||
@@ -176,6 +176,27 @@ class TestStairCalculatedParams(NewFile):
|
||||
calculated_data["Length"] += -0.2 + 0.1
|
||||
self.compare_data(pset_data, calculated_data)
|
||||
|
||||
# zero-width first tread
|
||||
pset_data = pset_data_base.copy()
|
||||
calculated_data = calculated_data_base.copy()
|
||||
pset_data["custom_first_last_tread_run"] = (0.0, 0.0)
|
||||
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
|
||||
self.compare_data(pset_data, calculated_data)
|
||||
|
||||
# zero-width last tread
|
||||
pset_data = pset_data_base.copy()
|
||||
calculated_data = calculated_data_base.copy()
|
||||
pset_data["custom_first_last_tread_run"] = (0.3, 0.0)
|
||||
calculated_data["Length"] = 0.9 # Only 3 treads at 0.3 each
|
||||
self.compare_data(pset_data, calculated_data)
|
||||
|
||||
# both first and last treads zero-width
|
||||
pset_data = pset_data_base.copy()
|
||||
calculated_data = calculated_data_base.copy()
|
||||
pset_data["custom_first_last_tread_run"] = (0.0, 0.0)
|
||||
calculated_data["Length"] = 0.6 # Only 2 middle treads at 0.3 each
|
||||
self.compare_data(pset_data, calculated_data)
|
||||
|
||||
# overlap affects stair length only by first tread
|
||||
pset_data = pset_data_base.copy()
|
||||
calculated_data = calculated_data_base.copy()
|
||||
@@ -296,6 +317,94 @@ class TestGenerateStair2DProfile(NewFile):
|
||||
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||
self.compare_data(generated_profile, expected_profile)
|
||||
|
||||
def test_create_concrete_stair_zero_width_first_tread(self):
|
||||
"""Test concrete stair with zero-width first tread"""
|
||||
kwargs = {
|
||||
"base_slab_depth": 0.25,
|
||||
"has_top_nib": False,
|
||||
"height": 1.0,
|
||||
"number_of_treads": 3,
|
||||
"stair_type": "CONCRETE",
|
||||
"top_slab_depth": 0.25,
|
||||
"tread_depth": 0.25,
|
||||
"tread_run": 0.3,
|
||||
"width": 1.2,
|
||||
"custom_first_last_tread_run": (0.0, 0.0),
|
||||
}
|
||||
verts_data = (
|
||||
V(0.0, 0, 0.0),
|
||||
# First tread skipped - goes straight to second tread
|
||||
V(0.0, 0, 0.5),
|
||||
V(0.3, 0, 0.5),
|
||||
V(0.3, 0, 0.75),
|
||||
V(0.6, 0, 0.75),
|
||||
V(0.6, 0, 1.0),
|
||||
V(0.9, 0, 1.0),
|
||||
V(0.9, 0, 0.67457),
|
||||
V(0.0, 0, -0.25),
|
||||
)
|
||||
edges_data = (
|
||||
(0, 1),
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
(3, 4),
|
||||
(4, 5),
|
||||
(5, 6),
|
||||
(6, 7),
|
||||
(8, 0),
|
||||
(7, 8),
|
||||
)
|
||||
edges_data = [e[::-1] for e in edges_data]
|
||||
faces_data = ()
|
||||
expected_profile = (verts_data, edges_data, faces_data)
|
||||
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||
self.compare_data(generated_profile, expected_profile)
|
||||
|
||||
def test_create_concrete_stair_zero_width_last_tread(self):
|
||||
"""Test concrete stair with zero-width last tread"""
|
||||
kwargs = {
|
||||
"base_slab_depth": 0.25,
|
||||
"has_top_nib": False,
|
||||
"height": 1.0,
|
||||
"number_of_treads": 3,
|
||||
"stair_type": "CONCRETE",
|
||||
"top_slab_depth": 0.25,
|
||||
"tread_depth": 0.25,
|
||||
"tread_run": 0.3,
|
||||
"width": 1.2,
|
||||
"custom_first_last_tread_run": (0.0, 0.0),
|
||||
}
|
||||
verts_data = (
|
||||
V(0.0, 0, 0.0),
|
||||
V(0.0, 0, 0.25),
|
||||
V(0.3, 0, 0.25),
|
||||
V(0.3, 0, 0.5),
|
||||
V(0.6, 0, 0.5),
|
||||
V(0.6, 0, 0.75),
|
||||
V(0.9, 0, 0.75),
|
||||
# Last tread skipped
|
||||
V(0.9, 0, 0.67457),
|
||||
V(0.1, 0, -0.25),
|
||||
V(0.0, 0, -0.25),
|
||||
)
|
||||
edges_data = (
|
||||
(0, 1),
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
(3, 4),
|
||||
(4, 5),
|
||||
(5, 6),
|
||||
(6, 7),
|
||||
(9, 0),
|
||||
(8, 9),
|
||||
(7, 8),
|
||||
)
|
||||
edges_data = [e[::-1] for e in edges_data]
|
||||
faces_data = ()
|
||||
expected_profile = (verts_data, edges_data, faces_data)
|
||||
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||
self.compare_data(generated_profile, expected_profile)
|
||||
|
||||
def test_create_wood_steel_stair(self):
|
||||
kwargs = {
|
||||
"height": 1.0,
|
||||
@@ -348,6 +457,100 @@ class TestGenerateStair2DProfile(NewFile):
|
||||
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||
self.compare_data(generated_profile, expected_profile)
|
||||
|
||||
def test_create_wood_steel_stair_zero_width_first_tread(self):
|
||||
"""Test wood/steel stair with zero-width first tread"""
|
||||
kwargs = {
|
||||
"height": 1.0,
|
||||
"number_of_treads": 3,
|
||||
"stair_type": "WOOD/STEEL",
|
||||
"tread_depth": 0.25,
|
||||
"tread_run": 0.3,
|
||||
"width": 1.2,
|
||||
"custom_first_last_tread_run": (0.0, 0.0),
|
||||
}
|
||||
verts_data = (
|
||||
# First tread skipped - start at second tread
|
||||
V(0.0, 0, 0.25),
|
||||
V(0.3, 0, 0.25),
|
||||
V(0.3, 0, 0.5),
|
||||
V(0.0, 0, 0.5),
|
||||
V(0.3, 0, 0.5),
|
||||
V(0.6, 0, 0.5),
|
||||
V(0.6, 0, 0.75),
|
||||
V(0.3, 0, 0.75),
|
||||
V(0.6, 0, 0.75),
|
||||
V(0.9, 0, 0.75),
|
||||
V(0.9, 0, 1.0),
|
||||
V(0.6, 0, 1.0),
|
||||
)
|
||||
edges_data = (
|
||||
(0, 1),
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
(3, 0),
|
||||
(4, 5),
|
||||
(5, 6),
|
||||
(6, 7),
|
||||
(7, 4),
|
||||
(8, 9),
|
||||
(9, 10),
|
||||
(10, 11),
|
||||
(11, 8),
|
||||
)
|
||||
|
||||
faces_data = ()
|
||||
|
||||
expected_profile = (verts_data, edges_data, faces_data)
|
||||
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||
self.compare_data(generated_profile, expected_profile)
|
||||
|
||||
def test_create_wood_steel_stair_zero_width_last_tread(self):
|
||||
"""Test wood/steel stair with zero-width last tread"""
|
||||
kwargs = {
|
||||
"height": 1.0,
|
||||
"number_of_treads": 3,
|
||||
"stair_type": "WOOD/STEEL",
|
||||
"tread_depth": 0.25,
|
||||
"tread_run": 0.3,
|
||||
"width": 1.2,
|
||||
"custom_first_last_tread_run": (0.0, 0.0),
|
||||
}
|
||||
verts_data = (
|
||||
V(0.0, 0, 0.0),
|
||||
V(0.3, 0, 0.0),
|
||||
V(0.3, 0, 0.25),
|
||||
V(0.0, 0, 0.25),
|
||||
V(0.3, 0, 0.25),
|
||||
V(0.6, 0, 0.25),
|
||||
V(0.6, 0, 0.5),
|
||||
V(0.3, 0, 0.5),
|
||||
V(0.6, 0, 0.5),
|
||||
V(0.9, 0, 0.5),
|
||||
V(0.9, 0, 0.75),
|
||||
V(0.6, 0, 0.75),
|
||||
# Last tread skipped
|
||||
)
|
||||
edges_data = (
|
||||
(0, 1),
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
(3, 0),
|
||||
(4, 5),
|
||||
(5, 6),
|
||||
(6, 7),
|
||||
(7, 4),
|
||||
(8, 9),
|
||||
(9, 10),
|
||||
(10, 11),
|
||||
(11, 8),
|
||||
)
|
||||
|
||||
faces_data = ()
|
||||
|
||||
expected_profile = (verts_data, edges_data, faces_data)
|
||||
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||
self.compare_data(generated_profile, expected_profile)
|
||||
|
||||
def test_create_generic_stair(self):
|
||||
kwargs = {"height": 1.0, "number_of_treads": 3, "stair_type": "GENERIC", "tread_run": 0.3, "width": 1.2}
|
||||
verts_data = (
|
||||
@@ -381,6 +584,41 @@ class TestGenerateStair2DProfile(NewFile):
|
||||
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||
self.compare_data(generated_profile, expected_profile)
|
||||
|
||||
def test_create_generic_stair_zero_width_treads(self):
|
||||
"""Test generic stair with zero-width first and last treads"""
|
||||
kwargs = {
|
||||
"height": 1.0,
|
||||
"number_of_treads": 3,
|
||||
"stair_type": "GENERIC",
|
||||
"tread_run": 0.3,
|
||||
"width": 1.2,
|
||||
"custom_first_last_tread_run": (0.0, 0.0),
|
||||
}
|
||||
verts_data = (
|
||||
V(0.0, 0, 0.0),
|
||||
# First tread skipped
|
||||
V(0.0, 0, 0.5),
|
||||
V(0.3, 0, 0.5),
|
||||
V(0.3, 0, 0.75),
|
||||
V(0.6, 0, 0.75),
|
||||
# Last tread skipped
|
||||
V(0.6, 0, 0.0),
|
||||
)
|
||||
edges_data = (
|
||||
(0, 1),
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
(3, 4),
|
||||
(4, 5),
|
||||
(5, 0),
|
||||
)
|
||||
edges_data = [e[::-1] for e in edges_data]
|
||||
|
||||
faces_data = ()
|
||||
expected_profile = (verts_data, edges_data, faces_data)
|
||||
generated_profile = subject.generate_stair_2d_profile(**kwargs)
|
||||
self.compare_data(generated_profile, expected_profile)
|
||||
|
||||
|
||||
class TestUsingArrays(NewFile):
|
||||
def setup_array(self, add_second_layer=False, sync_children=False):
|
||||
|
||||
+2
-2
@@ -96,11 +96,11 @@ writer.write()
|
||||
### CLI app for converting IFC files to CSV, ODS or XLSX format.
|
||||
|
||||
Usage:
|
||||
python ifc5Dspreadsheet.py input_file output_file [-l log_file] [-f format_type]
|
||||
python ifc5Dspreadsheet.py input_file output_dir [-l log_file] [-f format_type]
|
||||
|
||||
Arguments:
|
||||
input_file (str): The path to the input IFC file to process.
|
||||
output_file (str): The output directory for CSV or filename for other formats.
|
||||
output_dir (str): The output directory.
|
||||
|
||||
Options:
|
||||
-l, --log log_file (str): The path to the file where errors should be logged. Default is process.log.
|
||||
|
||||
@@ -319,8 +319,8 @@ class Ifc5Dwriter:
|
||||
cost_schedule: Optional[ifcopenshell.entity_instance] = None,
|
||||
):
|
||||
"""
|
||||
:param file: IFC file to exprot cost schedules from.
|
||||
:param output: Output directory for csv files.
|
||||
:param file: IFC file to export cost schedules from.
|
||||
:param output: Output directory.
|
||||
:param cost_schedule: exported cost schedule. If not provided, will export all available schedules.
|
||||
"""
|
||||
self.output = output
|
||||
@@ -404,6 +404,7 @@ class Ifc5DCsvWriter(Ifc5Dwriter):
|
||||
import csv
|
||||
|
||||
super().write()
|
||||
os.makedirs(self.output, exist_ok=True)
|
||||
for sheet, data in self.sheet_data.items():
|
||||
with open(
|
||||
os.path.join(self.output, "{}.csv".format(data["Name"])), "w", newline="", encoding="utf-8"
|
||||
@@ -436,6 +437,7 @@ class Ifc5DOdsWriter(Ifc5Dwriter):
|
||||
ns1.addElement(Number(decimalplaces="2", minintegerdigits="1", grouping="true"))
|
||||
self.doc.styles.addElement(ns1)
|
||||
|
||||
os.makedirs(self.output, exist_ok=True)
|
||||
file_name = ""
|
||||
for cost_schedule in self.cost_schedules:
|
||||
if file_name:
|
||||
@@ -537,6 +539,7 @@ class Ifc5DXlsxWriter(Ifc5Dwriter):
|
||||
import xlsxwriter
|
||||
|
||||
super().write()
|
||||
os.makedirs(self.output, exist_ok=True)
|
||||
file_name = ""
|
||||
for cost_schedule in self.cost_schedules:
|
||||
if file_name:
|
||||
@@ -683,7 +686,7 @@ class Ifc5DPdfWriter(Ifc5Dwriter):
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("input", type=str, help="Specify an IFC file to process")
|
||||
parser.add_argument("output", help="The output directory for CSV or filename for other formats")
|
||||
parser.add_argument("output", help="The output directory")
|
||||
parser.add_argument("-l", "--log", type=str, help="Specify where errors should be logged", default="process.log")
|
||||
parser.add_argument(
|
||||
"-f", "--format", type=str, help="Choose which format to export in (CSV/ODS/XLSX)", default="CSV"
|
||||
|
||||
@@ -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>&);
|
||||
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
|
||||
struct verbosity_counter {
|
||||
@@ -965,7 +965,10 @@ int main(int argc, char** argv) {
|
||||
time_t start,end;
|
||||
time(&start);
|
||||
|
||||
if (!init_input_file(IfcUtil::path::to_utf8(input_filename), ifc_file, no_progress || quiet, mmap)) {
|
||||
// @nb last argument true -> bypass_properties which are not read by any of the geometry serializers
|
||||
// XML, RocksDB, IFC are already special-cased above
|
||||
// SVG requires properties for IfcAnnotation/DRAWING properties
|
||||
if (!init_input_file(IfcUtil::path::to_utf8(input_filename), ifc_file, no_progress || quiet, mmap, output_extension != SVG)) {
|
||||
write_log(!quiet);
|
||||
serializer.reset();
|
||||
IfcUtil::path::delete_file(IfcUtil::path::to_utf8(output_temp_filename)); /**< @todo Windows Unicode support */
|
||||
@@ -1336,7 +1339,7 @@ void write_log(bool header) {
|
||||
|
||||
#include <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;
|
||||
|
||||
// Prevent IfcFile::Init() prints by setting output to null temporarily
|
||||
@@ -1344,20 +1347,36 @@ bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file,
|
||||
|
||||
time(&start);
|
||||
|
||||
bool requires_init = false;
|
||||
|
||||
#ifdef WITH_IFCXML
|
||||
if (boost::ends_with(boost::to_lower_copy(filename), ".ifcxml")) {
|
||||
ifc_file = IfcParse::parse_ifcxml(filename);
|
||||
} else
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
ifc_file = new IfcParse::IfcFile(IfcParse::uninitialized_tag{});
|
||||
requires_init = true;
|
||||
}
|
||||
|
||||
{
|
||||
ifc_file->bypass_type("IfcRelDefinesByProperties");
|
||||
ifc_file->bypass_type("IfcPropertySetDefinition");
|
||||
ifc_file->bypass_type("IfcProperty");
|
||||
ifc_file->bypass_type("IfcMaterialProperties");
|
||||
ifc_file->bypass_type("IfcProfileProperties");
|
||||
ifc_file->bypass_type("IfcPhysicalQuantity");
|
||||
|
||||
#ifdef USE_MMAP
|
||||
ifc_file = new IfcParse::IfcFile(filename, mmap);
|
||||
if (mmap) {
|
||||
ifc_file->initialize(filename, mmap);
|
||||
requires_init = false;
|
||||
}
|
||||
#else
|
||||
(void)mmap;
|
||||
ifc_file = new IfcParse::IfcFile(filename);
|
||||
(void)mmap;
|
||||
#endif
|
||||
}
|
||||
if (requires_init) {
|
||||
ifc_file->initialize(filename);
|
||||
}
|
||||
|
||||
if (!ifc_file || !ifc_file->good()) {
|
||||
Logger::Error("Unable to parse input file '" + filename + "'");
|
||||
|
||||
@@ -24,7 +24,8 @@ import importlib
|
||||
import ifcopenshell.util.selector
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
from typing import Literal, Union, Any, Callable, TYPE_CHECKING
|
||||
from typing import Literal, Union, Any, TYPE_CHECKING
|
||||
from collections.abc import Callable
|
||||
|
||||
try:
|
||||
from openpyxl import Workbook
|
||||
|
||||
@@ -367,11 +367,13 @@ void IfcGeom::Iterator::create_element_(ifcopenshell::geometry::Converter* kerne
|
||||
}));
|
||||
|
||||
if (!brep) {
|
||||
Logger::SetProduct(boost::none);
|
||||
return;
|
||||
}
|
||||
|
||||
auto elem = process_based_on_settings(settings, brep);
|
||||
if (!elem) {
|
||||
Logger::SetProduct(boost::none);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -394,6 +396,8 @@ void IfcGeom::Iterator::create_element_(ifcopenshell::geometry::Converter* kerne
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger::SetProduct(boost::none);
|
||||
}
|
||||
|
||||
IfcGeom::Element* IfcGeom::Iterator::process_based_on_settings(ifcopenshell::geometry::Settings settings, IfcGeom::BRepElement* elem, IfcGeom::TriangulationElement* previous)
|
||||
|
||||
@@ -172,6 +172,7 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_,
|
||||
}
|
||||
|
||||
auto interpolated_loop = polygon_from_points(points);
|
||||
interpolated_loop->external = w1->external;
|
||||
if (interpolated->kind() == taxonomy::FACE) {
|
||||
std::static_pointer_cast<taxonomy::face>(interpolated)->children.push_back(interpolated_loop);
|
||||
} else {
|
||||
|
||||
@@ -118,7 +118,8 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
|
||||
auto jt = it + 1;
|
||||
std::array<taxonomy::item::ptr, 2> fa = { *it, *jt };
|
||||
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) {
|
||||
if (fa[i]->kind() == taxonomy::FACE) {
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
// @todo this is only outer wire
|
||||
|
||||
if (shps[i].ShapeType() == TopAbs_FACE) {
|
||||
ws[i] = BRepTools::OuterWire(TopoDS::Face(shps[i]));
|
||||
ws[0][i] = BRepTools::OuterWire(TopoDS::Face(shps[i]));
|
||||
size_t j = 1;
|
||||
for (TopExp_Explorer exp(shps[i], TopAbs_WIRE); exp.More(); exp.Next()) {
|
||||
if (exp.Current() != ws[0][i]) {
|
||||
while (ws.size() <= j) {
|
||||
ws.emplace_back();
|
||||
}
|
||||
ws[j++][i] = TopoDS::Wire(exp.Current());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ws[i] = TopoDS::Wire(shps[i]);
|
||||
ws[0][i] = TopoDS::Wire(shps[i]);
|
||||
}
|
||||
}
|
||||
if (shps[0].ShapeType() == TopAbs_FACE) {
|
||||
@@ -154,35 +164,38 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
|
||||
BB.Add(comp, shps[1]);
|
||||
}
|
||||
}
|
||||
BRepTools_WireExplorer a(ws[0]);
|
||||
BRepTools_WireExplorer b(ws[1]);
|
||||
for (; a.More() && b.More(); a.Next(), b.Next()) {
|
||||
auto& e1 = a.Current();
|
||||
// auto e3 = TopoDS::Edge(b.Current().Reversed());
|
||||
auto& e3 = b.Current();
|
||||
|
||||
// Documentation says unconnected edges are automatically connected, but this is not the case
|
||||
TopoDS_Vertex e1a, e1b, e3a, e3b;
|
||||
TopExp::Vertices(e1, e1a, e1b, true);
|
||||
TopExp::Vertices(e3, e3a, e3b, true);
|
||||
auto e2 = BRepBuilderAPI_MakeEdge(e1b, e3a).Edge();
|
||||
auto e4 = BRepBuilderAPI_MakeEdge(e3b, e1a).Edge();
|
||||
|
||||
/*
|
||||
BRepFill_Filling fill;
|
||||
fill.Add(e1, GeomAbs_C0);
|
||||
fill.Add(e2, GeomAbs_C0);
|
||||
fill.Add(e3, GeomAbs_C0);
|
||||
fill.Add(e4, GeomAbs_C0);
|
||||
fill.Build();
|
||||
// faces.Append(fill.Face());
|
||||
BB.Add(comp, fill.Face());
|
||||
*/
|
||||
for (auto& wp : ws) {
|
||||
BRepTools_WireExplorer a(wp[0]);
|
||||
BRepTools_WireExplorer b(wp[1]);
|
||||
for (; a.More() && b.More(); a.Next(), b.Next()) {
|
||||
auto& e1 = a.Current();
|
||||
// auto e3 = TopoDS::Edge(b.Current().Reversed());
|
||||
auto& e3 = b.Current();
|
||||
|
||||
auto f = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e1a, e1b, e3b, true).Wire()).Face();
|
||||
BB.Add(comp, f);
|
||||
auto g = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e3b, e3a, e1a, true).Wire()).Face();
|
||||
BB.Add(comp, g);
|
||||
// Documentation says unconnected edges are automatically connected, but this is not the case
|
||||
TopoDS_Vertex e1a, e1b, e3a, e3b;
|
||||
TopExp::Vertices(e1, e1a, e1b, true);
|
||||
TopExp::Vertices(e3, e3a, e3b, true);
|
||||
auto e2 = BRepBuilderAPI_MakeEdge(e1b, e3a).Edge();
|
||||
auto e4 = BRepBuilderAPI_MakeEdge(e3b, e1a).Edge();
|
||||
|
||||
/*
|
||||
BRepFill_Filling fill;
|
||||
fill.Add(e1, GeomAbs_C0);
|
||||
fill.Add(e2, GeomAbs_C0);
|
||||
fill.Add(e3, GeomAbs_C0);
|
||||
fill.Add(e4, GeomAbs_C0);
|
||||
fill.Build();
|
||||
// faces.Append(fill.Face());
|
||||
BB.Add(comp, fill.Face());
|
||||
*/
|
||||
|
||||
auto f = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e1a, e1b, e3b, true).Wire()).Face();
|
||||
BB.Add(comp, f);
|
||||
auto g = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakePolygon(e3b, e3a, e1a, true).Wire()).Face();
|
||||
BB.Add(comp, g);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,62 +25,58 @@ using namespace ifcopenshell::geometry;
|
||||
|
||||
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAxis2PlacementLinear* inst) {
|
||||
|
||||
if (!inst->Location()->as<IfcSchema::IfcPointByDistanceExpression>())
|
||||
Logger::Error(std::runtime_error("Location must be IfcPointByDistanceExpression for IfcAxis2PlacementLinear"));
|
||||
if (!inst->Location()->as<IfcSchema::IfcPointByDistanceExpression>()) {
|
||||
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()));
|
||||
o = m->components().col(3).head<3>();
|
||||
taxonomy::matrix4::ptr m = taxonomy::cast<taxonomy::matrix4>(map(inst->Location()));
|
||||
o = m->components().col(3).head<3>();
|
||||
|
||||
// From 8.9.3.4 IfcAxis2PlacementLinear there are 4 cases that need to be considered
|
||||
// 1) Axis is given but not RefDirection
|
||||
// 2) RefDirection is given but not Axis
|
||||
// 3) Neither Axis or RefDirection are provided
|
||||
// 4) Both Axis and RefDirection are provided
|
||||
// From 8.9.3.4 IfcAxis2PlacementLinear there are 4 cases that need to be considered
|
||||
// 1) Axis is given but not RefDirection
|
||||
// 2) RefDirection is given but not Axis
|
||||
// 3) Neither Axis or RefDirection are provided
|
||||
// 4) Both Axis and RefDirection are provided
|
||||
|
||||
const bool hasAxis = inst->Axis() != nullptr;
|
||||
const bool hasRef = inst->RefDirection() != nullptr;
|
||||
const bool hasAxis = inst->Axis() != nullptr;
|
||||
const bool hasRef = inst->RefDirection() != nullptr;
|
||||
|
||||
if (hasAxis != hasRef) {
|
||||
/*
|
||||
if (hasAxis != hasRef) {
|
||||
Logger::Warning("Axis and RefDirection should be specified together", inst);
|
||||
}
|
||||
*/
|
||||
|
||||
if (hasAxis && !hasRef)
|
||||
{
|
||||
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
|
||||
axis = *a->components_;
|
||||
if (hasAxis && !hasRef) {
|
||||
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
|
||||
axis = *a->components_;
|
||||
|
||||
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
|
||||
// refDirection is not necessarily orthogonal to axis.
|
||||
// axis.cross(refDirection) gives y. y.cross(axis) gives x=refDirection
|
||||
refDirection = axis.cross(refDirection).cross(axis);
|
||||
}
|
||||
else if (!hasAxis && hasRef)
|
||||
{
|
||||
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
|
||||
refDirection = *r->components_;
|
||||
Eigen::Vector3d up(0, 0, 1);
|
||||
axis = refDirection.cross(up.cross(refDirection));
|
||||
}
|
||||
else if (!hasAxis && !hasRef)
|
||||
{
|
||||
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
|
||||
Eigen::Vector3d up(0, 0, 1);
|
||||
axis = refDirection.cross(up.cross(refDirection));
|
||||
}
|
||||
else
|
||||
{
|
||||
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
|
||||
axis = *a->components_;
|
||||
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
|
||||
// refDirection is not necessarily orthogonal to axis.
|
||||
// axis.cross(refDirection) gives y. y.cross(axis) gives x=refDirection
|
||||
refDirection = axis.cross(refDirection).cross(axis);
|
||||
} else if (!hasAxis && hasRef) {
|
||||
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
|
||||
refDirection = *r->components_;
|
||||
Eigen::Vector3d up(0, 0, 1);
|
||||
axis = refDirection.cross(up.cross(refDirection));
|
||||
} else if (!hasAxis && !hasRef) {
|
||||
refDirection = m->components().col(0).head<3>(); // RefDirection is the curve tangent when omitted
|
||||
Eigen::Vector3d up(0, 0, 1);
|
||||
axis = refDirection.cross(up.cross(refDirection));
|
||||
} else {
|
||||
taxonomy::direction3::ptr a = taxonomy::cast<taxonomy::direction3>(map(inst->Axis()));
|
||||
axis = *a->components_;
|
||||
|
||||
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
|
||||
refDirection = *r->components_;
|
||||
refDirection = axis.cross(refDirection).cross(axis); // refDirection needs to be orthogonal to axis
|
||||
}
|
||||
taxonomy::direction3::ptr r = taxonomy::cast<taxonomy::direction3>(map(inst->RefDirection()));
|
||||
refDirection = *r->components_;
|
||||
refDirection = axis.cross(refDirection).cross(axis); // refDirection needs to be orthogonal to axis
|
||||
}
|
||||
|
||||
// axis and refDirection need to be orthogonal
|
||||
return taxonomy::make<taxonomy::matrix4>(o, axis, refDirection);
|
||||
// axis and refDirection need to be orthogonal
|
||||
return taxonomy::make<taxonomy::matrix4>(o, axis, refDirection);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -24,7 +24,7 @@ using namespace ifcopenshell::geometry;
|
||||
|
||||
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcBoundingBox* inst) {
|
||||
if (!settings_.get<settings::KeepBoundingBoxes>().get()) {
|
||||
// @todo make sure it doesn't log.
|
||||
failed_on_purpose_.insert(inst);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,13 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in
|
||||
rot = taxonomy::matrix4(
|
||||
Eigen::Vector3d(0, 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);
|
||||
}
|
||||
if (faces.size() != profile_offsets.size()) {
|
||||
|
||||
@@ -82,7 +82,14 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) {
|
||||
rot = taxonomy::matrix4(
|
||||
Eigen::Vector3d(0, 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);
|
||||
}
|
||||
#else
|
||||
|
||||
@@ -372,6 +372,18 @@ const IfcUtil::IfcBaseEntity* mapping::get_single_material_association(const Ifc
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SCHEMA_HAS_IfcMaterialConstituentSet
|
||||
if (associated_material->as<IfcSchema::IfcMaterialConstituentSet>()) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ endif
|
||||
ifeq ($(PYVERSION), py313)
|
||||
PYNUMBER:=313
|
||||
endif
|
||||
ifeq ($(PYVERSION), py314)
|
||||
PYNUMBER:=314
|
||||
endif
|
||||
ifndef PYNUMBER
|
||||
$(error Unsupported PYVERSION '$(PYVERSION)')
|
||||
endif
|
||||
@@ -76,6 +79,11 @@ build-urls:
|
||||
test:
|
||||
pytest -p no:pytest-blender test --ignore=test/util/test_shape_builder.py
|
||||
|
||||
.PHONY: test-parallel
|
||||
test-parallel:
|
||||
@NPROCS=$$(nproc 2>/dev/null || sysctl -n hw.ncpu); \
|
||||
pytest -p no:pytest-blender -n $$NPROCS test --ignore=test/util/test_shape_builder.py
|
||||
|
||||
.PHONY: test-mathutils
|
||||
test-mathutils:
|
||||
pytest -p no:pytest-blender test/util/test_shape_builder.py
|
||||
|
||||
@@ -43,6 +43,7 @@ Examples
|
||||
Learning how to use the bSDD is best done by reading the official Swagger API docs.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from bsdd import Client,apply_ifc_classification_properties
|
||||
from pprint import pprint
|
||||
|
||||
|
||||
@@ -17,4 +17,5 @@ capabilities of the C++ core are available in Python.
|
||||
ifcopenshell-python/geometry_creation
|
||||
ifcopenshell-python/geometry_tree
|
||||
ifcopenshell-python/selector_syntax
|
||||
ifcopenshell-python/developer_guide
|
||||
ifcopenshell-python/schema_querying
|
||||
ifcopenshell-python/validation
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
Developer Guide
|
||||
===============
|
||||
|
||||
The core module implements low-level functionality to read and write IFC data. This includes:
|
||||
|
||||
- Reading IFC data from different serialisations into Python objects
|
||||
- Accessing direct and indirect attributes of IFC entities
|
||||
- Creating IFC entities
|
||||
- Generating GlobalIds
|
||||
- Removing IFC entities and all references
|
||||
- Modifying IFC direct attributes
|
||||
- Checking IFC class inheritance
|
||||
- Validating IFC data
|
||||
|
||||
TODO
|
||||
@@ -44,6 +44,8 @@ ZIP packages
|
||||
+-------------+---------------------------------+-------------------------------+---------------------------------+-----------------------------------+
|
||||
| Python 3.13 | :ios_python_url:`py313-linux64` | :ios_python_url:`py313-win64` | :ios_python_url:`py313-macos64` | :ios_python_url:`py313-macosm164` |
|
||||
+-------------+---------------------------------+-------------------------------+---------------------------------+-----------------------------------+
|
||||
| Python 3.14 | :ios_python_url:`py314-linux64` | :ios_python_url:`py314-win64` | :ios_python_url:`py314-macos64` | :ios_python_url:`py314-macosm164` |
|
||||
+-------------+---------------------------------+-------------------------------+---------------------------------+-----------------------------------+
|
||||
|
||||
2. Unzip the downloaded file and copy the ``ifcopenshell`` directory into your
|
||||
Python path. If you're not sure where your Python path is, run the following
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
Schema querying
|
||||
===============
|
||||
|
||||
Schema declarations
|
||||
-------------------
|
||||
|
||||
IfcOpenShell can query the IFC schema itself without instantiating or loading an IFC dataset.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import ifcopenshell
|
||||
ifc4 = ifcopenshell.schema_by_name("IFC4")
|
||||
|
||||
A schema definition is known as a declaration. You may loop through all declarations or retrieve a declaration by name. All declarations have a name.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for declaration in ifc4.declarations():
|
||||
print(declaration.name()) # 'IfcAbsorbedDoseMeasure', 'IfcAccelerationMeasure', 'IfcActionRequest', ...
|
||||
|
||||
ifcwall = ifc4.declaration_by_name("IfcWall")
|
||||
|
||||
You can check if an entity is abstract, and retrive both the supertype and subtypes of an entity:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(ifcwall.is_abstract()) # False
|
||||
print(ifcwall.supertype()) # <entity IfcBuildingElement>
|
||||
print(ifcwall.subtypes()) # (<entity IfcWallElementedCase>, <entity IfcWallStandardCase>)
|
||||
|
||||
You can retrieve only the direct attributes of an entity, or all the direct attributes including inherited attributes, or inverse attributes:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(ifcwall.attributes())
|
||||
print(ifcwall.all_attributes())
|
||||
print(ifcwall.all_inverse_attributes())
|
||||
|
||||
buildingSMART property set templates
|
||||
------------------------------------
|
||||
|
||||
For each IFC schema version, buildingSMART publishes built in property and quantity set templates for standardised properties. These define property names, property sets, data types, and which IFC class they are applicable to. You can query these templates.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import ifcopenshell.util.pset
|
||||
templates = ifcopenshell.util.pset.PsetQto("IFC4")
|
||||
|
||||
To get just the names of applicable templates for an entity:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# ['Pset_EnvironmentalImpactIndicators', 'Pset_EnvironmentalImpactValues', 'Pset_WallCommon', 'Qto_WallBaseQuantities', ...]
|
||||
print(templates.get_applicable_names("IfcWall"))
|
||||
|
||||
They may also be retrieved as an ``IfcPropertySetTemplate`` entity:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(templates.get_applicable("IfcWall"))
|
||||
|
||||
A single template may be retrieved by name:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
templates.get_by_name('Pset_WallCommon')
|
||||
|
||||
You may add your own IFC files containing pset template definitions:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
my_pset_library = ifcopenshell.open('/path/to/library.ifc')
|
||||
templates.templates.append(my_pset_library)
|
||||
@@ -0,0 +1,78 @@
|
||||
Validation
|
||||
==========
|
||||
|
||||
SPF syntax validation
|
||||
---------------------
|
||||
|
||||
IfcOpenShell can validate whether or not an IFC-SPF file contains correct SPF syntax.
|
||||
|
||||
.. code-block::
|
||||
|
||||
$ python -m ifcopenshell.simple_spf path/to/model.ifc
|
||||
Valid
|
||||
|
||||
Here are some examples of failures:
|
||||
|
||||
.. code-block::
|
||||
|
||||
$ python -m ifcopenshell.simple_spf fixtures/fail_double_comma.ifc
|
||||
On line 8 column 21:
|
||||
Unexpected comma (',')
|
||||
Expecting one of DBLQUOTE DOT HASH INT LPAR NONE QUOTE REAL STAR UPPER
|
||||
00008 | #1=IFCPERSON($,$,'',,$,$,$,$);
|
||||
^
|
||||
|
||||
$ python -m ifcopenshell.simple_spf fixtures/fail_double_semi.ifc
|
||||
On line 27 column 66:
|
||||
Unexpected semicolon (';')
|
||||
Expecting one of ENDSEC HASH
|
||||
00027 | #20=IFCPROJECT('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);;
|
||||
^
|
||||
|
||||
$ python -m ifcopenshell.simple_spf fixtures/fail_duplicate_id.ifc
|
||||
On line 27:
|
||||
Duplicate instance name #19
|
||||
00027 | #19=IFCPROJECT('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
$ python -m ifcopenshell.simple_spf fixtures/fail_no_header.ifc
|
||||
On line 2 column 1:
|
||||
Unexpected hex ('F')
|
||||
Expecting HEADER
|
||||
00002 | FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
|
||||
^
|
||||
|
||||
The optional ``--json`` argument may be used to instead get results in JSON.
|
||||
|
||||
.. code-block::
|
||||
|
||||
$ python -m ifcopenshell.simple_spf test.ifc
|
||||
{"type": "unexpected_token", "lineno": 8, "column": 48, "found_type": "semicolon", "found_value": ";", "expected": ["ENDSEC"], "line": "#1= IFCPERSON($,'Nicht definiert',$,$,$,$,$,$);;", "message": "On line 8 column 48:\nUnexpected semicolon (';')\nExpecting ENDSEC\n00008 | #1= IFCPERSON($,'Nicht definiert',$,$,$,$,$,$);;\n ^"}
|
||||
|
||||
IFC schema validation
|
||||
---------------------
|
||||
|
||||
IfcOpenShell can validate models against the IFC schema itself. It checks against attributes, entity names, data types, cardinality, and where rules.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ python -m ifcopenshell.validate -h
|
||||
|
||||
usage: validate.py [-h] [--rules] [--json] [--fields] [--spf] files [files ...]
|
||||
|
||||
positional arguments:
|
||||
files The IFC file to validate.
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--rules Run express rules.
|
||||
--json Output in JSON format.
|
||||
--fields Output more detailed information about failed entities (only with --json).
|
||||
--spf Output entities in SPF format (only with --json).
|
||||
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m ifcopenshell.validate /path/to/model.ifc --rules
|
||||
@@ -54,27 +54,27 @@ IfcOpenShell is a modular ecosystem of tools that work together, where each tool
|
||||
.. csv-table::
|
||||
:header: "Name", "Description"
|
||||
|
||||
"**IfcOpenShell**", "The core library for C++ developers. The library includes the ability to parse schemas, tessellate and process implicit geometry."
|
||||
"**IfcOpenShell-Python**", "Python bindings to the core IfcOpenShell C++ system, as well as high level analysis and authoring functions."
|
||||
"**IfcConvert**", "A command-line application for converting IFC geometry into file formats such as OBJ, DAE, GLB, STP, IGS, XML, SVG, H5, and IFC itself."
|
||||
"**Bonsai**", "A graphical add-on for Blender that lets you analyse, author, and modify IFC with Blender. Graphically create BIM models from scratch!"
|
||||
"**BCF**", "BIM Collaboration Format (BCF) is a standard to manage and exchange coordination topics between disciplines collaborating on a project by changing XML files or querying an API."
|
||||
"**BIMServer-Plugin**", "A plugin to the open source BIMServer CDE to allow you to use IfcOpenShell to parse, view, and audit models."
|
||||
"**BIMTester**", "A utility that allows you to write Gherkin-based tests for models."
|
||||
"**bSDD**", "A Python library to query the buildingSMART Data Dictionary API to search for standardised classifications and properties."
|
||||
"**Ifc2CA**", "Converts IFC models to FEM structural analytical models to be used in Code_Aster."
|
||||
"**Ifc4D**", "A series of utilities for converting to and from various 4D software like MS Project, PowerProject, and Oracle P6."
|
||||
"**Ifc5D**", "A collection of utilities of manipulating cost-related data to and from formats, reports, and optimisation engines."
|
||||
"**IfcCityJSON**", "A converter for CityJSON files and IFC. It currently only supports one-way conversion from CityJSON to IFC."
|
||||
"**IfcClash**", "A CLI utility and library that lets you perform clash detection on one or more IFC models. Clashes are defined in terms of clash sets with filters using the IFC query syntax."
|
||||
"**IfcCSV**", "View and edit IFC data using spreadsheets or tabular datasets, such as CSV, ODS, XLSX, Pandas DataFrames, and regular Python lists."
|
||||
"**IfcDiff**", "A CLI utility and library that lets you compare the changes between two IFC models."
|
||||
"**IfcFM**", "A highly standards-compliant tool (e.g. COBie 2.4, COBie 3.0, AOH-BSEM) to convert FM data in IFC databases to spreadsheets and other machine readable formats, such as ODS, XLSX, CSV, Pandas, XML, and JSON."
|
||||
"**IfcMax**", "A 3ds Max importer plugin able to import the IFC file format."
|
||||
"**IfcPatch**", "A CLI utility and library that lets you run and distribute predetermined modifications on an IFC file, known as a patch recipe. Useful in deploying a data pipeline or batch-fixing external models."
|
||||
"**IfcSverchok**", "A node based visual programming add-on for Blender to interact with IFC and Sverchok."
|
||||
"**IfcTester**", "Author and read Information Delivery Specification (IDS) files. You can validate IFC models against IDS and generate reports in multiple formats. It works from the command line, as a web app, or as a library."
|
||||
"**VoxelisationToolkit**", "Converts .ifc geometry into voxels, and lets you perform voxel based geometric analysis."
|
||||
"`IfcOpenShell <https://docs.ifcopenshell.org/ifcopenshell.html>`_", "The core library for C++ developers. The library includes the ability to parse schemas, tessellate and process implicit geometry."
|
||||
"`IfcOpenShell-Python <https://docs.ifcopenshell.org/ifcopenshell-python.html>`_", "Python bindings to the core IfcOpenShell C++ system, as well as high level analysis and authoring functions."
|
||||
"`IfcConvert <https://docs.ifcopenshell.org/ifcconvert.html>`_", "A command-line application for converting IFC geometry into file formats such as OBJ, DAE, GLB, STP, IGS, XML, SVG, H5, and IFC itself."
|
||||
"`Bonsai <https://docs.ifcopenshell.org/bonsai.html>`_", "A graphical add-on for Blender that lets you analyse, author, and modify IFC with Blender. Graphically create BIM models from scratch!"
|
||||
"`BCF <https://docs.ifcopenshell.org/bcf.html>`_", "BIM Collaboration Format (BCF) is a standard to manage and exchange coordination topics between disciplines collaborating on a project by changing XML files or querying an API."
|
||||
"`BIMServer-Plugin <https://docs.ifcopenshell.org/bimserver-plugin.html>`_", "A plugin to the open source BIMServer CDE to allow you to use IfcOpenShell to parse, view, and audit models."
|
||||
"`BIMTester <https://docs.ifcopenshell.org/bimtester.html>`_", "A utility that allows you to write Gherkin-based tests for models."
|
||||
"`bSDD <https://docs.ifcopenshell.org/bsdd.html>`_", "A Python library to query the buildingSMART Data Dictionary API to search for standardised classifications and properties."
|
||||
"`Ifc2CA <https://docs.ifcopenshell.org/ifc2ca.html>`_", "Converts IFC models to FEM structural analytical models to be used in Code_Aster."
|
||||
"`Ifc4D <https://docs.ifcopenshell.org/ifc4d.html>`_", "A series of utilities for converting to and from various 4D software like MS Project, PowerProject, and Oracle P6."
|
||||
"`Ifc5D <https://docs.ifcopenshell.org/ifc5d.html>`_", "A collection of utilities of manipulating cost-related data to and from formats, reports, and optimisation engines."
|
||||
"`IfcCityJSON <https://docs.ifcopenshell.org/ifccityjson.html>`_", "A converter for CityJSON files and IFC. It currently only supports one-way conversion from CityJSON to IFC."
|
||||
"`IfcClash <https://docs.ifcopenshell.org/ifcclash.html>`_", "A CLI utility and library that lets you perform clash detection on one or more IFC models. Clashes are defined in terms of clash sets with filters using the IFC query syntax."
|
||||
"`IfcCSV <https://docs.ifcopenshell.org/ifccsv.html>`_", "View and edit IFC data using spreadsheets or tabular datasets, such as CSV, ODS, XLSX, Pandas DataFrames, and regular Python lists."
|
||||
"`IfcDiff <https://docs.ifcopenshell.org/ifcdiff.html>`_", "A CLI utility and library that lets you compare the changes between two IFC models."
|
||||
"`IfcFM <https://docs.ifcopenshell.org/ifcfm.html>`_", "A highly standards-compliant tool (e.g. COBie 2.4, COBie 3.0, AOH-BSEM) to convert FM data in IFC databases to spreadsheets and other machine readable formats, such as ODS, XLSX, CSV, Pandas, XML, and JSON."
|
||||
"`IfcMax <https://docs.ifcopenshell.org/ifcmax.html>`_", "A 3ds Max importer plugin able to import the IFC file format."
|
||||
"`IfcPatch <https://docs.ifcopenshell.org/ifcpatch.html>`_", "A CLI utility and library that lets you run and distribute predetermined modifications on an IFC file, known as a patch recipe. Useful in deploying a data pipeline or batch-fixing external models."
|
||||
"`IfcSverchok <https://docs.ifcopenshell.org/ifcsverchok.html>`_", "A node based visual programming add-on for Blender to interact with IFC and Sverchok."
|
||||
"`IfcTester <https://docs.ifcopenshell.org/ifctester.html>`_", "Author and read Information Delivery Specification (IDS) files. You can validate IFC models against IDS and generate reports in multiple formats. It works from the command line, as a web app, or as a library."
|
||||
"`VoxelisationToolkit <https://github.com/opensourceBIM/voxelization_toolkit>`_", "Converts .ifc geometry into voxels, and lets you perform voxel based geometric analysis."
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ import zipfile
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union, TYPE_CHECKING, Any, overload, Literal
|
||||
from collections.abc import Sequence
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ifcopenshell.express.schema_class
|
||||
@@ -138,7 +139,12 @@ def open(
|
||||
path: Union[os.PathLike, str], format: Optional[str] = None, *, should_stream: bool = False, readonly: bool = False
|
||||
) -> Union[_file, sqlite, _stream]: ...
|
||||
def open(
|
||||
path: Union[os.PathLike, str], format: Optional[str] = None, should_stream: bool = False, readonly: bool = False
|
||||
path: Union[os.PathLike, str],
|
||||
format: Optional[str] = None,
|
||||
should_stream: bool = False,
|
||||
readonly: bool = False,
|
||||
mmap: bool = False,
|
||||
bypass_types: Optional[Sequence[str]] = None,
|
||||
) -> Union[_file, sqlite, _stream]:
|
||||
"""Loads an IFC dataset from a filepath
|
||||
|
||||
@@ -186,7 +192,17 @@ def open(
|
||||
if should_stream:
|
||||
return stream(path)
|
||||
if readonly: # Temporary conditional see #7131. Remove once newer builds don't segfault on Linux.
|
||||
f = ifcopenshell_wrapper.open(str(path.absolute()), readonly)
|
||||
f = ifcopenshell_wrapper.open(str(path.absolute()), readonly=readonly)
|
||||
elif bypass_types:
|
||||
f = ifcopenshell_wrapper.file(ifcopenshell_wrapper.uninitialized_tag())
|
||||
for ty in bypass_types:
|
||||
f.bypass_type(ty)
|
||||
if mmap:
|
||||
f.initialize(str(path.absolute()), mmap=mmap)
|
||||
else:
|
||||
f.initialize(str(path.absolute()))
|
||||
elif mmap:
|
||||
f = ifcopenshell_wrapper.open(str(path.absolute()), mmap=mmap)
|
||||
else:
|
||||
f = ifcopenshell_wrapper.open(str(path.absolute()))
|
||||
return file(f)
|
||||
@@ -299,20 +315,44 @@ def guess_format(path: Path) -> Literal[".ifc", ".ifcZIP", ".ifcXML", ".ifcJSON"
|
||||
return None
|
||||
|
||||
|
||||
def stream2(path: Union[Path, str]):
|
||||
def stream2(path: Union[Path, str], mmap: bool = False, page_size: int = 0):
|
||||
"""Streams the content of a file path from disk, yielding each instance
|
||||
as a dictionary.
|
||||
|
||||
Args:
|
||||
path (Union[Path, str]): input file path
|
||||
mmap (bool): open the file contents using memory mapping
|
||||
page_size (int): open file in python and feed chunks to the parser
|
||||
|
||||
Yields:
|
||||
dict: entity instance dictionaries
|
||||
"""
|
||||
streamer = ifcopenshell_wrapper.InstanceStreamer(str(path))
|
||||
while streamer:
|
||||
if inst := streamer.read_instance_py():
|
||||
yield inst
|
||||
if page_size:
|
||||
import builtins
|
||||
|
||||
f = builtins.open(path, encoding="ascii")
|
||||
strm = ifcopenshell_wrapper.InstanceStreamer()
|
||||
strm.pushPage(f.read(page_size))
|
||||
finished = False
|
||||
while True:
|
||||
while strm.hasSemicolon():
|
||||
if inst := strm.readInstancePy():
|
||||
yield inst
|
||||
else:
|
||||
finished = True
|
||||
break
|
||||
if finished:
|
||||
break
|
||||
else:
|
||||
if data := f.read(page_size):
|
||||
strm.pushPage(data)
|
||||
else:
|
||||
break
|
||||
else:
|
||||
streamer = ifcopenshell_wrapper.InstanceStreamer(str(path), mmap)
|
||||
while streamer:
|
||||
if inst := streamer.readInstancePy():
|
||||
yield inst
|
||||
|
||||
|
||||
def stream2_from_string(data: str):
|
||||
|
||||
@@ -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
|
||||
@@ -248,6 +248,10 @@ READ_ERROR = ifcopenshell_wrapper.file_open_status.READ_ERROR
|
||||
NO_HEADER = ifcopenshell_wrapper.file_open_status.NO_HEADER
|
||||
UNSUPPORTED_SCHEMA = ifcopenshell_wrapper.file_open_status.UNSUPPORTED_SCHEMA
|
||||
INVALID_SYNTAX = ifcopenshell_wrapper.file_open_status.INVALID_SYNTAX
|
||||
try:
|
||||
UNKNOWN = ifcopenshell_wrapper.file_open_status.UNKNOWN
|
||||
except:
|
||||
UNKNOWN = 5 # Workaround
|
||||
|
||||
import struct
|
||||
|
||||
@@ -496,6 +500,24 @@ class rocksdb_file_storage:
|
||||
return "".join("".join(map(str, t)) if t[1] else "" for t in zip(prefixes, version_tuple[0:2]))
|
||||
|
||||
|
||||
class file_header:
|
||||
def __init__(self, file, header_data):
|
||||
self.file = file
|
||||
self.header_data = header_data
|
||||
|
||||
@property
|
||||
def file_description(self) -> entity_instance:
|
||||
return entity_instance.wrap_value(self.header_data.file_description_py(), file=self.file)
|
||||
|
||||
@property
|
||||
def file_name(self) -> entity_instance:
|
||||
return entity_instance.wrap_value(self.header_data.file_name_py(), file=self.file)
|
||||
|
||||
@property
|
||||
def file_schema(self) -> entity_instance:
|
||||
return entity_instance.wrap_value(self.header_data.file_schema_py(), file=self.file)
|
||||
|
||||
|
||||
class file:
|
||||
"""Base class for containing IFC files.
|
||||
|
||||
@@ -513,7 +535,7 @@ class file:
|
||||
"""
|
||||
|
||||
wrapped_data: ifcopenshell_wrapper.file
|
||||
header: ifcopenshell_wrapper.IfcSpfHeader
|
||||
header: file_header
|
||||
units: dict[str, entity_instance] = {}
|
||||
history_size: int = 64
|
||||
history: list[Transaction]
|
||||
@@ -586,8 +608,11 @@ class file:
|
||||
"Unsupported schema: %s" % ",".join(self.header.file_schema.schema_identifiers),
|
||||
),
|
||||
INVALID_SYNTAX: lambda: (Error, "Syntax error during parse, check logs"),
|
||||
# This is the case when passing uninitialized_tag
|
||||
UNKNOWN: lambda: (None, None),
|
||||
}[f.good().value()]()
|
||||
raise exc(msg)
|
||||
if exc is not None:
|
||||
raise exc(msg)
|
||||
else:
|
||||
args = filter(None, [schema])
|
||||
args = map(ifcopenshell_wrapper.schema_by_name, args)
|
||||
@@ -1025,12 +1050,10 @@ class file:
|
||||
|
||||
@property
|
||||
def header(self):
|
||||
try: # Temporary workaround until new builds are ready. See #7131.
|
||||
h = self.wrapped_data.header()
|
||||
try:
|
||||
return file_header(self, self.wrapped_data.header())
|
||||
except:
|
||||
return self.wrapped_data.header
|
||||
object.__setattr__(h, "file_ref", lambda inst: entity_instance.wrap_value(inst, file=self))
|
||||
return h
|
||||
|
||||
@property
|
||||
def storage(self) -> Optional[rocksdb_file_storage]:
|
||||
|
||||
@@ -22,7 +22,7 @@ import os
|
||||
import sys
|
||||
import operator
|
||||
|
||||
from .. import ifcopenshell_wrapper
|
||||
from .. import open, ifcopenshell_wrapper
|
||||
from ..file import file
|
||||
from ..entity_instance import entity_instance
|
||||
|
||||
@@ -307,10 +307,7 @@ class iterator(ifcopenshell_wrapper.Iterator):
|
||||
self.file = file
|
||||
file_or_filename = file_or_filename.wrapped_data
|
||||
else:
|
||||
# @todo?
|
||||
self.file = None
|
||||
# Makes sure people are able to use python's platform agnostic paths
|
||||
file_or_filename = os.path.abspath(file_or_filename)
|
||||
file_or_filename = self.file = open(file_or_filename)
|
||||
|
||||
if include is not None and exclude is not None:
|
||||
raise ValueError("include and exclude cannot be specified simultaneously")
|
||||
@@ -342,7 +339,9 @@ class iterator(ifcopenshell_wrapper.Iterator):
|
||||
geometry_library, self.settings, file_or_filename, include_or_exclude, include is not None, num_threads
|
||||
)
|
||||
else:
|
||||
ifcopenshell_wrapper.Iterator.__init__(self, geometry_library, settings, file_or_filename, num_threads)
|
||||
self.this = ifcopenshell_wrapper.construct_iterator(
|
||||
geometry_library, self.settings, file_or_filename, num_threads
|
||||
)
|
||||
|
||||
if has_occ:
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user